├── 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 |
7 | );
8 |
9 | export const InfoCardTitle = ({ children }) => (
10 |
15 | );
16 |
17 | export const InfoCardValue = ({ children, size }) => (
18 |
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 | Tx hash
11 | Amount
12 | Num confirmations
13 | Block hash
14 | Block height
15 | Timestamp
16 | Total fees
17 | Dest addreses
18 |
19 |
20 | {transactions.map(tx => )}
21 |
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 | Payment hash
12 | Value
13 | Creation date
14 | Path
15 | Fee
16 |
17 |
18 |
19 | {payments.map(payment => )}
20 |
21 |
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 | Memo
11 | Receipt
12 | R preimage
13 | R hash
14 | Value
15 | Settled
16 | Creation date
17 | Settle date
18 | Payment request
19 |
20 |
21 |
22 | {invoices.map(invoice => (
23 |
24 | ))}
25 |
26 |
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 |
12 | Pub key
13 | Address
14 | Bytes sent
15 | Bytes Recv
16 | Sat sent
17 | Sat recv
18 | Ping time
19 |
20 |
21 |
22 | {peers
23 | .sort(peerSort)
24 | .map(peer => (
25 |
30 | ))}
31 |
32 |
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 |
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 |
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 |
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 |
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 |
48 | Ok
49 |
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 |
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 |
53 | New invoice
54 |
55 |
56 | Create invoice
57 |
58 |
59 |
60 |
61 |
62 |
63 | Create
64 |
65 |
66 | Cancel
67 |
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 |
49 | Disconnect
50 |
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 |
67 | Disconnect
68 |
69 |
70 | Cancel
71 |
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 |
72 |
73 | {this.renderSignedMessage()}
74 |
75 |
76 |
77 | Ok
78 |
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 |
61 | Connect to peer
62 |
63 |
64 | Connect to peer
65 |
66 |
67 |
72 |
73 |
74 |
75 | Connect
76 |
77 |
78 | Cancel
79 |
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 |
69 | Send payment
70 |
71 |
72 | Send payment
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | Send
81 |
82 |
83 | Cancel
84 |
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 |
44 |
47 |
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 | Message
64 | this.formChanged('msg', e.target.value)}
68 | />
69 |
70 |
71 |
72 |
73 |
74 |
75 | Signature
76 | this.formChanged('signature', e.target.value)}
80 | />
81 |
82 |
83 |
84 | {this.renderVerification()}
85 |
86 |
87 |
88 | Close
89 |
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 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | Info
60 |
61 |
62 |
63 |
64 | Transactions
65 |
66 |
67 |
68 |
69 | Peers
70 |
71 |
72 |
73 |
74 | Channels
75 |
76 |
77 |
78 |
79 | Invoices
80 |
81 |
82 |
83 |
84 | Payments
85 |
86 |
87 |
88 |
89 | Network
90 |
91 |
92 |
93 |
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 |
88 | Open channel
89 |
90 |
91 | Open channel
92 |
93 |
94 | {showPeerWarning && (
95 |
101 | )}
102 | {!showPeerWarning && (
103 |
104 | )}
105 |
106 |
107 |
108 | Open
109 |
110 |
111 | Cancel
112 |
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 |
56 | Close
57 |
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 |
103 |
104 |
105 |
106 | this.setState({ force: e.target.checked })}
112 | />
113 |
114 | Force
115 |
116 |
117 |
118 | Close
119 |
120 |
121 | Cancel
122 |
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 |
28 |
29 |
30 | Open {renderBadge(openChannels.length)}
31 |
32 |
33 |
34 |
39 | Pending open {renderBadge(pendingChannels.pending_open_channels.length)}
40 |
41 |
42 |
43 |
48 | Closing {renderBadge(pendingChannels.pending_closing_channels.length)}
49 |
50 |
51 |
52 |
57 | Force closing {renderBadge(
58 | pendingChannels.pending_force_closing_channels.length
59 | )}
60 |
61 |
62 |
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 |
--------------------------------------------------------------------------------