├── src ├── scss │ ├── components │ │ ├── _hex.scss │ │ ├── _def-list.scss │ │ ├── _loading.scss │ │ ├── _btc-amount.scss │ │ ├── _modal-alert.scss │ │ └── _graph.scss │ ├── scenes │ │ ├── _toast.scss │ │ ├── _home.scss │ │ └── _layout.scss │ ├── _bootstrap-extensions.scss │ ├── app.scss │ └── _bootstrap-variables.scss ├── public │ ├── lnd-explorer.png │ ├── lnd-explorer-icon.png │ └── index.html ├── client │ ├── services │ │ ├── peer-helpers.js │ │ ├── rest-helpers.js │ │ └── socket.js │ ├── components │ │ ├── bool-value.jsx │ │ ├── btc-amount.jsx │ │ ├── timestamp.jsx │ │ ├── modal-alert.jsx │ │ ├── def-list.jsx │ │ ├── hex.jsx │ │ └── loading.jsx │ ├── scenes │ │ ├── channels │ │ │ ├── components │ │ │ │ ├── pending-closing-channels-list-header.jsx │ │ │ │ ├── pending-open-channels-list-header.jsx │ │ │ │ ├── pending-force-channels-list-header.jsx │ │ │ │ ├── open-channels-list-header.jsx │ │ │ │ ├── channels-list.jsx │ │ │ │ ├── pending-closing-channels-list-item.jsx │ │ │ │ ├── pending-force-channels-list-item.jsx │ │ │ │ ├── pending-open-channels-list-item.jsx │ │ │ │ ├── open-channels-list-item.jsx │ │ │ │ └── channels-card.jsx │ │ │ └── channels-scene.jsx │ │ ├── home │ │ │ ├── components │ │ │ │ ├── peers-card.jsx │ │ │ │ ├── active-channels-card.jsx │ │ │ │ ├── pending-channels-card.jsx │ │ │ │ ├── channel-balance-card.jsx │ │ │ │ ├── info-card.jsx │ │ │ │ ├── wallet-balance-card.jsx │ │ │ │ └── blockchain-card.jsx │ │ │ └── home-scene.jsx │ │ ├── network │ │ │ ├── components │ │ │ │ ├── network-graph-card.jsx │ │ │ │ ├── network-info-card.jsx │ │ │ │ ├── node-info-card.jsx │ │ │ │ └── network-graph.jsx │ │ │ └── network-scene.jsx │ │ ├── transactions │ │ │ ├── components │ │ │ │ ├── tx-list-card.jsx │ │ │ │ ├── tx-list.jsx │ │ │ │ └── tx-list-item.jsx │ │ │ └── transactions-scene.jsx │ │ ├── payments │ │ │ ├── components │ │ │ │ ├── payments-list-card.jsx │ │ │ │ ├── payments-list.jsx │ │ │ │ └── payments-list-item.jsx │ │ │ └── payments-scene.jsx │ │ ├── peers │ │ │ ├── peers-scene.jsx │ │ │ └── components │ │ │ │ ├── peers-list-card.jsx │ │ │ │ ├── peers-list-item.jsx │ │ │ │ └── peers-list.jsx │ │ ├── invoices │ │ │ ├── components │ │ │ │ ├── invoices-card.jsx │ │ │ │ ├── invoices-list.jsx │ │ │ │ └── invoices-list-item.jsx │ │ │ └── invoices-scene.jsx │ │ ├── toast │ │ │ ├── components │ │ │ │ └── toast.jsx │ │ │ └── toast-container.jsx │ │ ├── send-payment │ │ │ ├── components │ │ │ │ ├── decoded-payment-request.jsx │ │ │ │ └── send-payment-form.jsx │ │ │ └── send-payment-modal.jsx │ │ ├── connect-peer │ │ │ ├── components │ │ │ │ └── connect-peer-form.jsx │ │ │ └── connect-peer-modal.jsx │ │ ├── create-invoice │ │ │ ├── components │ │ │ │ └── create-invoice-form.jsx │ │ │ └── create-invoice-modal.jsx │ │ ├── new-address │ │ │ └── new-address-modal.jsx │ │ ├── open-channel │ │ │ ├── components │ │ │ │ └── open-channel-form.jsx │ │ │ └── open-channel-modal.jsx │ │ ├── disconnect-peer │ │ │ └── disconnect-peer-modal.jsx │ │ ├── sign-message │ │ │ └── sign-message-modal.jsx │ │ ├── tools-menu.jsx │ │ ├── verify-message │ │ │ └── verify-message-modal.jsx │ │ ├── layout.jsx │ │ └── close-channel │ │ │ └── close-channel-modal.jsx │ └── app.jsx └── server │ ├── api │ ├── api-address.js │ ├── api-home.js │ ├── api-message.js │ ├── api-peers.js │ ├── api-invoices.js │ ├── api-payments.js │ ├── api-transactions.js │ ├── api-channels.js │ └── api-network.js │ ├── lnd.js │ ├── wss.js │ └── server.js ├── .prettierrc ├── .babelrc ├── Dockerfile ├── .editorconfig ├── .eslintrc ├── LICENSE ├── .gitignore ├── README.md └── package.json /src/scss/components/_hex.scss: -------------------------------------------------------------------------------- 1 | .hex-value { 2 | .full { 3 | word-break: break-all; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/public/lnd-explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/altangent/lnd-explorer/HEAD/src/public/lnd-explorer.png -------------------------------------------------------------------------------- /src/scss/components/_def-list.scss: -------------------------------------------------------------------------------- 1 | .def-list { 2 | .def-list-label { 3 | font-weight: bold; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/public/lnd-explorer-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/altangent/lnd-explorer/HEAD/src/public/lnd-explorer-icon.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "tabWidth": 2 6 | } 7 | -------------------------------------------------------------------------------- /src/scss/components/_loading.scss: -------------------------------------------------------------------------------- 1 | .loading { 2 | margin-left: auto; 3 | margin-right: auto; 4 | text-align: center; 5 | } 6 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env","react"], 3 | "plugins": ["transform-class-properties", "transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /src/scss/components/_btc-amount.scss: -------------------------------------------------------------------------------- 1 | .btc-amount { 2 | min-width: 125px; 3 | .in-sat { 4 | font-size: 0.8em; 5 | font-style: italic; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | COPY . /lnd-explorer 4 | 5 | WORKDIR /lnd-explorer 6 | 7 | RUN npm install \ 8 | && npm run build 9 | 10 | CMD npm start 11 | -------------------------------------------------------------------------------- /src/client/services/peer-helpers.js: -------------------------------------------------------------------------------- 1 | export function peerSort(a, b) { 2 | if (a.pub_key < b.pub_key) return -1; 3 | if (a.pub_key > b.pub_key) return 1; 4 | return 0; 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /src/scss/components/_modal-alert.scss: -------------------------------------------------------------------------------- 1 | .modal-alert { 2 | .alert { 3 | border-radius: 0; 4 | margin-left: -1rem; 5 | margin-right: -1rem; 6 | border-left: 0; 7 | border-right: 0; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/client/services/rest-helpers.js: -------------------------------------------------------------------------------- 1 | export const parseJson = res => { 2 | if (res.status !== 200) 3 | return res.json().then(json => { 4 | throw new Error(json.details || json.message); 5 | }); 6 | else return res.json(); 7 | }; 8 | -------------------------------------------------------------------------------- /src/scss/scenes/_toast.scss: -------------------------------------------------------------------------------- 1 | .toast-container { 2 | position: fixed; 3 | z-index: 1000; 4 | top: 1.5rem; 5 | left: 50%; 6 | margin-left: -150px; 7 | 8 | .toast { 9 | width: 350px; 10 | margin-bottom: 0.75rem; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/scss/_bootstrap-extensions.scss: -------------------------------------------------------------------------------- 1 | .card-header-title { 2 | @extend .h3; 3 | } 4 | 5 | .nav-tabs { 6 | .nav-link { 7 | background-color: $comp-bg + 3%; 8 | } 9 | .nav-link:hover { 10 | background-color: $comp-bg; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/client/components/bool-value.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export const BoolValue = ({ value }) => {value ? 'True' : 'False'}; 5 | 6 | BoolValue.propTypes = { 7 | value: PropTypes.bool.isRequired, 8 | }; 9 | -------------------------------------------------------------------------------- /src/scss/components/_graph.scss: -------------------------------------------------------------------------------- 1 | .links line { 2 | stroke: #999; 3 | stroke-opacity: 0.6; 4 | } 5 | 6 | .links line.selected { 7 | stroke: #338dc9; 8 | stroke-width: 2px; 9 | } 10 | 11 | .nodes circle { 12 | fill: #555555; 13 | stroke: #aaa; 14 | stroke-width: 1.5px; 15 | } 16 | 17 | .nodes circle.selected { 18 | fill: #00558d; 19 | stroke: #338dc9; 20 | } 21 | -------------------------------------------------------------------------------- /src/client/scenes/channels/components/pending-closing-channels-list-header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const PendingClosingChannelsListHeader = () => ( 4 | 5 | Remote pub key 6 | Channel point 7 | Capacity 8 | Local balance 9 | Remote balance 10 | Closing tx id 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /src/client/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import { Layout } from './scenes/layout'; 5 | 6 | import { socketClient } from './services/socket'; 7 | socketClient.connect(); 8 | 9 | ReactDom.render( 10 | 11 | 12 | , 13 | document.getElementById('app') 14 | ); 15 | -------------------------------------------------------------------------------- /src/client/components/btc-amount.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export const BtcAmount = ({ satoshi = 0 }) => ( 5 |
6 |
{(satoshi / 1e8).toFixed(8)} BTC
7 |
{satoshi} sat
8 |
9 | ); 10 | 11 | BtcAmount.propTypes = { 12 | satoshi: PropTypes.any, 13 | }; 14 | -------------------------------------------------------------------------------- /src/client/components/timestamp.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import moment from 'moment'; 4 | 5 | export const Timestamp = ({ timestamp }) => { 6 | if (typeof timestamp === 'string') timestamp = parseInt(timestamp); 7 | return {timestamp ? moment.unix(timestamp).format('l LT') : 'N/A'}; 8 | }; 9 | 10 | Timestamp.propTypes = { 11 | timestamp: PropTypes.any, 12 | }; 13 | -------------------------------------------------------------------------------- /src/server/api/api-address.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const lnd = require('../lnd'); 3 | const app = express(); 4 | 5 | app.post('/api/address', (req, res, next) => createAddress(req, res).catch(next)); 6 | 7 | module.exports = app; 8 | 9 | async function createAddress(req, res) { 10 | let type = parseInt(req.body.type); 11 | let address = await lnd.client.newAddress({ type }); 12 | res.send({ address }); 13 | } 14 | -------------------------------------------------------------------------------- /src/scss/scenes/_home.scss: -------------------------------------------------------------------------------- 1 | .info-card { 2 | background: linear-gradient(to bottom, #00416c, #00558d); 3 | text-align: center; 4 | border-radius: 0.3rem; 5 | height: 180px; 6 | margin-bottom: 0.5rem; 7 | 8 | .title { 9 | font-size: 2em; 10 | color: $body-color + 24%; 11 | } 12 | .value { 13 | font-size: 5em; 14 | font-weight: bold; 15 | } 16 | .value.small { 17 | margin-top: 1.5rem; 18 | font-size: 3em; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/client/scenes/channels/components/pending-open-channels-list-header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const PendingOpenChannelsListHeader = () => ( 4 | 5 | Remote pub key 6 | Channel point 7 | Capacity 8 | Local balance 9 | Remote balance 10 | Commit fee 11 | Commit weight 12 | Fee per kiloweight 13 | Confirmation height 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /src/client/scenes/channels/components/pending-force-channels-list-header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const PendingForceChannelsListHeader = () => ( 4 | 5 | Remote pub key 6 | Channel point 7 | Capacity 8 | Local balance 9 | Remote balance 10 | Closing tx id 11 | Limbo balance 12 | Maturity height 13 | Blocks til maturity 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /src/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | LND Explorer 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaVersion": 8, 5 | "sourceType": "module" 6 | }, 7 | "env": { 8 | "browser": true, 9 | "node": true, 10 | "mocha": true, 11 | "es6": true 12 | }, 13 | "extends": [ 14 | "eslint:recommended", 15 | "plugin:react/recommended" 16 | ], 17 | "rules": { 18 | "no-console": 0, 19 | "quotes": [2, "single"], 20 | "semi": [2, "always"], 21 | "comma-dangle": 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/server/api/api-home.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const lnd = require('../lnd'); 3 | const app = express(); 4 | 5 | app.get('/api/home', (req, res, next) => getHomeInfo(req, res).catch(next)); 6 | 7 | module.exports = app; 8 | 9 | async function getHomeInfo(req, res) { 10 | let [info, walletBalance, channelBalance] = await Promise.all([ 11 | lnd.client.getInfo({}), 12 | lnd.client.walletBalance({}), 13 | lnd.client.channelBalance({}), 14 | ]); 15 | res.send({ info, walletBalance, channelBalance }); 16 | } 17 | -------------------------------------------------------------------------------- /src/scss/scenes/_layout.scss: -------------------------------------------------------------------------------- 1 | body, 2 | html, 3 | #app { 4 | height: 100%; 5 | } 6 | 7 | .layout { 8 | display: flex; 9 | flex-direction: column; 10 | height: 100%; 11 | } 12 | 13 | .content { 14 | flex: 1 0 auto; 15 | } 16 | 17 | .navbar-brand { 18 | img { 19 | height: 32px; 20 | margin-top: -7px; 21 | } 22 | } 23 | 24 | .navbar-dark { 25 | background-color: $body-bg - 3%; 26 | } 27 | 28 | .footer { 29 | text-align: center; 30 | margin-top: 1.5rem; 31 | margin-bottom: 0.75rem; 32 | padding-left: 0.75rem; 33 | } 34 | -------------------------------------------------------------------------------- /src/client/scenes/home/components/peers-card.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { EntypoGlobe } from 'react-entypo'; 4 | import { InfoCard, InfoCardTitle, InfoCardValue } from './info-card'; 5 | 6 | export const PeersCard = ({ info }) => ( 7 | 8 | 9 | Peers 10 | 11 | {info.num_peers} 12 | 13 | ); 14 | 15 | PeersCard.propTypes = { 16 | info: PropTypes.object.isRequired, 17 | }; 18 | -------------------------------------------------------------------------------- /src/client/components/modal-alert.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export const ModalAlert = ({ message, alertType, error }) => { 5 | if (!message && !error) return ''; 6 | if (error) alertType = 'danger'; 7 | return ( 8 |
9 |
{message || error.message}
10 |
11 | ); 12 | }; 13 | 14 | ModalAlert.propTypes = { 15 | error: PropTypes.object, 16 | message: PropTypes.string, 17 | alertType: PropTypes.string, 18 | }; 19 | -------------------------------------------------------------------------------- /src/client/scenes/network/components/network-graph-card.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Card, CardHeader } from 'reactstrap'; 4 | import { NetworkGraph } from './network-graph'; 5 | 6 | export const NetworkGraphCard = ({ onNodeSelected }) => ( 7 | 8 | 9 |
Network
10 |
11 | 12 |
13 | ); 14 | 15 | NetworkGraphCard.propTypes = { 16 | onNodeSelected: PropTypes.func, 17 | }; 18 | -------------------------------------------------------------------------------- /src/client/scenes/channels/components/open-channels-list-header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const OpenChannelsListHeader = () => ( 4 | 5 | 6 | Active 7 | Remote pub key 8 | Channel point 9 | Channel id 10 | Capacity 11 | Local balance 12 | Remote balance 13 | Unsettled balance 14 | Commit fee 15 | Commit weight 16 | Fee per kiloweight 17 | Total sent 18 | Total recv 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /src/client/scenes/home/components/active-channels-card.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { EntypoFlowCascade } from 'react-entypo'; 4 | import { InfoCard, InfoCardTitle, InfoCardValue } from './info-card'; 5 | 6 | export const ActiveChannelsCard = ({ info }) => ( 7 | 8 | 9 | Active Channels 10 | 11 | {info.num_active_channels} 12 | 13 | ); 14 | 15 | ActiveChannelsCard.propTypes = { 16 | info: PropTypes.object.isRequired, 17 | }; 18 | -------------------------------------------------------------------------------- /src/client/scenes/transactions/components/tx-list-card.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Card, CardHeader, CardBody } from 'reactstrap'; 4 | import { TxList } from './tx-list'; 5 | 6 | export const TxListCard = ({ transactions }) => ( 7 | 8 | 9 | Transactions 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | 17 | TxListCard.propTypes = { 18 | transactions: PropTypes.array.isRequired, 19 | }; 20 | -------------------------------------------------------------------------------- /src/client/scenes/home/components/pending-channels-card.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { EntypoFlowParallel } from 'react-entypo'; 4 | import { InfoCard, InfoCardTitle, InfoCardValue } from './info-card'; 5 | 6 | export const PendingChannelsCard = ({ info }) => ( 7 | 8 | 9 | Pending Channels 10 | 11 | {info.num_pending_channels} 12 | 13 | ); 14 | 15 | PendingChannelsCard.propTypes = { 16 | info: PropTypes.object.isRequired, 17 | }; 18 | -------------------------------------------------------------------------------- /src/client/scenes/home/components/channel-balance-card.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { EntypoFlash } from 'react-entypo'; 4 | import { InfoCard, InfoCardTitle, InfoCardValue } from './info-card'; 5 | 6 | export const ChannelBalanceCard = ({ channelBalance }) => ( 7 | 8 | 9 | Channel Balance 10 | 11 | {channelBalance.balance} sat 12 | 13 | ); 14 | 15 | ChannelBalanceCard.propTypes = { 16 | channelBalance: PropTypes.object.isRequired, 17 | }; 18 | -------------------------------------------------------------------------------- /src/client/scenes/home/components/info-card.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const InfoCard = ({ children }) => ( 4 |
5 |
{children}
6 |
7 | ); 8 | 9 | export const InfoCardTitle = ({ children }) => ( 10 |
11 |
12 |
{children}
13 |
14 |
15 | ); 16 | 17 | export const InfoCardValue = ({ children, size }) => ( 18 |
19 |
20 |
{children}
21 |
22 |
23 | ); 24 | -------------------------------------------------------------------------------- /src/scss/app.scss: -------------------------------------------------------------------------------- 1 | // Override Bootstrap 2 | @import '../../node_modules/bootstrap/scss/_functions.scss'; 3 | @import './_bootstrap-variables.scss'; 4 | @import '../../node_modules/bootstrap/scss/bootstrap.scss'; 5 | @import './_bootstrap-extensions.scss'; 6 | 7 | // Common App 8 | //@import './_app-variables.scss'; 9 | 10 | // Components 11 | @import './components/_btc-amount.scss'; 12 | @import './components/_def-list.scss'; 13 | @import './components/_graph.scss'; 14 | @import './components/_hex.scss'; 15 | @import './components/_loading.scss'; 16 | @import './components/_modal-alert.scss'; 17 | 18 | // Scenes 19 | @import './scenes/_home.scss'; 20 | @import './scenes/_layout.scss'; 21 | @import './scenes/_toast.scss'; 22 | -------------------------------------------------------------------------------- /src/client/scenes/payments/components/payments-list-card.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Card, CardHeader, CardBody } from 'reactstrap'; 4 | import { PaymentsList } from './payments-list'; 5 | import { SendPaymentModal } from '../../send-payment/send-payment-modal'; 6 | 7 | export const PaymentsListCard = ({ payments }) => ( 8 | 9 | 10 | Payments 11 |
12 | 13 |
14 |
15 | 16 | 17 | 18 |
19 | ); 20 | 21 | PaymentsListCard.propTypes = { 22 | payments: PropTypes.array.isRequired, 23 | }; 24 | -------------------------------------------------------------------------------- /src/server/api/api-message.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const lnd = require('../lnd'); 3 | const app = express(); 4 | 5 | app.post('/api/message/sign', (req, res, next) => signMessage(req, res).catch(next)); 6 | app.post('/api/message/verify', (req, res, next) => verifyMessage(req, res).catch(next)); 7 | 8 | module.exports = app; 9 | 10 | async function signMessage(req, res) { 11 | let { msg } = req.body; 12 | let msgBytes = Buffer.from(msg); 13 | let result = await lnd.client.signMessage({ msg: msgBytes }); 14 | res.send(result); 15 | } 16 | 17 | async function verifyMessage(req, res) { 18 | let { msg, signature } = req.body; 19 | let msgBytes = Buffer.from(msg); 20 | let result = await lnd.client.verifyMessage({ msg: msgBytes, signature }); 21 | res.send(result); 22 | } 23 | -------------------------------------------------------------------------------- /src/client/scenes/transactions/components/tx-list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Table } from 'reactstrap'; 4 | import { TxListItem } from './tx-list-item'; 5 | 6 | export const TxList = ({ transactions }) => ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {transactions.map(tx => )} 21 |
Tx hashAmountNum confirmationsBlock hashBlock heightTimestampTotal feesDest addreses
22 | ); 23 | 24 | TxList.propTypes = { 25 | transactions: PropTypes.array.isRequired, 26 | }; 27 | -------------------------------------------------------------------------------- /src/client/scenes/payments/components/payments-list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Table } from 'reactstrap'; 4 | import { PaymentsListItem } from './payments-list-item'; 5 | 6 | export const PaymentsList = ({ payments }) => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {payments.map(payment => )} 20 | 21 |
Payment hashValueCreation datePathFee
22 | ); 23 | }; 24 | 25 | PaymentsList.propTypes = { 26 | payments: PropTypes.array.isRequired, 27 | }; 28 | -------------------------------------------------------------------------------- /src/client/services/socket.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { EventEmitter } from 'events'; 3 | import io from 'socket.io-client'; 4 | 5 | export class SocketClient extends EventEmitter { 6 | socket = undefined; 7 | 8 | connect() { 9 | return new Promise((resolve, reject) => { 10 | this.socket = io('/'); 11 | this.socket.on('connect', () => { 12 | console.log('connected to server'); 13 | resolve(); 14 | }); 15 | this.socket.on('error', reject); 16 | }); 17 | } 18 | } 19 | 20 | export const socketClient = new SocketClient(); 21 | 22 | export const withSocket = WrappedComponent => { 23 | return class SocketHighOrderComponent extends React.Component { 24 | render() { 25 | return ; 26 | } 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /src/client/scenes/channels/components/channels-list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Table } from 'reactstrap'; 4 | 5 | export const ChannelsList = ({ ListHeaderComponent, ListItemComponent, channels }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | {channels.map(channel => ( 13 | 17 | ))} 18 | 19 |
20 | ); 21 | }; 22 | 23 | ChannelsList.propTypes = { 24 | ListHeaderComponent: PropTypes.any.isRequired, 25 | ListItemComponent: PropTypes.any.isRequired, 26 | channels: PropTypes.array.isRequired, 27 | }; 28 | -------------------------------------------------------------------------------- /src/client/scenes/payments/components/payments-list-item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Hex } from '../../../components/hex'; 4 | import { BtcAmount } from '../../../components/btc-amount'; 5 | import { Timestamp } from '../../../components/timestamp'; 6 | 7 | export const PaymentsListItem = ({ payment }) => ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | 27 | PaymentsListItem.propTypes = { 28 | payment: PropTypes.object.isRequired, 29 | }; 30 | -------------------------------------------------------------------------------- /src/client/scenes/peers/peers-scene.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PeersListCard } from './components/peers-list-card'; 3 | import { Loading } from '../../components/loading'; 4 | 5 | export class PeersScene extends React.Component { 6 | constructor() { 7 | super(); 8 | this.state = { 9 | peers: undefined, 10 | }; 11 | } 12 | 13 | fetchPeers = () => { 14 | fetch('/api/peers', { credentials: 'same-origin' }) 15 | .then(res => res.json()) 16 | .then(data => this.setState(data)); 17 | }; 18 | 19 | componentWillMount() { 20 | this.fetchPeers(); 21 | } 22 | 23 | render() { 24 | let { peers } = this.state; 25 | if (!peers) return ; 26 | return ( 27 | 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/client/scenes/invoices/components/invoices-card.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Card, CardHeader, CardBody } from 'reactstrap'; 4 | import { InvoicesList } from './invoices-list'; 5 | import { CreateInvoiceModal } from '../../create-invoice/create-invoice-modal'; 6 | 7 | export const InvoicesCard = ({ invoices, onInvoiceCreated }) => { 8 | return ( 9 | 10 | 11 | Invoices 12 |
13 | 14 |
15 |
16 | 17 | 18 | 19 |
20 | ); 21 | }; 22 | 23 | InvoicesCard.propTypes = { 24 | invoices: PropTypes.array.isRequired, 25 | onInvoiceCreated: PropTypes.func, 26 | }; 27 | -------------------------------------------------------------------------------- /src/client/scenes/invoices/components/invoices-list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Table } from 'reactstrap'; 4 | import { InvoicesListItem } from './invoices-list-item'; 5 | 6 | export const InvoicesList = ({ invoices }) => ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {invoices.map(invoice => ( 23 | 24 | ))} 25 | 26 |
MemoReceiptR preimageR hashValueSettledCreation dateSettle datePayment request
27 | ); 28 | 29 | InvoicesList.propTypes = { 30 | invoices: PropTypes.array.isRequired, 31 | }; 32 | -------------------------------------------------------------------------------- /src/client/scenes/toast/components/toast.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Alert } from 'reactstrap'; 4 | 5 | export class Toast extends React.Component { 6 | static propTypes = { 7 | toast: PropTypes.object.isRequired, 8 | toastClosed: PropTypes.func, 9 | }; 10 | 11 | state = { 12 | show: true, 13 | }; 14 | 15 | close = () => { 16 | this.setState({ show: false }); 17 | this.props.toastClosed(this.props.toast); 18 | }; 19 | 20 | componentDidMount() { 21 | if (this.props.toast.autoclose) setTimeout(this.close, 10000); 22 | } 23 | 24 | render() { 25 | let { toast } = this.props; 26 | let { show } = this.state; 27 | return ( 28 |
29 | 30 | {toast.message} 31 | 32 |
33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/client/scenes/peers/components/peers-list-card.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Card, CardHeader, CardBody } from 'reactstrap'; 4 | import { PeersList } from './peers-list'; 5 | import { ConnectPeerModal } from '../../connect-peer/connect-peer-modal'; 6 | 7 | export const PeersListCard = ({ peers, onPeerConnected, onPeerDisconnected }) => ( 8 | 9 | 10 | Peers 11 |
12 | 13 |
14 |
15 | 16 | 17 | 18 |
19 | ); 20 | 21 | PeersListCard.propTypes = { 22 | peers: PropTypes.array.isRequired, 23 | onPeerConnected: PropTypes.func, 24 | onPeerDisconnected: PropTypes.func, 25 | }; 26 | -------------------------------------------------------------------------------- /src/server/api/api-peers.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const lnd = require('../lnd'); 4 | 5 | app.get('/api/peers', (req, res, next) => getPeers(req, res).catch(next)); 6 | app.delete('/api/peers', (req, res, next) => disconnectPeer(req, res).catch(next)); 7 | app.post('/api/peers', (req, res, next) => connectPeer(req, res).catch(next)); 8 | 9 | module.exports = app; 10 | 11 | async function getPeers(req, res) { 12 | let peers = await lnd.client.listPeers({}); 13 | res.send({ peers: peers.peers }); 14 | } 15 | 16 | async function disconnectPeer(req, res) { 17 | let pub_key = req.body.pub_key; 18 | await lnd.client.disconnectPeer({ pub_key }); 19 | res.send({}); 20 | } 21 | 22 | async function connectPeer(req, res) { 23 | let { pubkey, host, perm } = req.body; 24 | let addr = { 25 | pubkey, 26 | host, 27 | }; 28 | await lnd.client.connectPeer({ addr, perm }); 29 | res.send({}); 30 | } 31 | -------------------------------------------------------------------------------- /src/client/scenes/channels/components/pending-closing-channels-list-item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { BtcAmount } from '../../../components/btc-amount'; 4 | import { Hex } from '../../../components/hex'; 5 | 6 | export const PendingClosingChannelsListItem = ({ channel }) => ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | 29 | PendingClosingChannelsListItem.propTypes = { 30 | channel: PropTypes.object.isRequired, 31 | }; 32 | -------------------------------------------------------------------------------- /src/server/api/api-invoices.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const lnd = require('../lnd'); 3 | const wss = require('../wss'); 4 | const app = express(); 5 | 6 | app.get('/api/invoices', (req, res, next) => getInvoices(req, res).catch(next)); 7 | app.post('/api/invoices', (req, res, next) => createInvoice(req, res).catch(next)); 8 | 9 | module.exports = app; 10 | 11 | async function getInvoices(req, res) { 12 | let pending_only = req.query.pending_only == 'true'; 13 | let invoices = await lnd.client.listInvoices({ pending_only }); 14 | invoices.invoices.forEach(p => { 15 | p.description_hash = p.description_hash.toString('hex'); 16 | p.r_hash = p.r_hash.toString('hex'); 17 | p.r_preimage = p.r_preimage.toString('hex'); 18 | p.receipt = p.receipt.toString('hex'); 19 | }); 20 | res.send({ invoices }); 21 | } 22 | 23 | async function createInvoice(req, res) { 24 | let { memo, value } = req.body; 25 | let result = await lnd.client.addInvoice({ memo, value }); 26 | res.send(result); 27 | } 28 | -------------------------------------------------------------------------------- /src/client/scenes/peers/components/peers-list-item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Hex } from '../../../components/hex'; 4 | import { BtcAmount } from '../../../components/btc-amount'; 5 | import { DisconnectPeerModal } from '../../disconnect-peer/disconnect-peer-modal'; 6 | 7 | export const PeerListItem = ({ peer, onPeerDisconnected }) => ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {peer.address} 16 | {peer.bytes_sent} 17 | {peer.bytes_recv} 18 | 19 | 20 | 21 | 22 | 23 | 24 | {peer.ping_time / 1e3}ms 25 | 26 | ); 27 | 28 | PeerListItem.propTypes = { 29 | peer: PropTypes.object.isRequired, 30 | onPeerDisconnected: PropTypes.func, 31 | }; 32 | -------------------------------------------------------------------------------- /src/server/api/api-payments.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const winston = require('winston'); 3 | const lnd = require('../lnd'); 4 | const wss = require('../wss'); 5 | const app = express(); 6 | 7 | app.get('/api/payments', (req, res, next) => getPayments(req, res).catch(next)); 8 | app.post('/api/payment', (req, res, next) => sendPayment(req, res).catch(next)); 9 | app.post('/api/payment/decode', (req, res, next) => decodePayment(req, res).catch(next)); 10 | 11 | module.exports = app; 12 | 13 | async function getPayments(req, res) { 14 | let payments = await lnd.client.listPayments({}); 15 | res.send({ payments }); 16 | } 17 | 18 | async function sendPayment(req, res) { 19 | let { payment_request } = req.body; 20 | 21 | let call = lnd.client.sendPayment({}); 22 | call.write({ 23 | payment_request, 24 | }); 25 | wss.subscribeSendPayment(call); 26 | res.send({}); 27 | } 28 | 29 | async function decodePayment(req, res) { 30 | let { payment_request } = req.body; 31 | let result = await lnd.client.decodePayReq({ pay_req: payment_request }); 32 | res.send(result); 33 | } 34 | -------------------------------------------------------------------------------- /src/server/lnd.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston'); 2 | const lndAsync = require('lnd-async'); 3 | let _instance; 4 | 5 | module.exports = { 6 | connect, 7 | get client() { 8 | return _instance; 9 | }, 10 | }; 11 | 12 | async function connect() { 13 | try { 14 | let lndHost = process.env.LND_HOST; 15 | let lndPort = process.env.LND_PORT; 16 | let certPath = process.env.LND_CERT_PATH; 17 | let macaroonPath = process.env.LND_MACAROON_PATH; 18 | let noMacaroons = process.env.LND_NO_MACAROONS; 19 | 20 | lndPort = parseInt(lndPort) || undefined; // integer or undefined 21 | noMacaroons = noMacaroons === 'true' || undefined; // true or undefined 22 | 23 | // connect with supplied options or let connection 24 | // defaults take precedence by passing undefined 25 | _instance = await lndAsync.connect({ 26 | lndHost, 27 | lndPort, 28 | certPath, 29 | macaroonPath, 30 | noMacaroons, 31 | }); 32 | 33 | winston.info('connected to lnd'); 34 | } catch (ex) { 35 | winston.error(ex); 36 | process.exit(1); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/client/scenes/peers/components/peers-list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Table } from 'reactstrap'; 4 | import { PeerListItem } from './peers-list-item'; 5 | import { peerSort } from '../../../services/peer-helpers'; 6 | 7 | export const PeersList = ({ peers, onPeerDisconnected }) => ( 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {peers 23 | .sort(peerSort) 24 | .map(peer => ( 25 | 30 | ))} 31 | 32 |
12 | Pub keyAddressBytes sentBytes RecvSat sentSat recvPing time
33 | ); 34 | 35 | PeersList.propTypes = { 36 | peers: PropTypes.array.isRequired, 37 | onPeerDisconnected: PropTypes.func, 38 | }; 39 | -------------------------------------------------------------------------------- /src/client/scenes/transactions/components/tx-list-item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Hex } from '../../../components/hex'; 4 | import { BtcAmount } from '../../../components/btc-amount'; 5 | import { Timestamp } from '../../../components/timestamp'; 6 | 7 | export const TxListItem = ({ tx }) => ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {tx.num_confirmations} 16 | 17 | 18 | 19 | {tx.block_height} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {tx.dest_addresses.map((address, idx) => ( 28 |
29 | 30 |
31 | ))} 32 | 33 | 34 | ); 35 | 36 | TxListItem.propTypes = { 37 | tx: PropTypes.object.isRequired, 38 | }; 39 | -------------------------------------------------------------------------------- /src/server/wss.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const socketio = require('socket.io'); 3 | let io; 4 | 5 | module.exports = { 6 | connect, 7 | broadcastTransaction, 8 | broadcastInvoice, 9 | subscribeOpenChannel, 10 | subscribeCloseChannel, 11 | subscribeSendPayment, 12 | }; 13 | 14 | function connect(app) { 15 | let server = http.Server(app); 16 | io = socketio(server); 17 | return server; 18 | } 19 | 20 | function broadcastTransaction(tx) { 21 | io.emit('transaction', tx); 22 | } 23 | 24 | function broadcastInvoice(inv) { 25 | io.emit('invoice', inv); 26 | } 27 | 28 | function subscribeOpenChannel(call) { 29 | call.on('data', msg => io.emit('openchannel', msg)); 30 | call.on('error', err => io.emit('openchannelerror', err)); 31 | } 32 | 33 | function subscribeCloseChannel(call) { 34 | call.on('data', msg => io.emit('closechannel', msg)); 35 | call.on('error', err => io.emit('closechannelerror', err)); 36 | } 37 | 38 | function subscribeSendPayment(call) { 39 | call.on('data', msg => io.emit('sendpayment', msg)); 40 | call.on('error', err => io.emit('sendpaymenterror', err)); 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Brian Mancini 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/client/scenes/channels/components/pending-force-channels-list-item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { BtcAmount } from '../../../components/btc-amount'; 4 | import { Hex } from '../../../components/hex'; 5 | 6 | export const PendingForceChannelsListItem = ({ channel }) => ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {channel.maturity_height} 30 | {channel.blocks_til_maturity} 31 | 32 | ); 33 | 34 | PendingForceChannelsListItem.propTypes = { 35 | channel: PropTypes.object.isRequired, 36 | }; 37 | -------------------------------------------------------------------------------- /src/client/scenes/channels/components/pending-open-channels-list-item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { BtcAmount } from '../../../components/btc-amount'; 4 | import { Hex } from '../../../components/hex'; 5 | 6 | export const PendingOpenChannelsListItem = ({ channel }) => ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {channel.confirmation_height} 33 | 34 | ); 35 | 36 | PendingOpenChannelsListItem.propTypes = { 37 | channel: PropTypes.object.isRequired, 38 | }; 39 | -------------------------------------------------------------------------------- /src/client/scenes/send-payment/components/decoded-payment-request.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { BtcAmount } from '../../../components/btc-amount'; 4 | import { Hex } from '../../../components/hex'; 5 | 6 | export const DecodedPaymentRequest = ({ payreq }) => { 7 | if (!payreq) return ''; 8 | return ( 9 |
10 |
11 |
Destination:
12 |
13 | 14 |
15 |
16 |
17 |
Payment hash:
18 |
19 | 20 |
21 |
22 |
23 |
Amount:
24 |
25 | 26 |
27 |
28 |
29 | ); 30 | }; 31 | 32 | DecodedPaymentRequest.propTypes = { 33 | payreq: PropTypes.object, 34 | }; 35 | -------------------------------------------------------------------------------- /src/client/scenes/invoices/components/invoices-list-item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Hex } from '../../../components/hex'; 4 | import { BtcAmount } from '../../../components/btc-amount'; 5 | import { BoolValue } from '../../..//components/bool-value'; 6 | import { Timestamp } from '../../../components/timestamp'; 7 | 8 | export const InvoicesListItem = ({ invoice }) => ( 9 | 10 | {invoice.memo} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | 38 | InvoicesListItem.propTypes = { 39 | invoice: PropTypes.object.isRequired, 40 | }; 41 | -------------------------------------------------------------------------------- /src/client/scenes/send-payment/components/send-payment-form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Form, FormGroup, Label, Input } from 'reactstrap'; 4 | 5 | export class SendPaymentForm extends React.Component { 6 | static propTypes = { 7 | formChanged: PropTypes.func, 8 | }; 9 | 10 | state = { 11 | valid: false, 12 | payment_request: '', 13 | }; 14 | 15 | componentDidUpdate(prevProps, prevState) { 16 | if (prevState !== this.state) this.props.formChanged(this.state); 17 | } 18 | 19 | fieldChanged = (prop, value) => { 20 | let valid = this.validate({ ...this.state, [prop]: value }); 21 | this.setState({ [prop]: value, valid }); 22 | }; 23 | 24 | validate = ({ payment_request }) => { 25 | return !!payment_request; 26 | }; 27 | 28 | render() { 29 | return ( 30 |
31 | 32 | 33 | this.fieldChanged('payment_request', e.target.value)} 37 | /> 38 | 39 |
40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/client/scenes/transactions/transactions-scene.jsx: -------------------------------------------------------------------------------- 1 | import qs from 'qs'; 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { TxListCard } from './components/tx-list-card'; 5 | import { Loading } from '../../components/loading'; 6 | 7 | export class TransactionsScene extends React.Component { 8 | static propTypes = { 9 | newTxs: PropTypes.array, 10 | }; 11 | 12 | state = { 13 | txs: undefined, 14 | page: 1, 15 | pagesize: 100, 16 | sortBy: 'num_confirmations', 17 | sortDir: 'asc', 18 | }; 19 | 20 | componentWillMount() { 21 | this.loadData(); 22 | } 23 | 24 | componentWillReceiveProps(nextProps) { 25 | if (nextProps.newTxs !== this.props.newTxs) this.loadData(); 26 | } 27 | 28 | loadData() { 29 | let { page, pagesize, sortBy, sortDir } = this.state; 30 | fetch('/api/transactions?' + qs.stringify({ page, pagesize, sortBy, sortDir }), { 31 | credentials: 'same-origin' 32 | }) 33 | .then(res => res.json()) 34 | .then(json => json.txs) 35 | .then(txs => this.setState({ txs })); 36 | } 37 | 38 | render() { 39 | let { txs } = this.state; 40 | if (!txs) return ; 41 | return ; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (http://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # Typescript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | # prevent tls cert from making its way in here 64 | tls.cert 65 | 66 | # dist 67 | dist 68 | -------------------------------------------------------------------------------- /src/client/scenes/connect-peer/components/connect-peer-form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Form, FormGroup, Label, Input } from 'reactstrap'; 4 | 5 | export class ConnectPeerForm extends React.Component { 6 | static propTypes = { 7 | onChange: PropTypes.func.isRequired, 8 | pubkey: PropTypes.string, 9 | host: PropTypes.string, 10 | }; 11 | 12 | formChanged = (key, value) => { 13 | this.props.onChange(key, value); 14 | }; 15 | 16 | render() { 17 | let { pubkey, host } = this.props; 18 | return ( 19 |
20 | 21 | 22 | this.formChanged('pubkey', e.target.value)} 27 | /> 28 | 29 | 30 | 31 | this.formChanged('host', e.target.value)} 36 | /> 37 | 38 |
39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/client/scenes/payments/payments-scene.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { withSocket } from '../../services/socket'; 4 | import { Loading } from '../../components/loading'; 5 | import { PaymentsListCard } from './components/payments-list-card'; 6 | 7 | export class PaymentsSceneComponent extends React.Component { 8 | static propTypes = { 9 | socket: PropTypes.object.isRequired, 10 | }; 11 | 12 | constructor() { 13 | super(); 14 | this.state = { 15 | payments: undefined, 16 | }; 17 | } 18 | 19 | componentDidMount() { 20 | this.props.socket.on('sendpayment', this.fetchPayments); 21 | } 22 | 23 | componentWillUnmount() { 24 | this.props.socket.off('sendpayment', this.fetchPayments); 25 | } 26 | 27 | fetchPayments = () => { 28 | fetch('/api/payments', { credentials: 'same-origin' }) 29 | .then(res => res.json()) 30 | .then(data => this.setState(data)); 31 | }; 32 | 33 | componentWillMount() { 34 | this.fetchPayments(); 35 | } 36 | 37 | render() { 38 | let { payments } = this.state; 39 | if (!payments) return ; 40 | return ; 41 | } 42 | } 43 | 44 | export const PaymentsScene = withSocket(PaymentsSceneComponent); 45 | -------------------------------------------------------------------------------- /src/client/scenes/invoices/invoices-scene.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Loading } from '../../components/loading'; 4 | import { InvoicesCard } from './components/invoices-card'; 5 | import { withSocket } from '../../services/socket'; 6 | 7 | export class InvoicesSceneComponent extends React.Component { 8 | static propTypes = { 9 | socket: PropTypes.object.isRequired, 10 | }; 11 | 12 | constructor() { 13 | super(); 14 | this.state = { 15 | invoices: undefined, 16 | }; 17 | } 18 | 19 | componentDidMount() { 20 | let { socket } = this.props; 21 | socket.on('invoice', this.fetchInvoices); 22 | } 23 | 24 | componentWillUnmount() { 25 | let { socket } = this.props; 26 | socket.off('invoice', this.fetchInvoices); 27 | } 28 | 29 | fetchInvoices = () => { 30 | fetch('/api/invoices', { credentials: 'same-origin' }) 31 | .then(res => res.json()) 32 | .then(data => this.setState(data)); 33 | }; 34 | 35 | componentWillMount() { 36 | this.fetchInvoices(); 37 | } 38 | 39 | render() { 40 | let { invoices } = this.state; 41 | if (!invoices) return ; 42 | return ; 43 | } 44 | } 45 | 46 | export const InvoicesScene = withSocket(InvoicesSceneComponent); 47 | -------------------------------------------------------------------------------- /src/client/components/def-list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { ListGroup, ListGroupItem } from 'reactstrap'; 4 | 5 | export const DefList = ({ children, labelWidth = 2 }) => ( 6 | 7 | {React.Children.map(children, child => React.cloneElement(child, { labelWidth }))} 8 | 9 | ); 10 | 11 | DefList.propTypes = { 12 | children: PropTypes.any, 13 | labelWidth: PropTypes.number, 14 | }; 15 | 16 | export const DefListItem = ({ children, labelWidth }) => ( 17 | 18 |
19 | {React.Children.map(children, child => React.cloneElement(child, { labelWidth }))} 20 |
21 |
22 | ); 23 | 24 | DefListItem.propTypes = { 25 | children: PropTypes.any, 26 | labelWidth: PropTypes.number, 27 | }; 28 | 29 | export const DefListLabel = ({ children, labelWidth }) => ( 30 |
{children}
31 | ); 32 | 33 | DefListLabel.propTypes = { 34 | children: PropTypes.any, 35 | labelWidth: PropTypes.number, 36 | }; 37 | 38 | export const DefListValue = ({ children, labelWidth }) => ( 39 |
{children}
40 | ); 41 | 42 | DefListValue.propTypes = { 43 | children: PropTypes.any, 44 | labelWidth: PropTypes.number, 45 | }; 46 | -------------------------------------------------------------------------------- /src/server/api/api-transactions.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const lnd = require('../lnd'); 3 | const app = express(); 4 | 5 | app.get('/api/transactions', (req, res, next) => getTransactions(req, res).catch(next)); 6 | 7 | module.exports = app; 8 | 9 | async function getTransactions(req, res) { 10 | let { page = 1, pagesize = 25, sortBy = 'num_confirmations', sortDir = 'asc' } = req.query; 11 | let transactions = await lnd.client.getTransactions({}); 12 | let txs = limit(sort(transactions.transactions, sortBy, sortDir), page, pagesize); 13 | res.send({ txs }); 14 | } 15 | 16 | function sort(items, sortBy, sortDir) { 17 | items = items.slice(); 18 | items.sort((a, b) => { 19 | if (sortDir === 'asc' && a[sortBy] > b[sortBy]) return 1; 20 | if (sortDir === 'asc' && a[sortBy] < b[sortBy]) return -1; 21 | if (sortDir === 'asc' && a[sortBy] === b[sortBy]) return 0; 22 | if (sortDir === 'desc' && a[sortBy] > b[sortBy]) return -1; 23 | if (sortDir === 'desc' && a[sortBy] < b[sortBy]) return 1; 24 | if (sortDir === 'desc' && a[sortBy] === b[sortBy]) return 0; 25 | }); 26 | return items; 27 | } 28 | 29 | function limit(items, page, pagesize) { 30 | page = Math.max(1, (typeof page === 'number' ? page : parseInt(page)) || 1); 31 | pagesize = Math.max(1, (typeof pagesize === 'number' ? pagesize : parseInt(pagesize)) || 1); 32 | return items.slice((page - 1) * pagesize, pagesize); 33 | } 34 | -------------------------------------------------------------------------------- /src/client/scenes/home/components/wallet-balance-card.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Card, CardHeader, CardBody } from 'reactstrap'; 4 | import { DefList, DefListItem, DefListLabel, DefListValue } from '../../../components/def-list'; 5 | import { BtcAmount } from '../../../components/btc-amount'; 6 | 7 | export const WalletBalanceCard = ({ walletBalance, title }) => ( 8 | 9 | 10 | {title} 11 | 12 | 13 | 14 | 15 | Total balance 16 | 17 | 18 | 19 | 20 | 21 | Confirmed balance 22 | 23 | 24 | 25 | 26 | 27 | Unconfirmed balance 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | 37 | WalletBalanceCard.propTypes = { 38 | walletBalance: PropTypes.object.isRequired, 39 | title: PropTypes.string.isRequired, 40 | }; 41 | -------------------------------------------------------------------------------- /src/client/scenes/create-invoice/components/create-invoice-form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Form, FormGroup, Input, Label } from 'reactstrap'; 4 | 5 | export class CreateInvoiceForm extends React.Component { 6 | static propTypes = { 7 | formChanged: PropTypes.func, 8 | }; 9 | 10 | state = { 11 | valid: false, 12 | value: 0, 13 | memo: '', 14 | }; 15 | 16 | componentDidUpdate(prevProps, prevState) { 17 | if (prevState !== this.state) this.props.formChanged(this.state); 18 | } 19 | 20 | formChanged = (prop, value) => { 21 | let valid = this.validate({ ...this.state, [prop]: value }); 22 | this.setState({ [prop]: value, valid }); 23 | }; 24 | 25 | validate = () => { 26 | return this.state.value > 0; 27 | }; 28 | 29 | render() { 30 | let { value, memo } = this.state; 31 | return ( 32 |
33 | 34 | 35 | this.formChanged('value', parseInt(e.target.value) || '')} 40 | /> 41 | 42 | 43 | 44 | this.formChanged('memo', e.target.value)} 49 | /> 50 | 51 |
52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/client/scenes/home/components/blockchain-card.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Card, CardHeader, CardBody } from 'reactstrap'; 4 | import { DefList, DefListItem, DefListLabel, DefListValue } from '../../../components/def-list'; 5 | import { Hex } from '../../../components/hex'; 6 | import { BoolValue } from '../../../components/bool-value'; 7 | 8 | export const BlockchainCard = ({ info }) => ( 9 | 10 | 11 |
Blockchain
12 |
13 | 14 | 15 | 16 | Chain: 17 | 18 | {info.chains.join(', ')} {info.testnet ? ' testnet' : ''} 19 | 20 | 21 | 22 | Block height 23 | {info.block_height} 24 | 25 | 26 | Block hash 27 | 28 | 29 | 30 | 31 | 32 | Synced to chain 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | ); 41 | 42 | BlockchainCard.propTypes = { 43 | info: PropTypes.object.isRequired, 44 | }; 45 | -------------------------------------------------------------------------------- /src/client/scenes/channels/components/open-channels-list-item.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { BoolValue } from '../../../components/bool-value'; 4 | import { BtcAmount } from '../../../components/btc-amount'; 5 | import { Hex } from '../../../components/hex'; 6 | import { CloseChannelModal } from '../../close-channel/close-channel-modal'; 7 | 8 | export const OpenChannelsListItem = ({ channel }) => ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {channel.chan_id} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ); 53 | 54 | OpenChannelsListItem.propTypes = { 55 | channel: PropTypes.object, 56 | }; 57 | -------------------------------------------------------------------------------- /src/client/scenes/channels/channels-scene.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { withSocket } from '../../services/socket'; 5 | import { Loading } from '../../components/loading'; 6 | import { ChannelsCard } from './components/channels-card'; 7 | 8 | export class ChannelsSceneComponent extends React.Component { 9 | static propTypes = { 10 | socket: PropTypes.object.isRequired, 11 | }; 12 | 13 | constructor() { 14 | super(); 15 | this.state = { 16 | channelBalance: undefined, 17 | openChannels: undefined, 18 | pendingChannels: undefined, 19 | closeChannelScope: undefined, 20 | }; 21 | } 22 | 23 | componentDidMount() { 24 | let { socket } = this.props; 25 | socket.on('openchannel', this.delayFetchChannels); 26 | socket.on('closechannel', this.delayFetchChannels); 27 | } 28 | 29 | componentWillUnmount() { 30 | let { socket } = this.props; 31 | socket.off('openchannel', this.delayFetchChannels); 32 | socket.off('closechannel', this.delayFetchChannels); 33 | } 34 | 35 | delayFetchChannels = () => { 36 | setTimeout(this.fetchChannels, 100); 37 | }; 38 | 39 | fetchChannels = () => { 40 | fetch('/api/channels', { credentials: 'same-origin' }) 41 | .then(res => res.json()) 42 | .then(data => this.setState(data)); 43 | }; 44 | 45 | componentWillMount() { 46 | this.fetchChannels(); 47 | } 48 | 49 | render() { 50 | let { openChannels, pendingChannels } = this.state; 51 | if (!openChannels) return ; 52 | return ; 53 | } 54 | } 55 | 56 | export const ChannelsScene = withSocket(ChannelsSceneComponent); 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LND Explorer 2 | 3 | LND Explorer is a web interface for interacting with the Lightning Network via LND. 4 | 5 | ## Installation 6 | 7 | LND Explorer requires LND 0.5-beta. 8 | 9 | 10 | Run LND Explorer against a local installation on LND: 11 | 12 | ``` 13 | git clone https://github.com/altangent/lnd-explorer 14 | cd lnd-explorer 15 | npm install && npm run build 16 | npm start 17 | ``` 18 | 19 | ## Runtime options 20 | 21 | LND Explorer will connect to LND via localhost on the default port. It will use the default OS specific paths for the tls.cert and admin.macaroon. If you have modified the port, file paths, or are not using macaroons, you can use environment variables to drive the connection to LND. 22 | 23 | Supported environment variables: 24 | 25 | ``` 26 | SERVER_HOST - the host express will listen on (default: localhost) 27 | SERVER_PORT - the port express will listen on (default: 8000) 28 | LND_HOST - the LND host (default: localhost) 29 | LND_PORT - the LND port (default: 10009) 30 | LND_CERT_PATH - the path to tls.cert (default: OS specific default path for LND) 31 | LND_MACAROON_PATH - the path to the admin.macaroon file (default: OS specific default path for LND) 32 | LND_NO_MACAROONS - set to true to disable macaroons 33 | ``` 34 | 35 | ## Run with Docker 36 | 37 | Build the Dockerfile: 38 | 39 | ``` 40 | docker build . -t lnd-explorer 41 | ``` 42 | 43 | Run it, using the variables listed above to configure the application. SERVER_HOST 44 | needs to be present to have the LND Explorer listen on all interfaces inside the container. 45 | 46 | ``` 47 | docker run -e LND_HOST=lightning -e SERVER_HOST=0.0.0.0 -v /full/path/to/.lnd:/root/.lnd/ -p 8000:8000 lnd-explorer 48 | ``` 49 | 50 | Then just navigate to http://localhost:8000 51 | -------------------------------------------------------------------------------- /src/client/scenes/network/network-scene.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Loading } from '../../components/loading'; 3 | import { NetworkInfoCard } from './components/network-info-card'; 4 | import { NetworkGraphCard } from './components/network-graph-card'; 5 | import { NodeInfoCard } from './components/node-info-card'; 6 | 7 | export class NetworkScene extends React.Component { 8 | constructor() { 9 | super(); 10 | this.state = { 11 | networkInfo: undefined, 12 | node: undefined, 13 | }; 14 | } 15 | 16 | fetchData() { 17 | fetch('/api/network', { credentials: 'same-origin' }) 18 | .then(res => res.json()) 19 | .then(networkInfo => this.setState({ networkInfo: networkInfo.networkInfo })); 20 | } 21 | 22 | fetchNode(pubKey) { 23 | return fetch('/api/network/' + pubKey, { credentials: 'same-origin' }) 24 | .then(res => res.json()); 25 | } 26 | 27 | onNodeSelected = pubKey => { 28 | this.fetchNode(pubKey).then(node => this.setState({ node })); 29 | }; 30 | 31 | componentWillMount() { 32 | this.fetchData(); 33 | } 34 | 35 | render() { 36 | let { networkInfo, node } = this.state; 37 | if (!networkInfo) return ; 38 | return ( 39 |
40 |
41 |
42 | 43 |
44 |
45 |
46 |
47 | 48 |
49 |
50 | 51 |
52 |
53 |
54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/server/api/api-channels.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const lnd = require('../lnd'); 3 | const wss = require('../wss'); 4 | const app = express(); 5 | 6 | app.get('/api/channels', (req, res, next) => getChannels(req, res).catch(next)); 7 | app.post('/api/channels', (req, res, next) => openChannel(req, res, next).catch(next)); 8 | app.delete('/api/channels', (req, res, next) => closeChannel(req, res, next).catch(next)); 9 | 10 | module.exports = app; 11 | 12 | async function getChannels(req, res) { 13 | let [openChannels, pendingChannels] = await Promise.all([ 14 | lnd.client.listChannels({}), 15 | lnd.client.pendingChannels({}), 16 | ]); 17 | 18 | let channelBalance = openChannels.channels.length 19 | ? openChannels.channels.map(p => parseInt(p.local_balance)).reduce((a, b) => a + b) 20 | : 0; 21 | 22 | res.send({ openChannels: openChannels.channels, pendingChannels, channelBalance }); 23 | } 24 | 25 | async function openChannel(req, res) { 26 | let { node_pubkey_string, local_funding_amount, push_sat } = req.body; 27 | 28 | local_funding_amount = parseInt(local_funding_amount); 29 | push_sat = push_sat ? parseInt(push_sat) : undefined; 30 | 31 | let conn = await lnd.client.openChannel({ 32 | node_pubkey: Buffer.from(node_pubkey_string, 'hex'), 33 | local_funding_amount, 34 | push_sat, 35 | }); 36 | wss.subscribeOpenChannel(conn); 37 | res.send({ ok: true }); 38 | } 39 | 40 | async function closeChannel(req, res) { 41 | let { channel_point, force = false } = req.body; 42 | 43 | let [funding_txid_str, output_index] = channel_point.split(':'); 44 | 45 | channel_point = { 46 | funding_txid_str, 47 | output_index: parseInt(output_index) 48 | }; 49 | 50 | let conn = await lnd.client.closeChannel({ channel_point, force }); 51 | wss.subscribeCloseChannel(conn); 52 | res.send({}); 53 | } 54 | -------------------------------------------------------------------------------- /src/client/scenes/new-address/new-address-modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'; 4 | import { Hex } from '../../components/hex'; 5 | 6 | export class NewAddressModal extends React.Component { 7 | static propTypes = { 8 | type: PropTypes.string.isRequired, 9 | open: PropTypes.bool.isRequired, 10 | toggle: PropTypes.func.isRequired, 11 | }; 12 | 13 | state = { 14 | address: null, 15 | }; 16 | 17 | componentWillReceiveProps(nextProps) { 18 | if (nextProps.open && !this.props.open) { 19 | this.newAddress({ type: nextProps.type }); 20 | } 21 | } 22 | 23 | newAddress = ({ type }) => { 24 | fetch('/api/address', { 25 | method: 'post', 26 | headers: { 27 | 'Content-Type': 'application/json', 28 | }, 29 | body: JSON.stringify({ type }), 30 | credentials: 'same-origin', 31 | }) 32 | .then(res => res.json()) 33 | .then(json => this.setState({ address: json.address.address })); 34 | }; 35 | 36 | render() { 37 | return ( 38 |
39 | 40 | New {this.renderTypeString()} address 41 | 42 |

43 | 44 |

45 |
46 | 47 | 50 | 51 |
52 |
53 | ); 54 | } 55 | 56 | renderTypeString() { 57 | if (this.props.type === '0') return 'p2wkh'; 58 | if (this.props.type === '1') return 'np2wkh'; 59 | if (this.props.type === '2') return 'p2pkh'; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/client/scenes/open-channel/components/open-channel-form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Form, FormGroup, Input, Label } from 'reactstrap'; 4 | 5 | export class OpenChannelForm extends React.Component { 6 | static propTypes = { 7 | peers: PropTypes.array, 8 | onChange: PropTypes.func, 9 | selectedPeer: PropTypes.string, 10 | localAmount: PropTypes.any, 11 | pushAmount: PropTypes.any, 12 | }; 13 | 14 | formChanged = (prop, value) => { 15 | this.props.onChange(prop, value); 16 | }; 17 | 18 | render() { 19 | let { peers = [], selectedPeer, localAmount, pushAmount } = this.props; 20 | return ( 21 |
22 | 23 | 24 | this.formChanged('selectedPeer', e.target.value)} 27 | value={selectedPeer} 28 | > 29 | 30 | {peers.map(p => ( 31 | 34 | ))} 35 | 36 | 37 | 38 | 39 | this.formChanged('localAmount', parseInt(e.target.value) || '')} 42 | value={localAmount} 43 | /> 44 | 45 | 46 | 47 | this.formChanged('pushAmount', parseInt(e.target.value) || '')} 50 | value={pushAmount} 51 | /> 52 | 53 |
54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/server/server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const winston = require('winston'); 3 | const express = require('express'); 4 | const compression = require('compression'); 5 | const bodyParser = require('body-parser'); 6 | const serveStatic = require('serve-static'); 7 | const favicon = require('serve-favicon'); 8 | const lnd = require('./lnd'); 9 | const app = express(); 10 | 11 | const wss = require('./wss'); 12 | const server = wss.connect(app); 13 | 14 | lnd.connect().then(() => { 15 | winston.info('subscribing to transactions'); 16 | let txSub = lnd.client.subscribeTransactions({}); 17 | txSub.on('data', wss.broadcastTransaction); 18 | 19 | winston.info('subscribing to invoices'); 20 | let invSub = lnd.client.subscribeInvoices({}); 21 | invSub.on('data', wss.broadcastInvoice); 22 | }); 23 | 24 | app.use(compression()); 25 | app.use(favicon(path.join(__dirname, '../public/lnd-explorer-icon.png'))); 26 | app.use('/public', serveStatic(path.join(__dirname, '../public'))); 27 | app.use('/public/app', serveStatic(path.join(__dirname, '../../dist/app'))); 28 | app.use('/public/css', serveStatic(path.join(__dirname, '../../dist/css'))); 29 | app.use(bodyParser.json()); 30 | 31 | app.use(require('./api/api-home')); 32 | app.use(require('./api/api-transactions')); 33 | app.use(require('./api/api-peers')); 34 | app.use(require('./api/api-channels')); 35 | app.use(require('./api/api-invoices')); 36 | app.use(require('./api/api-payments')); 37 | app.use(require('./api/api-network')); 38 | app.use(require('./api/api-address')); 39 | app.use(require('./api/api-message')); 40 | 41 | app.get('*', (req, res) => res.sendFile(path.join(__dirname, '../public/index.html'))); 42 | 43 | app.use((err, req, res, next) => { 44 | winston.error(err); 45 | res.status(500).json(err); 46 | }); 47 | 48 | let port = parseInt(process.env.SERVER_PORT) || 8000; 49 | let host = process.env.SERVER_HOST || 'localhost'; 50 | 51 | server.listen(port, host, () => winston.info(`express listening on ${host}:${port}`)); 52 | -------------------------------------------------------------------------------- /src/client/scenes/network/components/network-info-card.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Card, CardHeader } from 'reactstrap'; 4 | import { DefList, DefListItem, DefListLabel, DefListValue } from '../../../components/def-list'; 5 | import { BtcAmount } from '../../../components/btc-amount'; 6 | 7 | export const NetworkInfoCard = ({ networkInfo }) => ( 8 | 9 | 10 |
Network Info
11 |
12 | 13 | 14 | Graph diameter 15 | {networkInfo.graph_diameter} 16 | 17 | 18 | Ave out degree 19 | {networkInfo.avg_out_degree} 20 | 21 | 22 | Max out degree 23 | {networkInfo.max_out_degree} 24 | 25 | 26 | Num nodes 27 | {networkInfo.num_nodes} 28 | 29 | 30 | Num channels 31 | {networkInfo.num_channels} 32 | 33 | 34 | Total network capacity 35 | 36 | 37 | 38 | 39 | 40 | Avg channel size 41 | 42 | 43 | 44 | 45 | 46 | Max network capacity 47 | 48 | 49 | 50 | 51 | 52 |
53 | ); 54 | 55 | NetworkInfoCard.propTypes = { 56 | networkInfo: PropTypes.object.isRequired, 57 | }; 58 | -------------------------------------------------------------------------------- /src/client/scenes/create-invoice/create-invoice-modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'; 4 | import { CreateInvoiceForm } from './components/create-invoice-form'; 5 | import { ModalAlert } from '../../components/modal-alert'; 6 | import { parseJson } from '../../services/rest-helpers'; 7 | 8 | export class CreateInvoiceModal extends React.Component { 9 | static propTypes = { 10 | onInvoiceCreated: PropTypes.func, 11 | }; 12 | 13 | state = { 14 | open: false, 15 | form: undefined, 16 | error: undefined, 17 | }; 18 | 19 | toggle = () => { 20 | this.setState({ open: !this.state.open, error: undefined }); 21 | }; 22 | 23 | ok = () => { 24 | this.createInvoice(this.state.form) 25 | .then(invoice => { 26 | if (this.props.onInvoiceCreated) this.props.onInvoiceCreated(invoice); 27 | }) 28 | .then(this.toggle) 29 | .catch(error => this.setState({ error })); 30 | }; 31 | 32 | createInvoice = ({ memo, value }) => { 33 | return fetch('/api/invoices', { 34 | method: 'post', 35 | headers: { 36 | 'Content-Type': 'application/json', 37 | }, 38 | body: JSON.stringify({ memo, value }), 39 | credentials: 'same-origin', 40 | }).then(parseJson); 41 | }; 42 | 43 | formChanged = form => { 44 | this.setState({ form }); 45 | }; 46 | 47 | render() { 48 | let valid = this.state.form && this.state.form.valid; 49 | let { open, error } = this.state; 50 | return ( 51 |
52 | 55 | 56 | Create invoice 57 | 58 | 59 | 60 | 61 | 62 | 65 | 68 | 69 | 70 |
71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/client/scenes/disconnect-peer/disconnect-peer-modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'; 4 | import { ModalAlert } from '../../components/modal-alert'; 5 | import { parseJson } from '../../services/rest-helpers'; 6 | 7 | export class DisconnectPeerModal extends React.Component { 8 | static propTypes = { 9 | peer: PropTypes.object.isRequired, 10 | onPeerDisconnected: PropTypes.func, 11 | }; 12 | 13 | state = { 14 | open: false, 15 | error: undefined, 16 | }; 17 | 18 | toggle = () => { 19 | this.setState({ open: !this.state.open, error: undefined }); 20 | }; 21 | 22 | ok = () => { 23 | this.disconnectPeer() 24 | .then(peer => { 25 | if (this.props.onPeerDisconnected) this.props.onPeerDisconnected(peer); 26 | }) 27 | .then(this.toggle) 28 | .catch(error => this.setState({ error })); 29 | }; 30 | 31 | disconnectPeer() { 32 | let { pub_key } = this.props.peer; 33 | return fetch('/api/peers', { 34 | method: 'delete', 35 | headers: { 36 | 'Content-Type': 'application/json', 37 | }, 38 | body: JSON.stringify({ pub_key }), 39 | credentials: 'same-origin', 40 | }).then(parseJson); 41 | } 42 | 43 | render() { 44 | let { peer } = this.props; 45 | let { error } = this.state; 46 | return ( 47 |
48 | 51 | 52 | Disconnect from peer 53 | 54 | 55 |
56 |
57 | Are you sure you want to disconnect from the peer? 58 |
59 |
60 |
61 |
Address:
62 |
{peer.address}
63 |
64 |
65 | 66 | 69 | 72 | 73 |
74 |
75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/client/scenes/network/components/node-info-card.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Card, CardHeader, CardBody } from 'reactstrap'; 4 | import { DefList, DefListItem, DefListLabel, DefListValue } from '../../../components/def-list'; 5 | import { Timestamp } from '../../../components/timestamp'; 6 | import { Hex } from '../../../components/hex'; 7 | import { BtcAmount } from '../../../components/btc-amount'; 8 | import { ConnectPeerModal } from '../../connect-peer/connect-peer-modal'; 9 | import { OpenChannelModal } from '../../open-channel/open-channel-modal'; 10 | 11 | export const NodeInfoCard = ({ node }) => ( 12 | 13 | 14 |
15 | 16 |
17 |
18 | 22 |
23 |
Node
24 |
25 | {renderNode(node)} 26 |
27 | ); 28 | 29 | function renderNode(node) { 30 | if (!node) return ''; 31 | return ( 32 | 33 | 34 | Pub key: 35 | 36 | 37 | 38 | 39 | 40 | Alias 41 | 42 | {node.node.alias ? node.node.alias : Alias not configured} 43 | 44 | 45 | 46 | Addresses: 47 | {node.node.addresses.map(a => a.addr).join(', ')} 48 | 49 | 50 | Last updated: 51 | 52 | 53 | 54 | 55 | 56 | Channels: 57 | {node.num_channels} 58 | 59 | 60 | Total capacity: 61 | 62 | 63 | 64 | 65 | 66 | ); 67 | } 68 | 69 | NodeInfoCard.propTypes = { 70 | node: PropTypes.object, 71 | }; 72 | -------------------------------------------------------------------------------- /src/client/scenes/sign-message/sign-message-modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | Modal, 5 | ModalHeader, 6 | ModalBody, 7 | ModalFooter, 8 | Button, 9 | Form, 10 | FormGroup, 11 | Label, 12 | Input, 13 | } from 'reactstrap'; 14 | import { Hex } from '../../components/hex'; 15 | 16 | export class SignMessageModal extends React.Component { 17 | static propTypes = { 18 | open: PropTypes.bool.isRequired, 19 | toggle: PropTypes.func.isRequired, 20 | messageSigned: PropTypes.func, 21 | }; 22 | 23 | state = { 24 | msg: '', 25 | signedMessage: undefined, 26 | debounceTimeout: undefined, 27 | }; 28 | 29 | signMessage = () => { 30 | let { msg } = this.state; 31 | if (msg.length < 2) return; 32 | 33 | fetch('/api/message/sign', { 34 | method: 'post', 35 | headers: { 36 | 'Content-Type': 'application/json', 37 | }, 38 | body: JSON.stringify({ msg }), 39 | credentials: 'same-origin', 40 | }) 41 | .then(res => res.json()) 42 | .then(json => this.setState({ signedMessage: json.signature, msg })); 43 | }; 44 | 45 | formChanged = (prop, value) => { 46 | this.setState({ [prop]: value }); 47 | this.debounceSignMessage(); 48 | }; 49 | 50 | debounceSignMessage = () => { 51 | clearTimeout(this.state.debounceTimeout); 52 | let timeout = setTimeout(this.signMessage, 250); 53 | this.setState({ debounceTimeout: timeout }); 54 | }; 55 | 56 | render() { 57 | return ( 58 | 59 | Sign message 60 | 61 |
62 |
63 | 64 | 65 | this.formChanged('msg', e.target.value)} 69 | /> 70 | 71 |
72 |
73 | {this.renderSignedMessage()} 74 |
75 | 76 | 79 | 80 |
81 | ); 82 | } 83 | 84 | renderSignedMessage() { 85 | let { msg, signedMessage } = this.state; 86 | if (!signedMessage || !msg) return ''; 87 | return ( 88 |
89 |
Signed message:
90 |
91 | 92 |
93 |
94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/client/scenes/connect-peer/connect-peer-modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'; 4 | import { ConnectPeerForm } from './components/connect-peer-form'; 5 | import { ModalAlert } from '../../components/modal-alert'; 6 | import { parseJson } from '../../services/rest-helpers'; 7 | 8 | export class ConnectPeerModal extends React.Component { 9 | static propTypes = { 10 | connectionComplete: PropTypes.func, 11 | openPubkey: PropTypes.string, 12 | openHost: PropTypes.string, 13 | onPeerConnected: PropTypes.func, 14 | }; 15 | 16 | state = { 17 | open: false, 18 | pubkey: '', 19 | host: '', 20 | error: undefined, 21 | }; 22 | 23 | componentWillReceiveProps(nextProps) { 24 | if (nextProps.openPubkey && nextProps.openPubkey !== this.state.pubkey) { 25 | this.setState({ pubkey: nextProps.openPubkey, host: nextProps.openHost }); 26 | } 27 | } 28 | 29 | toggle = () => { 30 | this.setState({ open: !this.state.open, error: undefined }); 31 | }; 32 | 33 | ok = () => { 34 | this.connectToPeer(this.state) 35 | .then(peer => { 36 | if (this.props.onPeerConnected) this.props.onPeerConnected(peer); 37 | }) 38 | .then(this.toggle) 39 | .catch(error => this.setState({ error })); 40 | }; 41 | 42 | formUpdated = (key, value) => { 43 | this.setState({ [key]: value }); 44 | }; 45 | 46 | connectToPeer = ({ pubkey, host }) => { 47 | return fetch('/api/peers', { 48 | method: 'post', 49 | headers: { 50 | 'Content-Type': 'application/json', 51 | }, 52 | body: JSON.stringify({ pubkey, host }), 53 | credentials: 'same-origin', 54 | }).then(parseJson); 55 | }; 56 | 57 | render() { 58 | return ( 59 |
60 | 63 | 64 | Connect to peer 65 | 66 | 67 | 72 | 73 | 74 | 77 | 80 | 81 | 82 |
83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/client/components/hex.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Popover, PopoverBody } from 'reactstrap'; 4 | import { EntypoCopy } from 'react-entypo'; 5 | import copy from 'copy-to-clipboard'; 6 | import uuid4 from 'uuid/v4'; 7 | 8 | export class Hex extends React.PureComponent { 9 | static propTypes = { 10 | id: PropTypes.string, 11 | value: PropTypes.string, 12 | showStart: PropTypes.bool, 13 | substrLength: PropTypes.number, 14 | full: PropTypes.bool, 15 | }; 16 | 17 | state = { 18 | id: uuid4(), 19 | popoverOpen: false, 20 | }; 21 | 22 | componentWillMount() { 23 | if (this.props.id) this.setState({ id: this.props.id }); 24 | } 25 | 26 | toggle = () => { 27 | this.setState({ popoverOpen: !this.state.popoverOpen }); 28 | }; 29 | 30 | mouseEntered = () => { 31 | this.setState({ popoverOpen: true }); 32 | }; 33 | 34 | mouseExited = () => { 35 | this.setState({ popoverOpen: false }); 36 | }; 37 | 38 | copyClicked = e => { 39 | e.preventDefault(); 40 | copy(this.props.value); 41 | }; 42 | 43 | _truncateValue(value, showStart, substrLength) { 44 | if (showStart) return value.substr(0, substrLength); 45 | else return value.substr(Math.max(0, value.length - substrLength - 1), substrLength); 46 | } 47 | _renderValue(originalValue, truncatedValue, showStart) { 48 | if (truncatedValue.length === originalValue.length) 49 | return {truncatedValue}; 50 | else { 51 | if (showStart) return {truncatedValue}...; 52 | else return ...{truncatedValue}; 53 | } 54 | } 55 | 56 | render() { 57 | let { value, showStart = true, substrLength = 8, full } = this.props; 58 | let { popoverOpen, id } = this.state; 59 | let truncatedValue = full ? value : this._truncateValue(value, showStart, substrLength); 60 | let showPopover = !full; 61 | return ( 62 |
70 | {this._renderValue(value, truncatedValue, showStart)} 71 |   72 | 73 | 74 | 75 | {showPopover && ( 76 | 82 | {value} 83 | 84 | )} 85 |
86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/client/scenes/tools-menu.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'; 3 | import { NewAddressModal } from './new-address/new-address-modal'; 4 | import { SignMessageModal } from './sign-message/sign-message-modal'; 5 | import { VerifyMessageModal } from './verify-message/verify-message-modal'; 6 | 7 | export class ToolsMenu extends React.Component { 8 | static propTypes = {}; 9 | 10 | state = { 11 | open: false, 12 | nestedWitnessAddressModal: false, 13 | witnessAddressModal: false, 14 | pubkeyAddressModal: false, 15 | signModal: false, 16 | verifyModal: false, 17 | }; 18 | 19 | toggle = () => { 20 | this.setState({ open: !this.state.open }); 21 | }; 22 | 23 | toggleNestedWitnessAddressModal = () => { 24 | this.setState({ nestedWitnessAddressModal: !this.state.nestedWitnessAddressModal }); 25 | }; 26 | 27 | toggleWitnessAddressModal = () => { 28 | this.setState({ witnessAddressModal: !this.state.witnessAddressModal }); 29 | }; 30 | 31 | togglePubkeyAddressModal = () => { 32 | this.setState({ pubkeyAddressModal: !this.state.pubkeyAddressModal }); 33 | }; 34 | 35 | toggleSignModal = () => { 36 | this.setState({ signModal: !this.state.signModal }); 37 | }; 38 | 39 | toggleVerifyModal = () => { 40 | this.setState({ verifyModal: !this.state.verifyModal }); 41 | }; 42 | 43 | render() { 44 | return ( 45 | 46 | 51 | 56 | 61 | 62 | 63 | 64 | Tools 65 | 66 | 67 | 68 | New np2wkh address 69 | 70 | New p2wkh address 71 | New p2pkh address 72 | Sign message 73 | Verify message 74 | 75 | 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/client/scenes/send-payment/send-payment-modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'; 4 | import { SendPaymentForm } from './components/send-payment-form'; 5 | import { ModalAlert } from '../../components/modal-alert'; 6 | import { parseJson } from '../../services/rest-helpers'; 7 | import { DecodedPaymentRequest } from './components/decoded-payment-request'; 8 | 9 | export class SendPaymentModal extends React.Component { 10 | static propTypes = { 11 | paymentSent: PropTypes.func, 12 | }; 13 | 14 | state = { 15 | open: false, 16 | form: undefined, 17 | payreq: undefined, 18 | error: undefined, 19 | }; 20 | 21 | toggle = () => { 22 | this.setState({ open: !this.state.open, error: undefined }); 23 | }; 24 | 25 | ok = () => { 26 | this.sendPayment(this.state.form) 27 | .then(this.toggle) 28 | .catch(error => this.setState({ error })); 29 | }; 30 | 31 | sendPayment = ({ payment_request }) => { 32 | return fetch('/api/payment', { 33 | method: 'post', 34 | headers: { 35 | 'Content-Type': 'application/json', 36 | }, 37 | body: JSON.stringify({ payment_request }), 38 | credentials: 'same-origin', 39 | }).then(parseJson); 40 | }; 41 | 42 | decodeInvoice = ({ payment_request }) => { 43 | if (payment_request) { 44 | fetch('/api/payment/decode', { 45 | method: 'post', 46 | headers: { 47 | 'Content-Type': 'application/json', 48 | }, 49 | body: JSON.stringify({ payment_request }), 50 | credentials: 'same-origin', 51 | }) 52 | .then(parseJson) 53 | .then(payreq => this.setState({ payreq })) 54 | .catch(error => this.setState({ error })); 55 | } 56 | }; 57 | 58 | formChanged = form => { 59 | this.setState({ form }); 60 | this.decodeInvoice(form); 61 | }; 62 | 63 | render() { 64 | let valid = this.state.form && this.state.form.valid; 65 | let { error } = this.state; 66 | return ( 67 |
68 | 71 | 72 | Send payment 73 | 74 | 75 | 76 | 77 | 78 | 79 | 82 | 85 | 86 | 87 |
88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lnd-explorer", 3 | "version": "1.5.0", 4 | "description": "Explorer for Lightning Network nodes powered by LND", 5 | "scripts": { 6 | "start": "cross-env NODE_ENV=production node src/server/server", 7 | "build": "npm-run-all build:*", 8 | "build:app": "cross-env NODE_ENV=production browserify src/client/app.jsx --extension=.jsx -o ./dist/app/bundle.js", 9 | "build:app-compress": "uglifyjs -c -o ./dist/app/bundle.js ./dist/app/bundle.js", 10 | "build:scss": "node-sass -o dist/css --output-style compressed src/scss/app.scss", 11 | "watch": "npm-run-all --parallel watch:*", 12 | "watch:app": "browserify src/client/app.jsx --extension=.jsx -o ./dist/app/bundle.js && watchify src/client/app.jsx --debug -v --extension=.jsx -o ./dist/app/bundle.js", 13 | "watch:scss": "node-sass -o dist/css src/scss/app.scss && node-sass -w -o dist/css src/scss/app.scss", 14 | "watch:server": "nodemon --ignore dist --ignore src/client src/server/server" 15 | }, 16 | "keywords": [ 17 | "lnd", 18 | "lnd explorer", 19 | "lightning network", 20 | "lightning network explorer", 21 | "bitcoin", 22 | "litecoin", 23 | "bcashisascam" 24 | ], 25 | "author": "Brian Mancini ", 26 | "license": "MIT", 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/altangent/lnd-explorer.git" 30 | }, 31 | "dependencies": { 32 | "body-parser": "^1.18.3", 33 | "bootstrap": "^4.3.1", 34 | "compression": "^1.7.3", 35 | "copy-to-clipboard": "^3.0.8", 36 | "cross-env": "^5.2.0", 37 | "d3": "^4.13.0", 38 | "express": "^4.16.4", 39 | "grpc": "^1.24.9", 40 | "lnd-async": "^1.8.0", 41 | "moment": "^2.24.0", 42 | "node-cache": "^4.2.0", 43 | "prop-types": "^15.6.2", 44 | "pug": "^3.0.1", 45 | "react": "^16.8.1", 46 | "react-dom": "^16.8.1", 47 | "react-entypo": "^1.3.0", 48 | "react-router": "^4.3.1", 49 | "react-router-dom": "^4.3.1", 50 | "reactstrap": "^5.0.0", 51 | "serve-favicon": "^2.5.0", 52 | "serve-static": "^1.13.2", 53 | "socket.io": "^2.4.0", 54 | "socket.io-client": "^2.2.0", 55 | "uuid": "^3.3.2", 56 | "winston": "^2.4.4" 57 | }, 58 | "devDependencies": { 59 | "babel-core": "^6.26.3", 60 | "babel-eslint": "^8.2.6", 61 | "babel-plugin-transform-class-properties": "^6.24.1", 62 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 63 | "babel-preset-env": "^1.7.0", 64 | "babel-preset-react": "^6.24.1", 65 | "babelify": "^8.0.0", 66 | "browserify": "^15.2.0", 67 | "eslint": "^4.19.1", 68 | "eslint-plugin-react": "^7.12.4", 69 | "node-sass": "^4.13.1", 70 | "nodemon": "^1.18.9", 71 | "npm-run-all": "^4.1.5", 72 | "prettier": "^1.16.4", 73 | "uglify-js": "^3.4.9", 74 | "watchify": "^3.11.1" 75 | }, 76 | "browserify": { 77 | "transform": [ 78 | [ 79 | "babelify" 80 | ] 81 | ] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/client/scenes/home/home-scene.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { WalletBalanceCard } from './components/wallet-balance-card'; 3 | import { Loading } from '../../components/loading'; 4 | import { ChannelBalanceCard } from './components/channel-balance-card'; 5 | import { PendingChannelsCard } from './components/pending-channels-card'; 6 | import { ActiveChannelsCard } from './components/active-channels-card'; 7 | import { PeersCard } from './components/peers-card'; 8 | import { BlockchainCard } from './components/blockchain-card'; 9 | import { Hex } from '../../components/hex'; 10 | 11 | export class HomeScene extends React.Component { 12 | constructor() { 13 | super(); 14 | this.state = { 15 | info: undefined, 16 | channelBalance: undefined, 17 | walletBalance: undefined, 18 | }; 19 | } 20 | 21 | loadData() { 22 | fetch('/api/home', { credentials: 'same-origin' }) 23 | .then(res => res.json()) 24 | .then(data => this.setState(data)) 25 | .catch(err => this.setState({ loadError: err })); 26 | } 27 | 28 | componentWillMount() { 29 | this.loadData(); 30 | } 31 | 32 | render() { 33 | let { info, channelBalance, walletBalance } = this.state; 34 | if (!info) return ; 35 | return ( 36 |
37 |
38 |
39 | 40 |
41 |
42 | 43 |
44 |
45 | 46 |
47 |
48 | 49 |
50 |
51 |
52 |
53 | 54 |
55 |
56 | 57 |
58 |
59 |
60 |
61 |
62 | Alias: 63 |
{info.alias ? info.alias : Alias not configured}
64 |
65 |
66 | Pub Key: 67 | 68 |
69 |
70 | Address: 71 | {info.uris.length > 0 ? ( 72 | 73 | ) : ( 74 |
75 | External address not configured 76 |
77 | )} 78 |
79 |
80 |
81 |
82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/client/components/loading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Loading = () => ( 4 |
5 | 13 | 14 | 23 | 24 | 25 | 34 | 35 | 36 | 45 | 46 | 47 | 56 | 57 | 58 | 67 | 68 | 69 | 78 | 79 | 80 | 89 | 90 | 91 | 100 | 101 | 102 |
103 | ); 104 | -------------------------------------------------------------------------------- /src/client/scenes/verify-message/verify-message-modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | Modal, 5 | ModalHeader, 6 | ModalBody, 7 | ModalFooter, 8 | Button, 9 | FormGroup, 10 | Label, 11 | Input, 12 | } from 'reactstrap'; 13 | import { Hex } from '../../components/hex'; 14 | import { BoolValue } from '../../components/bool-value'; 15 | 16 | export class VerifyMessageModal extends React.Component { 17 | static propTypes = { 18 | open: PropTypes.bool.isRequired, 19 | toggle: PropTypes.func.isRequired, 20 | }; 21 | 22 | state = { 23 | msg: '', 24 | signature: '', 25 | debounceTimeout: undefined, 26 | verification: undefined, 27 | }; 28 | 29 | verifyMessage = () => { 30 | let { msg, signature } = this.state; 31 | if (!msg || !signature) return; 32 | fetch('/api/message/verify', { 33 | method: 'post', 34 | headers: { 35 | 'Content-Type': 'application/json', 36 | }, 37 | body: JSON.stringify({ msg, signature }), 38 | credentials: 'same-origin', 39 | }) 40 | .then(res => res.json()) 41 | .then(json => this.setState({ verification: json })); 42 | }; 43 | 44 | formChanged = (prop, value) => { 45 | this.setState({ [prop]: value }); 46 | this.debounceVerifyMessage(); 47 | }; 48 | 49 | debounceVerifyMessage = () => { 50 | clearTimeout(this.state.debounceTimeout); 51 | let timeout = setTimeout(this.verifyMessage, 300); 52 | this.setState({ debounceTimeout: timeout }); 53 | }; 54 | 55 | render() { 56 | return ( 57 | 58 | Verify message 59 | 60 |
61 |
62 | 63 | 64 | this.formChanged('msg', e.target.value)} 68 | /> 69 | 70 |
71 |
72 |
73 |
74 | 75 | 76 | this.formChanged('signature', e.target.value)} 80 | /> 81 | 82 |
83 |
84 | {this.renderVerification()} 85 |
86 | 87 | 90 | 91 |
92 | ); 93 | } 94 | 95 | renderVerification() { 96 | let { verification } = this.state; 97 | if (!verification) return ''; 98 | return ( 99 |
100 |
101 |
Valid:
102 |
103 | 104 |
105 |
106 |
107 |
Pub Key
108 |
109 | 110 |
111 |
112 |
113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/client/scenes/layout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Link, Route } from 'react-router-dom'; 4 | import { 5 | Badge, 6 | Nav, 7 | Navbar, 8 | NavItem, 9 | NavLink, 10 | NavbarBrand, 11 | Collapse, 12 | NavbarToggler, 13 | } from 'reactstrap'; 14 | 15 | import { ToastContainer } from './toast/toast-container'; 16 | import { HomeScene } from './home/home-scene'; 17 | import { TransactionsScene } from './transactions/transactions-scene'; 18 | import { PeersScene } from './peers/peers-scene'; 19 | import { ChannelsScene } from './channels/channels-scene'; 20 | import { InvoicesScene } from './invoices/invoices-scene'; 21 | import { PaymentsScene } from './payments/payments-scene'; 22 | import { NetworkScene } from './network/network-scene'; 23 | import { ToolsMenu } from './tools-menu'; 24 | 25 | export class Layout extends React.Component { 26 | static propTypes = { 27 | newTxs: PropTypes.array, 28 | toast: PropTypes.array, 29 | }; 30 | 31 | constructor(props) { 32 | super(props); 33 | this.toggle = this.toggle.bind(this); 34 | this.state = { 35 | isOpen: false, 36 | }; 37 | } 38 | 39 | toggle() { 40 | this.setState({ 41 | isOpen: !this.state.isOpen, 42 | }); 43 | } 44 | 45 | render() { 46 | return ( 47 |
48 | 49 |
50 | 51 | 52 | LND Explorer 53 | 54 | 55 | 56 | 94 | 95 | 96 |
97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 |
105 |
106 |
107 | Fork on GitHub | Powered by{' '} 108 | LND 109 |
110 |
111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/client/scenes/toast/toast-container.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Toast } from './components/toast'; 4 | import { withSocket } from '../../services/socket'; 5 | 6 | export class ToastContainerComponent extends React.Component { 7 | static propTypes = { 8 | socket: PropTypes.object.isRequired, 9 | }; 10 | 11 | state = { 12 | toasts: [], 13 | }; 14 | 15 | componentDidMount() { 16 | let { socket } = this.props; 17 | socket.on('openchannel', this.onOpenChannel); 18 | socket.on('openchannelerror', this.onOpenChannelError); 19 | socket.on('closechannel', this.onCloseChannel); 20 | socket.on('closechannelerror', this.onCloseChannelError); 21 | socket.on('sendpayment', this.onSendPayment); 22 | socket.on('sendpaymenterror', this.onSendPaymentError); 23 | socket.on('invoice', this.onInvoice); 24 | } 25 | 26 | componentWillUnmount() { 27 | let { socket } = this.props; 28 | socket.off('openchannel', this.onChannelOpen); 29 | socket.off('openchannelerror', this.onOpenChannelError); 30 | socket.off('closechannel', this.onCloseChannel); 31 | socket.off('closechannelerror', this.onCloseChannelError); 32 | socket.off('sendpayment', this.onSendPayment); 33 | socket.off('sendpaymenterror', this.onSendPaymentError); 34 | socket.off('invoice', this.onInvoice); 35 | } 36 | 37 | onOpenChannel = msg => { 38 | let toast = { type: 'success ' }; 39 | if (msg.update === 'chan_pending') { 40 | toast.message = 'Open channel has been initialized'; 41 | toast.autoclose = true; 42 | } 43 | if (msg.update === 'chan_open') toast.message = 'Channel has been successfully opened'; 44 | this.addToast(toast); 45 | }; 46 | 47 | onOpenChannelError = err => { 48 | let toast = { type: 'danger', message: 'Failed to open channel with error: ' + err.details }; 49 | this.addToast(toast); 50 | }; 51 | 52 | onCloseChannel = msg => { 53 | let toast = { type: 'success' }; 54 | if (msg.update === 'close_pending') { 55 | toast.message = 'Close channel has been initiated'; 56 | toast.autoclose = true; 57 | } 58 | if (msg.update === 'chan_close') toast.message = 'Channel has been closed'; 59 | this.addToast(toast); 60 | }; 61 | 62 | onCloseChannelError = err => { 63 | let toast = { type: 'danger', message: 'Failed to close channel with error: ' + err.details }; 64 | this.addToast(toast); 65 | }; 66 | 67 | onSendPayment = msg => { 68 | let toast; 69 | if (msg.payment_error) 70 | toast = { type: 'danger', message: 'Send payment failed with error: ' + msg.payment_error }; 71 | if (!msg.payment_error) 72 | toast = { 73 | type: 'success', 74 | message: 'Sent payment for ' + msg.payment_route.total_amt, 75 | autoclose: true, 76 | }; 77 | this.addToast(toast); 78 | }; 79 | 80 | onSendPaymentError = err => { 81 | let toast = { type: 'danger', message: 'Failed to send payment with error: ' + err.details }; 82 | this.addToast(toast); 83 | }; 84 | 85 | onInvoice = msg => { 86 | if (msg.settled) { 87 | let toast = { type: 'success', message: `Payment for '${msg.memo}' has been received.` }; 88 | this.addToast(toast); 89 | } 90 | }; 91 | 92 | addToast = toast => { 93 | let toasts = this.state.toasts.slice(); 94 | toasts.push(toast); 95 | this.setState({ toasts }); 96 | }; 97 | 98 | removeToast = toast => { 99 | let toasts = this.state.toasts.slice(); 100 | let index = toasts.findIndex(t => t === toast); 101 | toasts = toasts.splice(index, 1); 102 | this.setState({ toasts }); 103 | }; 104 | 105 | render() { 106 | let { toasts } = this.state; 107 | return ( 108 |
109 | {toasts.map((toast, i) => ( 110 | 111 | ))} 112 |
113 | ); 114 | } 115 | } 116 | 117 | export const ToastContainer = withSocket(ToastContainerComponent); 118 | -------------------------------------------------------------------------------- /src/client/scenes/open-channel/open-channel-modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { Modal, ModalHeader, ModalFooter, ModalBody, Button } from 'reactstrap'; 5 | import { OpenChannelForm } from './components/open-channel-form'; 6 | import { ModalAlert } from '../../components/modal-alert'; 7 | import { parseJson } from '../../services/rest-helpers'; 8 | import { peerSort } from '../../services/peer-helpers'; 9 | 10 | export class OpenChannelModal extends React.Component { 11 | static propTypes = { 12 | resolve: PropTypes.func, 13 | reject: PropTypes.func, 14 | openPubKey: PropTypes.string, 15 | }; 16 | 17 | state = { 18 | open: false, 19 | peers: undefined, 20 | selectedPeer: undefined, 21 | unconnectedPeer: false, 22 | localAmount: 0, 23 | pushAmount: 0, 24 | valid: false, 25 | error: undefined, 26 | }; 27 | 28 | toggle = () => { 29 | if (!this.state.open) this.loadPeers(); 30 | this.setState({ 31 | open: !this.state.open, 32 | error: undefined, 33 | selectedPeer: undefined, 34 | localAmount: 0, 35 | pushAmount: 0, 36 | }); 37 | }; 38 | 39 | ok = () => { 40 | let { selectedPeer, localAmount, pushAmount } = this.state; 41 | this.openChannel({ 42 | node_pubkey_string: selectedPeer, 43 | local_funding_amount: localAmount, 44 | push_sat: pushAmount, 45 | }) 46 | .then(this.toggle) 47 | .catch(error => this.setState({ error })); 48 | }; 49 | 50 | loadPeers = () => { 51 | fetch('/api/peers', { credentials: 'same-origin' }) 52 | .then(res => res.json()) 53 | .then(peers => { 54 | peers = peers.peers; 55 | peers.sort(peerSort); 56 | let selectedPeer = peers.find(p => p.pub_key === this.props.openPubKey); 57 | this.setState({ peers, selectedPeer: selectedPeer && selectedPeer.pub_key }); 58 | }); 59 | }; 60 | 61 | openChannel({ node_pubkey_string, local_funding_amount, push_sat }) { 62 | return fetch('/api/channels', { 63 | method: 'post', 64 | headers: { 65 | 'Content-Type': 'application/json', 66 | }, 67 | body: JSON.stringify({ node_pubkey_string, local_funding_amount, push_sat }), 68 | credentials: 'same-origin', 69 | }).then(parseJson); 70 | } 71 | 72 | formChanged = (prop, value) => { 73 | let valid = this.validate({ ...this.state, [prop]: value }); 74 | this.setState({ [prop]: value, valid }); 75 | }; 76 | 77 | validate = ({ selectedPeer, localAmount, pushAmount }) => { 78 | return selectedPeer && localAmount > pushAmount && pushAmount >= 0; 79 | }; 80 | 81 | render() { 82 | let { open, peers, error, valid } = this.state; 83 | let { openPubKey } = this.props; 84 | let showPeerWarning = openPubKey && peers && !peers.find(p => p.pub_key === openPubKey); 85 | return ( 86 |
87 | 90 | 91 | Open channel 92 | 93 | 94 | {showPeerWarning && ( 95 | 101 | )} 102 | {!showPeerWarning && ( 103 | 104 | )} 105 | 106 | 107 | 110 | 113 | 114 | 115 |
116 | ); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/client/scenes/network/components/network-graph.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import * as d3 from 'd3'; 4 | 5 | export class NetworkGraph extends React.Component { 6 | static propTypes = { 7 | onNodeSelected: PropTypes.func, 8 | }; 9 | 10 | componentDidMount() { 11 | this.renderNetworkGraph(this.svg); 12 | } 13 | 14 | shouldComponentUpdate() { 15 | return false; 16 | } 17 | 18 | render() { 19 | return (this.svg = elem)} />; 20 | } 21 | 22 | mapLndGraph(json) { 23 | let nodes = json.nodes; 24 | let links = json.edges.map(p => ({ 25 | source: p.node1_pub, 26 | target: p.node2_pub, 27 | edge: p, 28 | })); 29 | return { nodes, links }; 30 | } 31 | 32 | renderNetworkGraph = svg => { 33 | let { onNodeSelected } = this.props; 34 | svg = d3.select(svg); 35 | svg.attr('width', svg.node().parentNode.clientWidth).attr('height', 600); 36 | 37 | let width = svg.attr('width'); 38 | let height = svg.attr('height'); 39 | let maxNodes = 100; 40 | 41 | fetch('/api/home', { credentials: 'same-origin' }) 42 | .then(res => res.json()) 43 | .then(home => 44 | fetch('/api/graph?nodes=' + maxNodes, { credentials: 'same-origin' }) 45 | .then(res => res.json()) 46 | .then(graphJson => this.mapLndGraph(graphJson)) 47 | .then(graph => render(graph, home.info)) 48 | ); 49 | 50 | function render(graph, info) { 51 | let { nodes, links } = graph; 52 | let selectedPubKey = info.identity_pubkey; 53 | onNodeSelected(selectedPubKey); 54 | 55 | var g = svg.append('g'); 56 | 57 | let simulation = d3 58 | .forceSimulation() 59 | .force('links', d3.forceLink().id(d => d.pub_key)) 60 | .force( 61 | 'charge', 62 | d3 63 | .forceCollide() 64 | .radius(30) 65 | .strength(0.1) 66 | ) 67 | .force('charge', d3.forceManyBody().strength(-(width / 2.5))) 68 | .force('center', d3.forceCenter(width / 2, height / 2)); 69 | 70 | let link = g 71 | .append('g') 72 | .attr('class', 'links') 73 | .selectAll('line') 74 | .data(links) 75 | .enter() 76 | .append('line') 77 | .attr( 78 | 'class', 79 | d => (d.source === selectedPubKey || d.target === selectedPubKey ? 'selected' : '') 80 | ); 81 | 82 | let node = g 83 | .append('g') 84 | .attr('class', 'nodes') 85 | .selectAll('circle') 86 | .data(nodes) 87 | .enter() 88 | .append('circle') 89 | .attr('class', d => (d.pub_key === selectedPubKey ? 'selected' : '')) 90 | .attr('r', d => (d.pub_key === selectedPubKey ? 9 : 6)) 91 | .on('click', nodeClicked); 92 | 93 | simulation.nodes(nodes).on('tick', ticked); 94 | simulation.force('links').links(links); 95 | 96 | function ticked() { 97 | link 98 | .attr('x1', d => d.source.x) 99 | .attr('y1', d => d.source.y) 100 | .attr('x2', d => d.target.x) 101 | .attr('y2', d => d.target.y); 102 | 103 | node.attr('cx', d => d.x).attr('cy', d => d.y); 104 | } 105 | 106 | function nodeClicked(d) { 107 | selectedPubKey = d.pub_key; 108 | onNodeSelected(d.pub_key); 109 | 110 | d3 111 | .select('.nodes circle.selected') 112 | .attr('r', 6) 113 | .attr('class', null); 114 | d3 115 | .select(this) 116 | .attr('r', 9) 117 | .attr('class', 'selected'); 118 | 119 | // remove selected line 120 | d3.selectAll('.links .selected').attr('class', null); 121 | 122 | // add selected lines 123 | d3 124 | .selectAll('.links line') 125 | .attr( 126 | 'class', 127 | d => 128 | d.edge.node1_pub === selectedPubKey || d.edge.node2_pub === selectedPubKey 129 | ? 'selected' 130 | : '' 131 | ); 132 | } 133 | 134 | svg.call( 135 | d3 136 | .zoom() 137 | .scaleExtent([1 / 2, 8]) 138 | .on('zoom', zoomed) 139 | ); 140 | 141 | function zoomed() { 142 | g.attr('transform', d3.event.transform); 143 | } 144 | } 145 | }; 146 | } 147 | -------------------------------------------------------------------------------- /src/client/scenes/close-channel/close-channel-modal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | Modal, 5 | ModalHeader, 6 | ModalBody, 7 | ModalFooter, 8 | Button, 9 | FormGroup, 10 | Label, 11 | Input, 12 | } from 'reactstrap'; 13 | import { BtcAmount } from '../../components/btc-amount'; 14 | import { Hex } from '../../components/hex'; 15 | import { ModalAlert } from '../../components/modal-alert'; 16 | import { parseJson } from '../../services/rest-helpers'; 17 | 18 | export class CloseChannelModal extends React.Component { 19 | static propTypes = { 20 | channel: PropTypes.object, 21 | }; 22 | 23 | state = { 24 | open: false, 25 | error: undefined, 26 | force: false, 27 | }; 28 | 29 | toggle = () => this.setState({ open: !this.state.open, error: undefined, force: false }); 30 | 31 | ok = () => { 32 | let channel = this.props.channel; 33 | this.closeChannel(channel) 34 | .then(this.toggle) 35 | .catch(error => this.setState({ error })); 36 | }; 37 | 38 | closeChannel = channel => { 39 | return fetch('/api/channels', { 40 | method: 'delete', 41 | headers: { 42 | 'Content-Type': 'application/json', 43 | }, 44 | body: JSON.stringify({ channel_point: channel.channel_point, force: this.state.force }), 45 | credentials: 'same-origin', 46 | }).then(parseJson); 47 | }; 48 | 49 | render() { 50 | let { channel } = this.props; 51 | let { open, error, force } = this.state; 52 | if (!channel) return
; 53 | return ( 54 |
55 | 58 | 59 | Close channel? 60 | 61 | 62 |
63 |
64 | Are you sure you want to close the channel: 65 |
66 |
67 |
68 |
Channel id:
69 |
{channel.chan_id}
70 |
71 |
72 |
Channel point:
73 |
74 | 75 |
76 |
77 |
78 |
79 | This will settle the following balances: 80 |
81 |
82 |
83 |
84 |
85 |
Local:
86 |
87 | 88 |
89 |
90 |
91 |
92 |
93 |
Remote:
94 |
95 | 96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | 104 | 105 | 106 | this.setState({ force: e.target.checked })} 112 | /> 113 | 116 | 117 | 120 | 123 | 124 | 125 |
126 | ); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/client/scenes/channels/components/channels-card.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Link } from 'react-router-dom'; 3 | import { withRouter } from 'react-router'; 4 | import { Card, CardHeader, CardBody, Nav, NavItem, NavLink, Badge } from 'reactstrap'; 5 | import { ChannelsList } from './channels-list'; 6 | import { OpenChannelModal } from '../../open-channel/open-channel-modal'; 7 | 8 | import { OpenChannelsListHeader } from './open-channels-list-header'; 9 | import { OpenChannelsListItem } from './open-channels-list-item'; 10 | import { PendingOpenChannelsListItem } from './pending-open-channels-list-item'; 11 | import { PendingOpenChannelsListHeader } from './pending-open-channels-list-header'; 12 | import { PendingClosingChannelsListItem } from './pending-closing-channels-list-item'; 13 | import { PendingClosingChannelsListHeader } from './pending-closing-channels-list-header'; 14 | import { PendingForceChannelsListItem } from './pending-force-channels-list-item'; 15 | import { PendingForceChannelsListHeader } from './pending-force-channels-list-header'; 16 | 17 | export const ChannelsCard = withRouter(({ location, openChannels, pendingChannels }) => { 18 | return ( 19 | 20 | 21 |
22 | 23 |
24 | Channels 25 |
26 | 27 | 63 | ( 67 | 72 | )} 73 | /> 74 | ( 77 | 82 | )} 83 | /> 84 | ( 87 | 92 | )} 93 | /> 94 | ( 97 | 102 | )} 103 | /> 104 | 105 |
106 | ); 107 | }); 108 | 109 | function renderBadge(count) { 110 | if (!count) return ''; 111 | else 112 | return ( 113 | 114 | {count} 115 | 116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /src/server/api/api-network.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const winston = require('winston'); 3 | const lnd = require('../lnd'); 4 | const Cache = require('node-cache'); 5 | const app = express(); 6 | const cache = new Cache({ stdTTL: 60 }); 7 | 8 | app.get('/api/network', (req, res, next) => getNetwork(req, res).catch(next)); 9 | app.get('/api/network/:pub_key', (req, res, next) => getNode(req, res).catch(next)); 10 | app.get('/api/graph', (req, res, next) => getGraph(req, res).catch(next)); 11 | 12 | module.exports = app; 13 | 14 | async function getNetwork(req, res) { 15 | let networkInfo = cache.get('networkInfo') || (await loadNetworkInfo()); 16 | res.send({ networkInfo }); 17 | } 18 | 19 | async function getNode(req, res) { 20 | let { pub_key } = req.params; 21 | let node = await lnd.client.getNodeInfo({ pub_key }); 22 | res.send(node); 23 | } 24 | 25 | async function getGraph(req, res) { 26 | let nodeCount = parseInt(req.query.nodes) || 50; 27 | let info = await lnd.client.getInfo({}); 28 | let pubkey = info.identity_pubkey; 29 | 30 | let data = cache.get('networkGraph') || (await loadGraph()); 31 | 32 | let nodeMap = constructNodeMap(data); 33 | let edgeMap = constructEdgeMap(data); 34 | let prunedNodeMap = bfs(nodeMap, edgeMap, pubkey, nodeCount); 35 | let prunedData = destructNodeMap(prunedNodeMap, edgeMap); 36 | res.send(prunedData); 37 | } 38 | 39 | function constructNodeMap(data) { 40 | winston.profile('constructing node map'); 41 | let nodeMap = new Map(data.nodes.map(node => [node.pub_key, node])); 42 | winston.profile('constructing node map'); 43 | return nodeMap; 44 | } 45 | 46 | function constructEdgeMap({ edges }) { 47 | winston.profile('constructing edge map'); 48 | let edgeMap = new Map(); 49 | for (let edge of edges) { 50 | if (!edgeMap.has(edge.node1_pub)) edgeMap.set(edge.node1_pub, new Set()); 51 | edgeMap.get(edge.node1_pub).add(edge); 52 | 53 | if (!edgeMap.has(edge.node2_pub)) edgeMap.set(edge.node2_pub, new Set()); 54 | edgeMap.get(edge.node2_pub).add(edge); 55 | } 56 | winston.profile('constructing edge map'); 57 | return edgeMap; 58 | } 59 | 60 | function destructNodeMap(nodeMap, edgeMap) { 61 | let nodes = Array.from(nodeMap.values()); 62 | let edges = []; 63 | for (let node of nodes) { 64 | if (!edgeMap.has(node.pub_key)) continue; 65 | let nodeEdges = edgeMap.get(node.pub_key).values(); 66 | for (let edge of nodeEdges) { 67 | if (nodeMap.has(edge.node1_pub) && nodeMap.has(edge.node2_pub)) edges.push(edge); 68 | } 69 | } 70 | return { nodes, edges }; 71 | } 72 | 73 | function bfs(nodeMap, edgeMap, pubkey, max) { 74 | winston.profile('breadth first search'); 75 | let visited = new Map(); 76 | let start = nodeMap.get(pubkey); 77 | 78 | function processStep(nodeQueue, filter) { 79 | winston.info('processing round with', nodeQueue.length); 80 | let resultNodes = []; 81 | 82 | // ensure our node queue is sorted so that our 83 | // node distribution isn't stale nodes or nodes 84 | // that we cannot connect with 85 | if (filter) nodeQueue = sortNodes(filterNodes(nodeQueue)); 86 | else nodeQueue = sortNodes(nodeQueue); 87 | 88 | while (visited.size < max) { 89 | let node = nodeQueue.shift(); 90 | if (!node) break; 91 | 92 | visited.set(node.pub_key, node); 93 | 94 | if (!edgeMap.has(node.pub_key)) continue; 95 | 96 | let edges = edgeMap.get(node.pub_key).values(); 97 | 98 | for (let edge of edges) { 99 | let sourceNode = nodeMap.get(edge.node1_pub); 100 | if (!visited.has(sourceNode.pub_key)) resultNodes.push(sourceNode); 101 | 102 | let targetNode = nodeMap.get(edge.node2_pub); 103 | if (!visited.has(targetNode.pub_key)) resultNodes.push(targetNode); 104 | } 105 | } 106 | 107 | return resultNodes; 108 | } 109 | 110 | let resultNodes = processStep([start], false); 111 | while (visited.size < max && resultNodes.length) { 112 | resultNodes = processStep(resultNodes, true); 113 | } 114 | 115 | winston.profile('breadth first search'); 116 | return visited; 117 | } 118 | 119 | function filterNodes(nodes) { 120 | return nodes.filter(n => n.addresses.length); 121 | } 122 | 123 | function sortNodes(nodes) { 124 | let result = nodes.slice(); 125 | result.sort((a, b) => { 126 | if (a.last_update < b.last_update) return 1; 127 | else if (a.last_update > b.last_update) return -1; 128 | else return 0; 129 | }); 130 | return result; 131 | } 132 | 133 | /////////////////////////// 134 | 135 | async function loadGraph() { 136 | winston.profile('loading graph'); 137 | let data = await lnd.client.describeGraph({}); 138 | cache.set('networkGraph', data); 139 | winston.profile('loading graph'); 140 | return data; 141 | } 142 | 143 | async function loadNetworkInfo() { 144 | winston.profile('loading network info'); 145 | let data = await lnd.client.getNetworkInfo({}); 146 | cache.set('networkInfo', data); 147 | winston.profile('loading network info'); 148 | return data; 149 | } 150 | -------------------------------------------------------------------------------- /src/scss/_bootstrap-variables.scss: -------------------------------------------------------------------------------- 1 | // bootstrap.scss 2 | // https://getbootstrap.com/docs/4.0/getting-started/theming/ 3 | 4 | // 5 | // Color system 6 | // 7 | 8 | // stylelint-disable 9 | $white: #fff !default; 10 | $gray-100: #f8f9fa !default; 11 | $gray-200: #e9ecef !default; 12 | $gray-300: #dee2e6 !default; 13 | $gray-400: #ced4da !default; 14 | $gray-500: #adb5bd !default; 15 | $gray-600: #6c757d !default; 16 | $gray-700: #495057 !default; 17 | $gray-800: #343a40 !default; 18 | $gray-900: #212529 !default; 19 | $black: #000 !default; 20 | 21 | $grays: () !default; 22 | $grays: map-merge( 23 | 24 | ( 25 | '100': $gray-100, 26 | '200': $gray-200, 27 | '300': $gray-300, 28 | '400': $gray-400, 29 | '500': $gray-500, 30 | '600': $gray-600, 31 | '700': $gray-700, 32 | '800': $gray-800, 33 | '900': $gray-900 34 | ), 35 | $grays 36 | ); 37 | 38 | // $blue: #007bff !default; 39 | // $indigo: #6610f2 !default; 40 | // $purple: #6f42c1 !default; 41 | // $pink: #e83e8c !default; 42 | // $red: #dc3545 !default; 43 | // $orange: #fd7e14 !default; 44 | // $yellow: #ffc107 !default; 45 | // $green: #28a745 !default; 46 | // $teal: #20c997 !default; 47 | // $cyan: #17a2b8 !default; 48 | 49 | // $colors: () !default; 50 | // $colors: map-merge(( 51 | // "blue": $blue, 52 | // "indigo": $indigo, 53 | // "purple": $purple, 54 | // "pink": $pink, 55 | // "red": $red, 56 | // "orange": $orange, 57 | // "yellow": $yellow, 58 | // "green": $green, 59 | // "teal": $teal, 60 | // "cyan": $cyan, 61 | // "white": $white, 62 | // "gray": $gray-600, 63 | // "gray-dark": $gray-800 64 | // ), $colors); 65 | 66 | $primary: #338dc9; 67 | $secondary: $primary + 24%; 68 | // $success: $green !default; 69 | // $info: $cyan !default; 70 | // $warning: $yellow !default; 71 | // $danger: $red !default; 72 | $light: #222229 + 86%; 73 | $dark: #222229 - 18%; 74 | 75 | // $theme-colors: () !default; 76 | // $theme-colors: map-merge(( 77 | // "primary": $primary, 78 | // "secondary": $secondary, 79 | // "success": $success, 80 | // "info": $info, 81 | // "warning": $warning, 82 | // "danger": $danger, 83 | // "light": $light, 84 | // "dark": $dark 85 | // ), $theme-colors); 86 | // // stylelint-enable 87 | 88 | // // Set a specific jump point for requesting color jumps 89 | // $theme-color-interval: 8% !default; 90 | 91 | // // The yiq lightness value that determines when the lightness of color changes from "dark" to "light". Acceptable values are between 0 and 255. 92 | // $yiq-contrasted-threshold: 150 !default; 93 | 94 | // // Customize the light and dark text colors for use in our YIQ color contrast function. 95 | // $yiq-text-dark: $gray-900 !default; 96 | // $yiq-text-light: $white !default; 97 | 98 | // // Options 99 | // // 100 | // // Quickly modify global styling by enabling or disabling optional features. 101 | 102 | // $enable-caret: true !default; 103 | // $enable-rounded: true !default; 104 | // $enable-shadows: false !default; 105 | // $enable-gradients: false !default; 106 | // $enable-transitions: true !default; 107 | // $enable-hover-media-query: false !default; // Deprecated, no longer affects any compiled CSS 108 | // $enable-grid-classes: true !default; 109 | // $enable-print-styles: true !default; 110 | 111 | // // Spacing 112 | // // 113 | // // Control the default styling of most Bootstrap elements by modifying these 114 | // // variables. Mostly focused on spacing. 115 | // // You can add more entries to the $spacers map, should you need more variation. 116 | 117 | // // stylelint-disable 118 | // $spacer: 1rem !default; 119 | // $spacers: () !default; 120 | // $spacers: map-merge(( 121 | // 0: 0, 122 | // 1: ($spacer * .25), 123 | // 2: ($spacer * .5), 124 | // 3: $spacer, 125 | // 4: ($spacer * 1.5), 126 | // 5: ($spacer * 3) 127 | // ), $spacers); 128 | 129 | // // This variable affects the `.h-*` and `.w-*` classes. 130 | // $sizes: () !default; 131 | // $sizes: map-merge(( 132 | // 25: 25%, 133 | // 50: 50%, 134 | // 75: 75%, 135 | // 100: 100% 136 | // ), $sizes); 137 | // // stylelint-enable 138 | 139 | // Body 140 | // 141 | // Settings for the `` element. 142 | 143 | $body-bg: #222229; 144 | $body-color: #bbb; 145 | 146 | // Custom App Variables 147 | $comp-bg: $body-bg + 6%; 148 | $comp-border-color: $body-bg + 18; 149 | 150 | // // Links 151 | // // 152 | // // Style anchor elements. 153 | 154 | // $link-color: theme-color("primary") !default; 155 | // $link-decoration: none !default; 156 | // $link-hover-color: darken($link-color, 15%) !default; 157 | // $link-hover-decoration: underline !default; 158 | 159 | // // Paragraphs 160 | // // 161 | // // Style p element. 162 | 163 | // $paragraph-margin-bottom: 1rem !default; 164 | 165 | // // Grid breakpoints 166 | // // 167 | // // Define the minimum dimensions at which your layout will change, 168 | // // adapting to different screen sizes, for use in media queries. 169 | 170 | // $grid-breakpoints: ( 171 | // xs: 0, 172 | // sm: 576px, 173 | // md: 768px, 174 | // lg: 992px, 175 | // xl: 1200px 176 | // ) !default; 177 | 178 | // @include _assert-ascending($grid-breakpoints, "$grid-breakpoints"); 179 | // @include _assert-starts-at-zero($grid-breakpoints); 180 | 181 | // // Grid containers 182 | // // 183 | // // Define the maximum width of `.container` for different screen sizes. 184 | 185 | // $container-max-widths: ( 186 | // sm: 540px, 187 | // md: 720px, 188 | // lg: 960px, 189 | // xl: 1140px 190 | // ) !default; 191 | 192 | // @include _assert-ascending($container-max-widths, "$container-max-widths"); 193 | 194 | // // Grid columns 195 | // // 196 | // // Set the number of columns and specify the width of the gutters. 197 | 198 | // $grid-columns: 12 !default; 199 | // $grid-gutter-width: 30px !default; 200 | 201 | // // Components 202 | // // 203 | // // Define common padding and border radius sizes and more. 204 | 205 | // $line-height-lg: 1.5 !default; 206 | // $line-height-sm: 1.5 !default; 207 | 208 | // $border-width: 1px !default; 209 | // $border-color: $gray-300 !default; 210 | 211 | $border-radius: 0; 212 | $border-radius-lg: 0; 213 | $border-radius-sm: 0; 214 | 215 | // $component-active-color: $white !default; 216 | // $component-active-bg: theme-color("primary") !default; 217 | 218 | // $caret-width: .3em !default; 219 | 220 | // $transition-base: all .2s ease-in-out !default; 221 | // $transition-fade: opacity .15s linear !default; 222 | // $transition-collapse: height .35s ease !default; 223 | 224 | // // Fonts 225 | // // 226 | // // Font, line-height, and color for body text, headings, and more. 227 | 228 | // // stylelint-disable value-keyword-case 229 | // $font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !default; 230 | // $font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !default; 231 | // $font-family-base: $font-family-sans-serif !default; 232 | // // stylelint-enable value-keyword-case 233 | 234 | $font-size-base: 0.8rem; // Assumes the browser default, typically `16px` 235 | // $font-size-lg: ($font-size-base * 1.25) !default; 236 | // $font-size-sm: ($font-size-base * .875) !default; 237 | 238 | // $font-weight-light: 300 !default; 239 | // $font-weight-normal: 400 !default; 240 | // $font-weight-bold: 700 !default; 241 | 242 | // $font-weight-base: $font-weight-normal !default; 243 | // $line-height-base: 1.5 !default; 244 | 245 | // $h1-font-size: $font-size-base * 2.5 !default; 246 | // $h2-font-size: $font-size-base * 2 !default; 247 | // $h3-font-size: $font-size-base * 1.75 !default; 248 | // $h4-font-size: $font-size-base * 1.5 !default; 249 | // $h5-font-size: $font-size-base * 1.25 !default; 250 | // $h6-font-size: $font-size-base !default; 251 | 252 | // $headings-margin-bottom: ($spacer / 2) !default; 253 | // $headings-font-family: inherit !default; 254 | // $headings-font-weight: 500 !default; 255 | // $headings-line-height: 1.2 !default; 256 | // $headings-color: inherit !default; 257 | 258 | // $display1-size: 6rem !default; 259 | // $display2-size: 5.5rem !default; 260 | // $display3-size: 4.5rem !default; 261 | // $display4-size: 3.5rem !default; 262 | 263 | // $display1-weight: 300 !default; 264 | // $display2-weight: 300 !default; 265 | // $display3-weight: 300 !default; 266 | // $display4-weight: 300 !default; 267 | // $display-line-height: $headings-line-height !default; 268 | 269 | // $lead-font-size: ($font-size-base * 1.25) !default; 270 | // $lead-font-weight: 300 !default; 271 | 272 | // $small-font-size: 80% !default; 273 | 274 | // $text-muted: $gray-600 !default; 275 | 276 | // $blockquote-small-color: $gray-600 !default; 277 | // $blockquote-font-size: ($font-size-base * 1.25) !default; 278 | 279 | // $hr-border-color: rgba($black, .1) !default; 280 | // $hr-border-width: $border-width !default; 281 | 282 | // $mark-padding: .2em !default; 283 | 284 | // $dt-font-weight: $font-weight-bold !default; 285 | 286 | // $kbd-box-shadow: inset 0 -.1rem 0 rgba($black, .25) !default; 287 | // $nested-kbd-font-weight: $font-weight-bold !default; 288 | 289 | // $list-inline-padding: .5rem !default; 290 | 291 | // $mark-bg: #fcf8e3 !default; 292 | 293 | // $hr-margin-y: $spacer !default; 294 | 295 | // Tables 296 | // 297 | // Customizes the `.table` component with basic values, each used across all table variations. 298 | 299 | // $table-cell-padding: .75rem !default; 300 | // $table-cell-padding-sm: .3rem !default; 301 | 302 | $table-bg: $comp-bg; 303 | // $table-accent-bg: rgba($black, .05) !default; 304 | // $table-hover-bg: rgba($black, .075) !default; 305 | // $table-active-bg: $table-hover-bg !default; 306 | 307 | // $table-border-width: $border-width !default; 308 | $table-border-color: $comp-border-color; 309 | 310 | // $table-head-bg: $gray-200 !default; 311 | // $table-head-color: $gray-700 !default; 312 | 313 | // $table-dark-bg: $gray-900 !default; 314 | // $table-dark-accent-bg: rgba($white, .05) !default; 315 | // $table-dark-hover-bg: rgba($white, .075) !default; 316 | // $table-dark-border-color: lighten($gray-900, 7.5%) !default; 317 | // $table-dark-color: $body-bg !default; 318 | 319 | // // Buttons + Forms 320 | // // 321 | // // Shared variables that are reassigned to `$input-` and `$btn-` specific variables. 322 | 323 | // $input-btn-padding-y: .375rem !default; 324 | // $input-btn-padding-x: .75rem !default; 325 | // $input-btn-line-height: $line-height-base !default; 326 | 327 | // $input-btn-focus-width: .2rem !default; 328 | // $input-btn-focus-color: rgba($component-active-bg, .25) !default; 329 | // $input-btn-focus-box-shadow: 0 0 0 $input-btn-focus-width $input-btn-focus-color !default; 330 | 331 | // $input-btn-padding-y-sm: .25rem !default; 332 | // $input-btn-padding-x-sm: .5rem !default; 333 | // $input-btn-line-height-sm: $line-height-sm !default; 334 | 335 | // $input-btn-padding-y-lg: .5rem !default; 336 | // $input-btn-padding-x-lg: 1rem !default; 337 | // $input-btn-line-height-lg: $line-height-lg !default; 338 | 339 | // $input-btn-border-width: $border-width !default; 340 | 341 | // // Buttons 342 | // // 343 | // // For each of Bootstrap's buttons, define text, background, and border color. 344 | 345 | // $btn-padding-y: $input-btn-padding-y !default; 346 | // $btn-padding-x: $input-btn-padding-x !default; 347 | // $btn-line-height: $input-btn-line-height !default; 348 | 349 | // $btn-padding-y-sm: $input-btn-padding-y-sm !default; 350 | // $btn-padding-x-sm: $input-btn-padding-x-sm !default; 351 | // $btn-line-height-sm: $input-btn-line-height-sm !default; 352 | 353 | // $btn-padding-y-lg: $input-btn-padding-y-lg !default; 354 | // $btn-padding-x-lg: $input-btn-padding-x-lg !default; 355 | // $btn-line-height-lg: $input-btn-line-height-lg !default; 356 | 357 | // $btn-border-width: $input-btn-border-width !default; 358 | 359 | // $btn-font-weight: $font-weight-normal !default; 360 | // $btn-box-shadow: inset 0 1px 0 rgba($white, .15), 0 1px 1px rgba($black, .075) !default; 361 | // $btn-focus-width: $input-btn-focus-width !default; 362 | // $btn-focus-box-shadow: $input-btn-focus-box-shadow !default; 363 | // $btn-disabled-opacity: .65 !default; 364 | // $btn-active-box-shadow: inset 0 3px 5px rgba($black, .125) !default; 365 | 366 | // $btn-link-disabled-color: $gray-600 !default; 367 | 368 | // $btn-block-spacing-y: .5rem !default; 369 | 370 | // // Allows for customizing button radius independently from global border radius 371 | // $btn-border-radius: $border-radius !default; 372 | // $btn-border-radius-lg: $border-radius-lg !default; 373 | $btn-border-radius-sm: 2px; 374 | 375 | // $btn-transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out !default; 376 | 377 | // // Forms 378 | 379 | // $input-padding-y: $input-btn-padding-y !default; 380 | // $input-padding-x: $input-btn-padding-x !default; 381 | // $input-line-height: $input-btn-line-height !default; 382 | 383 | // $input-padding-y-sm: $input-btn-padding-y-sm !default; 384 | // $input-padding-x-sm: $input-btn-padding-x-sm !default; 385 | // $input-line-height-sm: $input-btn-line-height-sm !default; 386 | 387 | // $input-padding-y-lg: $input-btn-padding-y-lg !default; 388 | // $input-padding-x-lg: $input-btn-padding-x-lg !default; 389 | // $input-line-height-lg: $input-btn-line-height-lg !default; 390 | 391 | $input-bg: $comp-bg + 36%; 392 | // $input-disabled-bg: $gray-200 !default; 393 | 394 | $input-color: $body-color; 395 | $input-border-color: $input-bg; 396 | $input-border-width: 1px; 397 | // $input-box-shadow: inset 0 1px 1px rgba($black, .075) !default; 398 | 399 | $input-border-radius: 0.3rem; 400 | // $input-border-radius-lg: $border-radius-lg !default; 401 | // $input-border-radius-sm: $border-radius-sm !default; 402 | 403 | // $input-focus-bg: $input-bg !default; 404 | // $input-focus-border-color: lighten($component-active-bg, 25%) !default; 405 | // $input-focus-color: $input-color !default; 406 | // $input-focus-width: $input-btn-focus-width !default; 407 | // $input-focus-box-shadow: $input-btn-focus-box-shadow !default; 408 | 409 | // $input-placeholder-color: $gray-600 !default; 410 | 411 | // $input-height-border: $input-border-width * 2 !default; 412 | 413 | // $input-height-inner: ($font-size-base * $input-btn-line-height) + ($input-btn-padding-y * 2) !default; 414 | // $input-height: calc(#{$input-height-inner} + #{$input-height-border}) !default; 415 | 416 | // $input-height-inner-sm: ($font-size-sm * $input-btn-line-height-sm) + ($input-btn-padding-y-sm * 2) !default; 417 | // $input-height-sm: calc(#{$input-height-inner-sm} + #{$input-height-border}) !default; 418 | 419 | // $input-height-inner-lg: ($font-size-lg * $input-btn-line-height-lg) + ($input-btn-padding-y-lg * 2) !default; 420 | // $input-height-lg: calc(#{$input-height-inner-lg} + #{$input-height-border}) !default; 421 | 422 | // $input-transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out !default; 423 | 424 | // $form-text-margin-top: .25rem !default; 425 | 426 | // $form-check-input-gutter: 1.25rem !default; 427 | // $form-check-input-margin-y: .3rem !default; 428 | // $form-check-input-margin-x: .25rem !default; 429 | 430 | // $form-check-inline-margin-x: .75rem !default; 431 | // $form-check-inline-input-margin-x: .3125rem !default; 432 | 433 | // $form-group-margin-bottom: 1rem !default; 434 | 435 | // $input-group-addon-color: $input-color !default; 436 | // $input-group-addon-bg: $gray-200 !default; 437 | // $input-group-addon-border-color: $input-border-color !default; 438 | 439 | // $custom-control-gutter: 1.5rem !default; 440 | // $custom-control-spacer-x: 1rem !default; 441 | 442 | // $custom-control-indicator-size: 1rem !default; 443 | // $custom-control-indicator-bg: $gray-300 !default; 444 | // $custom-control-indicator-bg-size: 50% 50% !default; 445 | // $custom-control-indicator-box-shadow: inset 0 .25rem .25rem rgba($black, .1) !default; 446 | 447 | // $custom-control-indicator-disabled-bg: $gray-200 !default; 448 | // $custom-control-label-disabled-color: $gray-600 !default; 449 | 450 | // $custom-control-indicator-checked-color: $component-active-color !default; 451 | // $custom-control-indicator-checked-bg: $component-active-bg !default; 452 | // $custom-control-indicator-checked-disabled-bg: rgba(theme-color("primary"), .5) !default; 453 | // $custom-control-indicator-checked-box-shadow: none !default; 454 | 455 | // $custom-control-indicator-focus-box-shadow: 0 0 0 1px $body-bg, $input-btn-focus-box-shadow !default; 456 | 457 | // $custom-control-indicator-active-color: $component-active-color !default; 458 | // $custom-control-indicator-active-bg: lighten($component-active-bg, 35%) !default; 459 | // $custom-control-indicator-active-box-shadow: none !default; 460 | 461 | // $custom-checkbox-indicator-border-radius: $border-radius !default; 462 | // $custom-checkbox-indicator-icon-checked: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='#{$custom-control-indicator-checked-color}' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E"), "#", "%23") !default; 463 | 464 | // $custom-checkbox-indicator-indeterminate-bg: $component-active-bg !default; 465 | // $custom-checkbox-indicator-indeterminate-color: $custom-control-indicator-checked-color !default; 466 | // $custom-checkbox-indicator-icon-indeterminate: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='#{$custom-checkbox-indicator-indeterminate-color}' d='M0 2h4'/%3E%3C/svg%3E"), "#", "%23") !default; 467 | // $custom-checkbox-indicator-indeterminate-box-shadow: none !default; 468 | 469 | // $custom-radio-indicator-border-radius: 50% !default; 470 | // $custom-radio-indicator-icon-checked: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='#{$custom-control-indicator-checked-color}'/%3E%3C/svg%3E"), "#", "%23") !default; 471 | 472 | // $custom-select-padding-y: .375rem !default; 473 | // $custom-select-padding-x: .75rem !default; 474 | // $custom-select-height: $input-height !default; 475 | // $custom-select-indicator-padding: 1rem !default; // Extra padding to account for the presence of the background-image based indicator 476 | // $custom-select-line-height: $input-btn-line-height !default; 477 | // $custom-select-color: $input-color !default; 478 | // $custom-select-disabled-color: $gray-600 !default; 479 | // $custom-select-bg: $white !default; 480 | // $custom-select-disabled-bg: $gray-200 !default; 481 | // $custom-select-bg-size: 8px 10px !default; // In pixels because image dimensions 482 | // $custom-select-indicator-color: $gray-800 !default; 483 | // $custom-select-indicator: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='#{$custom-select-indicator-color}' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E"), "#", "%23") !default; 484 | // $custom-select-border-width: $input-btn-border-width !default; 485 | // $custom-select-border-color: $input-border-color !default; 486 | // $custom-select-border-radius: $border-radius !default; 487 | 488 | // $custom-select-focus-border-color: $input-focus-border-color !default; 489 | // $custom-select-focus-box-shadow: inset 0 1px 2px rgba($black, .075), 0 0 5px rgba($custom-select-focus-border-color, .5) !default; 490 | 491 | // $custom-select-font-size-sm: 75% !default; 492 | // $custom-select-height-sm: $input-height-sm !default; 493 | 494 | // $custom-select-font-size-lg: 125% !default; 495 | // $custom-select-height-lg: $input-height-lg !default; 496 | 497 | // $custom-file-height: $input-height !default; 498 | // $custom-file-focus-border-color: $input-focus-border-color !default; 499 | // $custom-file-focus-box-shadow: $input-btn-focus-box-shadow !default; 500 | 501 | // $custom-file-padding-y: $input-btn-padding-y !default; 502 | // $custom-file-padding-x: $input-btn-padding-x !default; 503 | // $custom-file-line-height: $input-btn-line-height !default; 504 | // $custom-file-color: $input-color !default; 505 | // $custom-file-bg: $input-bg !default; 506 | // $custom-file-border-width: $input-btn-border-width !default; 507 | // $custom-file-border-color: $input-border-color !default; 508 | // $custom-file-border-radius: $input-border-radius !default; 509 | // $custom-file-box-shadow: $input-box-shadow !default; 510 | // $custom-file-button-color: $custom-file-color !default; 511 | // $custom-file-button-bg: $input-group-addon-bg !default; 512 | // $custom-file-text: ( 513 | // en: "Browse" 514 | // ) !default; 515 | 516 | // // Form validation 517 | // $form-feedback-margin-top: $form-text-margin-top !default; 518 | // $form-feedback-font-size: $small-font-size !default; 519 | // $form-feedback-valid-color: theme-color("success") !default; 520 | // $form-feedback-invalid-color: theme-color("danger") !default; 521 | 522 | // Dropdowns 523 | // 524 | // Dropdown menu container and contents. 525 | 526 | // $dropdown-min-width: 10rem !default; 527 | // $dropdown-padding-y: .5rem !default; 528 | // $dropdown-spacer: .125rem !default; 529 | $dropdown-bg: $comp-bg + 6%; 530 | $dropdown-border-color: $comp-border-color; 531 | $dropdown-border-radius: 0.3rem; 532 | $dropdown-border-width: 0; 533 | $dropdown-divider-bg: $comp-border-color; 534 | // $dropdown-box-shadow: 0 .5rem 1rem rgba($black, .175) !default; 535 | 536 | $dropdown-link-color: $body-color; 537 | $dropdown-link-hover-color: $dropdown-link-color - 6%; 538 | $dropdown-link-hover-bg: $dropdown-bg + 6%; 539 | 540 | $dropdown-link-active-color: $dropdown-link-color - 6%; 541 | $dropdown-link-active-bg: $dropdown-bg + 6%; 542 | 543 | // $dropdown-link-disabled-color: $gray-600 !default; 544 | 545 | // $dropdown-item-padding-y: .25rem !default; 546 | // $dropdown-item-padding-x: 1.5rem !default; 547 | 548 | // $dropdown-header-color: $gray-600 !default; 549 | 550 | // // Z-index master list 551 | // // 552 | // // Warning: Avoid customizing these values. They're used for a bird's eye view 553 | // // of components dependent on the z-axis and are designed to all work together. 554 | 555 | // $zindex-dropdown: 1000 !default; 556 | // $zindex-sticky: 1020 !default; 557 | // $zindex-fixed: 1030 !default; 558 | // $zindex-modal-backdrop: 1040 !default; 559 | // $zindex-modal: 1050 !default; 560 | // $zindex-popover: 1060 !default; 561 | // $zindex-tooltip: 1070 !default; 562 | 563 | // // Navs 564 | 565 | // $nav-link-padding-y: .5rem !default; 566 | // $nav-link-padding-x: 1rem !default; 567 | // $nav-link-disabled-color: $gray-600 !default; 568 | 569 | $nav-tabs-border-color: transparent; 570 | $nav-tabs-border-width: 1px; 571 | $nav-tabs-border-radius: 3px; 572 | $nav-tabs-link-hover-border-color: transparent; 573 | $nav-tabs-link-active-color: $body-color; 574 | $nav-tabs-link-active-bg: $body-bg; 575 | $nav-tabs-link-active-border-color: transparent; 576 | 577 | // $nav-pills-border-radius: $border-radius !default; 578 | // $nav-pills-link-active-color: $component-active-color !default; 579 | // $nav-pills-link-active-bg: $component-active-bg !default; 580 | 581 | // // Navbar 582 | 583 | // $navbar-padding-y: ($spacer / 2) !default; 584 | // $navbar-padding-x: $spacer !default; 585 | 586 | // $navbar-nav-link-padding-x: .5rem !default; 587 | 588 | // $navbar-brand-font-size: $font-size-lg !default; 589 | // // Compute the navbar-brand padding-y so the navbar-brand will have the same height as navbar-text and nav-link 590 | // $nav-link-height: ($font-size-base * $line-height-base + $nav-link-padding-y * 2) !default; 591 | // $navbar-brand-height: $navbar-brand-font-size * $line-height-base !default; 592 | // $navbar-brand-padding-y: ($nav-link-height - $navbar-brand-height) / 2 !default; 593 | 594 | // $navbar-toggler-padding-y: .25rem !default; 595 | // $navbar-toggler-padding-x: .75rem !default; 596 | // $navbar-toggler-font-size: $font-size-lg !default; 597 | // $navbar-toggler-border-radius: $btn-border-radius !default; 598 | 599 | // $navbar-dark-color: rgba($white, .5) !default; 600 | // $navbar-dark-hover-color: rgba($white, .75) !default; 601 | // $navbar-dark-active-color: $white !default; 602 | // $navbar-dark-disabled-color: rgba($white, .25) !default; 603 | // $navbar-dark-toggler-icon-bg: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='#{$navbar-dark-color}' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E"), "#", "%23") !default; 604 | // $navbar-dark-toggler-border-color: rgba($white, .1) !default; 605 | 606 | // navbar-light-color: $body-bg + 6%; 607 | // $navbar-light-hover-color: rgba($black, 0.7) !default; 608 | // $navbar-light-active-color: rgba($black, 0.9) !default; 609 | // $navbar-light-disabled-color: rgba($black, 0.3) !default; 610 | // $navbar-light-toggler-icon-bg: str-replace( 611 | // url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='#{$navbar-light-color}' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E"), 612 | // '#', 613 | // '%23' 614 | // ) 615 | // !default; 616 | // $navbar-light-toggler-border-color: rgba($black, 0.1) !default; 617 | 618 | // // Pagination 619 | 620 | // $pagination-padding-y: .5rem !default; 621 | // $pagination-padding-x: .75rem !default; 622 | // $pagination-padding-y-sm: .25rem !default; 623 | // $pagination-padding-x-sm: .5rem !default; 624 | // $pagination-padding-y-lg: .75rem !default; 625 | // $pagination-padding-x-lg: 1.5rem !default; 626 | // $pagination-line-height: 1.25 !default; 627 | 628 | // $pagination-color: $link-color !default; 629 | // $pagination-bg: $white !default; 630 | // $pagination-border-width: $border-width !default; 631 | // $pagination-border-color: $gray-300 !default; 632 | 633 | // $pagination-focus-box-shadow: $input-btn-focus-box-shadow !default; 634 | 635 | // $pagination-hover-color: $link-hover-color !default; 636 | // $pagination-hover-bg: $gray-200 !default; 637 | // $pagination-hover-border-color: $gray-300 !default; 638 | 639 | // $pagination-active-color: $component-active-color !default; 640 | // $pagination-active-bg: $component-active-bg !default; 641 | // $pagination-active-border-color: $pagination-active-bg !default; 642 | 643 | // $pagination-disabled-color: $gray-600 !default; 644 | // $pagination-disabled-bg: $white !default; 645 | // $pagination-disabled-border-color: $gray-300 !default; 646 | 647 | // // Jumbotron 648 | 649 | // $jumbotron-padding: 2rem !default; 650 | // $jumbotron-bg: $gray-200 !default; 651 | 652 | // // Cards 653 | 654 | // $card-spacer-y: .75rem !default; 655 | // $card-spacer-x: 1.25rem !default; 656 | $card-border-width: 0; 657 | $card-border-radius: 0.3rem; 658 | $card-border-color: $comp-bg; 659 | $card-inner-border-radius: 0.3rem; 660 | $card-cap-bg: $comp-bg; 661 | $card-bg: $comp-bg; 662 | 663 | // $card-img-overlay-padding: 1.25rem !default; 664 | 665 | // $card-group-margin: ($grid-gutter-width / 2) !default; 666 | // $card-deck-margin: $card-group-margin !default; 667 | 668 | // $card-columns-count: 3 !default; 669 | // $card-columns-gap: 1.25rem !default; 670 | // $card-columns-margin: $card-spacer-y !default; 671 | 672 | // // Tooltips 673 | 674 | // $tooltip-font-size: $font-size-sm !default; 675 | // $tooltip-max-width: 200px !default; 676 | // $tooltip-color: $white !default; 677 | // $tooltip-bg: $black !default; 678 | // $tooltip-border-radius: $border-radius !default; 679 | // $tooltip-opacity: .9 !default; 680 | // $tooltip-padding-y: .25rem !default; 681 | // $tooltip-padding-x: .5rem !default; 682 | // $tooltip-margin: 0 !default; 683 | 684 | // $tooltip-arrow-width: .8rem !default; 685 | // $tooltip-arrow-height: .4rem !default; 686 | // $tooltip-arrow-color: $tooltip-bg !default; 687 | 688 | // Popovers 689 | 690 | // $popover-font-size: $font-size-sm !default; 691 | $popover-bg: $comp-bg + 6%; 692 | // $popover-max-width: 276px !default; 693 | $popover-border-width: 0; 694 | // $popover-border-color: rgba($black, .2) !default; 695 | // $popover-border-radius: $border-radius-lg !default; 696 | // $popover-box-shadow: 0 .25rem .5rem rgba($black, .2) !default; 697 | 698 | // $popover-header-bg: darken($popover-bg, 3%) !default; 699 | // $popover-header-color: $headings-color !default; 700 | // $popover-header-padding-y: .5rem !default; 701 | // $popover-header-padding-x: .75rem !default; 702 | 703 | // $popover-body-color: $body-color !default; 704 | // $popover-body-padding-y: $popover-header-padding-y !default; 705 | // $popover-body-padding-x: $popover-header-padding-x !default; 706 | 707 | // $popover-arrow-width: 1rem !default; 708 | // $popover-arrow-height: .5rem !default; 709 | // $popover-arrow-color: $popover-bg !default; 710 | 711 | // $popover-arrow-outer-color: fade-in($popover-border-color, .05) !default; 712 | 713 | // // Badges 714 | 715 | // $badge-font-size: 75% !default; 716 | // $badge-font-weight: $font-weight-bold !default; 717 | // $badge-padding-y: .25em !default; 718 | // $badge-padding-x: .4em !default; 719 | // $badge-border-radius: $border-radius !default; 720 | 721 | // $badge-pill-padding-x: .6em !default; 722 | // // Use a higher than normal value to ensure completely rounded edges when 723 | // // customizing padding or font-size on labels. 724 | $badge-pill-border-radius: 2rem; 725 | 726 | // Modals 727 | 728 | // // Padding applied to the modal body 729 | // $modal-inner-padding: 1rem !default; 730 | 731 | // $modal-dialog-margin: .5rem !default; 732 | // $modal-dialog-margin-y-sm-up: 1.75rem !default; 733 | 734 | // $modal-title-line-height: $line-height-base !default; 735 | 736 | $modal-content-bg: $comp-bg; 737 | // $modal-content-border-color: rgba($black, 0.2) !default; 738 | // $modal-content-border-width: $border-width !default; 739 | // $modal-content-box-shadow-xs: 0 .25rem .5rem rgba($black, .5) !default; 740 | // $modal-content-box-shadow-sm-up: 0 .5rem 1rem rgba($black, .5) !default; 741 | 742 | // $modal-backdrop-bg: $black !default; 743 | // $modal-backdrop-opacity: .5 !default; 744 | $modal-header-border-color: $comp-border-color; 745 | $modal-footer-border-color: $comp-border-color; 746 | // $modal-header-border-width: $modal-content-border-width !default; 747 | // $modal-footer-border-width: $modal-header-border-width !default; 748 | // $modal-header-padding: 1rem !default; 749 | 750 | // $modal-lg: 800px !default; 751 | // $modal-md: 500px !default; 752 | // $modal-sm: 300px !default; 753 | 754 | // $modal-transition: transform .3s ease-out !default; 755 | 756 | // // Alerts 757 | // // 758 | // // Define alert colors, border radius, and padding. 759 | 760 | // $alert-padding-y: .75rem !default; 761 | // $alert-padding-x: 1.25rem !default; 762 | // $alert-margin-bottom: 1rem !default; 763 | // $alert-border-radius: $border-radius !default; 764 | // $alert-link-font-weight: $font-weight-bold !default; 765 | // $alert-border-width: $border-width !default; 766 | 767 | // $alert-bg-level: -10 !default; 768 | // $alert-border-level: -9 !default; 769 | // $alert-color-level: 6 !default; 770 | 771 | // // Progress bars 772 | 773 | // $progress-height: 1rem !default; 774 | // $progress-font-size: ($font-size-base * .75) !default; 775 | // $progress-bg: $gray-200 !default; 776 | // $progress-border-radius: $border-radius !default; 777 | // $progress-box-shadow: inset 0 .1rem .1rem rgba($black, .1) !default; 778 | // $progress-bar-color: $white !default; 779 | // $progress-bar-bg: theme-color("primary") !default; 780 | // $progress-bar-animation-timing: 1s linear infinite !default; 781 | // $progress-bar-transition: width .6s ease !default; 782 | 783 | // List group 784 | 785 | $list-group-bg: $comp-bg; 786 | $list-group-border-color: $comp-border-color; 787 | // $list-group-border-width: $border-width !default; 788 | // $list-group-border-radius: $border-radius !default; 789 | 790 | // $list-group-item-padding-y: .75rem !default; 791 | // $list-group-item-padding-x: 1.25rem !default; 792 | 793 | // $list-group-hover-bg: $gray-100 !default; 794 | // $list-group-active-color: $component-active-color !default; 795 | // $list-group-active-bg: $component-active-bg !default; 796 | // $list-group-active-border-color: $list-group-active-bg !default; 797 | 798 | // $list-group-disabled-color: $gray-600 !default; 799 | // $list-group-disabled-bg: $list-group-bg !default; 800 | 801 | // $list-group-action-color: $gray-700 !default; 802 | // $list-group-action-hover-color: $list-group-action-color !default; 803 | 804 | // $list-group-action-active-color: $body-color !default; 805 | // $list-group-action-active-bg: $gray-200 !default; 806 | 807 | // // Image thumbnails 808 | 809 | // $thumbnail-padding: .25rem !default; 810 | // $thumbnail-bg: $body-bg !default; 811 | // $thumbnail-border-width: $border-width !default; 812 | // $thumbnail-border-color: $gray-300 !default; 813 | // $thumbnail-border-radius: $border-radius !default; 814 | // $thumbnail-box-shadow: 0 1px 2px rgba($black, .075) !default; 815 | 816 | // // Figures 817 | 818 | // $figure-caption-font-size: 90% !default; 819 | // $figure-caption-color: $gray-600 !default; 820 | 821 | // // Breadcrumbs 822 | 823 | // $breadcrumb-padding-y: .75rem !default; 824 | // $breadcrumb-padding-x: 1rem !default; 825 | // $breadcrumb-item-padding: .5rem !default; 826 | 827 | // $breadcrumb-margin-bottom: 1rem !default; 828 | 829 | // $breadcrumb-bg: $gray-200 !default; 830 | // $breadcrumb-divider-color: $gray-600 !default; 831 | // $breadcrumb-active-color: $gray-600 !default; 832 | // $breadcrumb-divider: "/" !default; 833 | 834 | // // Carousel 835 | 836 | // $carousel-control-color: $white !default; 837 | // $carousel-control-width: 15% !default; 838 | // $carousel-control-opacity: .5 !default; 839 | 840 | // $carousel-indicator-width: 30px !default; 841 | // $carousel-indicator-height: 3px !default; 842 | // $carousel-indicator-spacer: 3px !default; 843 | // $carousel-indicator-active-bg: $white !default; 844 | 845 | // $carousel-caption-width: 70% !default; 846 | // $carousel-caption-color: $white !default; 847 | 848 | // $carousel-control-icon-width: 20px !default; 849 | 850 | // $carousel-control-prev-icon-bg: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='#{$carousel-control-color}' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E"), "#", "%23") !default; 851 | // $carousel-control-next-icon-bg: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='#{$carousel-control-color}' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E"), "#", "%23") !default; 852 | 853 | // $carousel-transition: transform .6s ease !default; 854 | 855 | // // Close 856 | 857 | // $close-font-size: $font-size-base * 1.5 !default; 858 | // $close-font-weight: $font-weight-bold !default; 859 | // $close-color: $black !default; 860 | // $close-text-shadow: 0 1px 0 $white !default; 861 | 862 | // // Code 863 | 864 | // $code-font-size: 87.5% !default; 865 | // $code-color: $pink !default; 866 | 867 | // $kbd-padding-y: .2rem !default; 868 | // $kbd-padding-x: .4rem !default; 869 | // $kbd-font-size: $code-font-size !default; 870 | // $kbd-color: $white !default; 871 | // $kbd-bg: $gray-900 !default; 872 | 873 | // $pre-color: $gray-900 !default; 874 | // $pre-scrollable-max-height: 340px !default; 875 | 876 | // // Printing 877 | // $print-page-size: a3 !default; 878 | // $print-body-min-width: map-get($grid-breakpoints, "lg") !default; 879 | --------------------------------------------------------------------------------