mirror of
https://github.com/Retropex/umbrel-bitcoin.git
synced 2025-05-12 03:00:49 +02:00
Implement Bitcoin app
This commit is contained in:
parent
4607e73b00
commit
ac88a2bf2c
@ -19,4 +19,5 @@ coverage
|
||||
.nyc_output
|
||||
Makefile
|
||||
*.env
|
||||
.github/
|
||||
.github/
|
||||
bitcoin
|
67
.github/workflows/on-push.yml
vendored
67
.github/workflows/on-push.yml
vendored
@ -1,52 +1,33 @@
|
||||
name: Docker build on push
|
||||
env:
|
||||
DOCKER_CLI_EXPERIMENTAL: enabled
|
||||
name: Build and publish Docker image on push
|
||||
|
||||
on: push
|
||||
|
||||
env:
|
||||
DOCKER_CLI_EXPERIMENTAL: enabled
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-18.04
|
||||
name: Build and push middleware image
|
||||
steps:
|
||||
- name: Set env variables
|
||||
run: echo "BRANCH=$(echo ${GITHUB_REF#refs/heads/} | sed 's/\//-/g')" >> $GITHUB_ENV
|
||||
build:
|
||||
name: Build Docker image
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- name: Set BRANCH
|
||||
run: echo "BRANCH=$(echo ${GITHUB_REF#refs/heads/} | sed 's/\//-/g')" >> $GITHUB_ENV
|
||||
|
||||
- name: Show set env variables
|
||||
run: |
|
||||
printf " BRANCH: %s\n" "$BRANCH"
|
||||
- name: Login to Docker Hub
|
||||
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to Docker Hub
|
||||
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
- name: Checkout project
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Checkout project
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
id: qemu
|
||||
- name: Setup Docker buildx action
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Setup Docker buildx action
|
||||
uses: docker/setup-buildx-action@v1
|
||||
id: buildx
|
||||
|
||||
- name: Show available Docker buildx platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
id: cache
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Run Docker buildx
|
||||
run: |
|
||||
docker buildx build \
|
||||
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||
--tag ${{ secrets.DOCKER_HUB_USER }}/middleware:$BRANCH \
|
||||
--output "type=registry" ./
|
||||
- name: Run Docker buildx
|
||||
run: |
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag ${{ secrets.DOCKER_HUB_USER }}/umbrel-bitcoin:$BRANCH \
|
||||
--push .
|
83
.github/workflows/on-tag.yml
vendored
83
.github/workflows/on-tag.yml
vendored
@ -1,65 +1,36 @@
|
||||
name: Docker build on tag
|
||||
env:
|
||||
DOCKER_CLI_EXPERIMENTAL: enabled
|
||||
name: Build and publish Docker image on tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v[0-9]+.[0-9]+.[0-9]+
|
||||
- v[0-9]+.[0-9]+.[0-9]+-*
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
env:
|
||||
DOCKER_CLI_EXPERIMENTAL: enabled
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-18.04
|
||||
name: Build and push middleware image
|
||||
steps:
|
||||
- name: Setup Environment
|
||||
run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
||||
build:
|
||||
name: Build Docker image
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- name: Set VERSION
|
||||
run: echo "VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Show set environment variables
|
||||
run: |
|
||||
printf " TAG: %s\n" "$TAG"
|
||||
- name: Login to Docker Hub
|
||||
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
|
||||
- name: Login to Docker Hub
|
||||
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
- name: Checkout project
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Checkout project
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
id: qemu
|
||||
- name: Setup Docker buildx action
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Setup Docker buildx action
|
||||
uses: docker/setup-buildx-action@v1
|
||||
id: buildx
|
||||
|
||||
- name: Show available Docker buildx platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
id: cache
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Run Docker buildx against tag
|
||||
run: |
|
||||
docker buildx build \
|
||||
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||
--tag ${{ secrets.DOCKER_HUB_USER }}/middleware:$TAG \
|
||||
--output "type=registry" ./
|
||||
|
||||
- name: Run Docker buildx against latest
|
||||
run: |
|
||||
docker buildx build \
|
||||
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
||||
--tag ${{ secrets.DOCKER_HUB_USER }}/middleware:latest \
|
||||
--output "type=registry" ./
|
||||
- name: Run Docker buildx
|
||||
run: |
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag ${{ secrets.DOCKER_HUB_USER }}/umbrel-bitcoin:$VERSION \
|
||||
--push .
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -9,4 +9,5 @@ logs/
|
||||
lb_settings.json
|
||||
.nyc_output
|
||||
coverage
|
||||
.todo
|
||||
.todo
|
||||
bitcoin
|
||||
|
@ -18,6 +18,9 @@ RUN yarn install --production
|
||||
# Copy project files and folders to the current working directory (i.e. '/app')
|
||||
COPY . .
|
||||
|
||||
RUN yarn install:ui
|
||||
RUN yarn build:ui
|
||||
|
||||
# Final image
|
||||
FROM node:12-buster-slim AS umbrel-middleware
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
The following license covers the code in commit 4607e73b0044b23bab21c86d2218450f50523fa6
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2018-2019 Casa, Inc. https://keys.casa/
|
133
LICENSE.md
Normal file
133
LICENSE.md
Normal file
@ -0,0 +1,133 @@
|
||||
> Umbrel is licensed under the PolyForm Noncommercial License 1.0.0. Please refer to our [License FAQ](https://github.com/getumbrel/umbrel/wiki/License-FAQ) if you have any questions or reach out to us directly at help@getumbrel.com.
|
||||
|
||||
# PolyForm Noncommercial License 1.0.0
|
||||
|
||||
<https://polyformproject.org/licenses/noncommercial/1.0.0>
|
||||
|
||||
## Acceptance
|
||||
|
||||
In order to get any license under these terms, you must agree
|
||||
to them as both strict obligations and conditions to all
|
||||
your licenses.
|
||||
|
||||
## Copyright License
|
||||
|
||||
The licensor grants you a copyright license for the
|
||||
software to do everything you might do with the software
|
||||
that would otherwise infringe the licensor's copyright
|
||||
in it for any permitted purpose. However, you may
|
||||
only distribute the software according to [Distribution
|
||||
License](#distribution-license) and make changes or new works
|
||||
based on the software according to [Changes and New Works
|
||||
License](#changes-and-new-works-license).
|
||||
|
||||
## Distribution License
|
||||
|
||||
The licensor grants you an additional copyright license
|
||||
to distribute copies of the software. Your license
|
||||
to distribute covers distributing the software with
|
||||
changes and new works permitted by [Changes and New Works
|
||||
License](#changes-and-new-works-license).
|
||||
|
||||
## Notices
|
||||
|
||||
You must ensure that anyone who gets a copy of any part of
|
||||
the software from you also gets a copy of these terms or the
|
||||
URL for them above, as well as copies of any plain-text lines
|
||||
beginning with `Required Notice:` that the licensor provided
|
||||
with the software. For example:
|
||||
|
||||
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
|
||||
|
||||
## Changes and New Works License
|
||||
|
||||
The licensor grants you an additional copyright license to
|
||||
make changes and new works based on the software for any
|
||||
permitted purpose.
|
||||
|
||||
## Patent License
|
||||
|
||||
The licensor grants you a patent license for the software that
|
||||
covers patent claims the licensor can license, or becomes able
|
||||
to license, that you would infringe by using the software.
|
||||
|
||||
## Noncommercial Purposes
|
||||
|
||||
Any noncommercial purpose is a permitted purpose.
|
||||
|
||||
## Personal Uses
|
||||
|
||||
Personal use for research, experiment, and testing for
|
||||
the benefit of public knowledge, personal study, private
|
||||
entertainment, hobby projects, amateur pursuits, or religious
|
||||
observance, without any anticipated commercial application,
|
||||
is use for a permitted purpose.
|
||||
|
||||
## Noncommercial Organizations
|
||||
|
||||
Use by any charitable organization, educational institution,
|
||||
public research organization, public safety or health
|
||||
organization, environmental protection organization,
|
||||
or government institution is use for a permitted purpose
|
||||
regardless of the source of funding or obligations resulting
|
||||
from the funding.
|
||||
|
||||
## Fair Use
|
||||
|
||||
You may have "fair use" rights for the software under the
|
||||
law. These terms do not limit them.
|
||||
|
||||
## No Other Rights
|
||||
|
||||
These terms do not allow you to sublicense or transfer any of
|
||||
your licenses to anyone else, or prevent the licensor from
|
||||
granting licenses to anyone else. These terms do not imply
|
||||
any other licenses.
|
||||
|
||||
## Patent Defense
|
||||
|
||||
If you make any written claim that the software infringes or
|
||||
contributes to infringement of any patent, your patent license
|
||||
for the software granted under these terms ends immediately. If
|
||||
your company makes such a claim, your patent license ends
|
||||
immediately for work on behalf of your company.
|
||||
|
||||
## Violations
|
||||
|
||||
The first time you are notified in writing that you have
|
||||
violated any of these terms, or done anything with the software
|
||||
not covered by your licenses, your licenses can nonetheless
|
||||
continue if you come into full compliance with these terms,
|
||||
and take practical steps to correct past violations, within
|
||||
32 days of receiving notice. Otherwise, all your licenses
|
||||
end immediately.
|
||||
|
||||
## No Liability
|
||||
|
||||
***As far as the law allows, the software comes as is, without
|
||||
any warranty or condition, and the licensor will not be liable
|
||||
to you for any damages arising out of these terms or the use
|
||||
or nature of the software, under any kind of legal claim.***
|
||||
|
||||
## Definitions
|
||||
|
||||
The **licensor** is the individual or entity offering these
|
||||
terms, and the **software** is the software the licensor makes
|
||||
available under these terms.
|
||||
|
||||
**You** refers to the individual or entity agreeing to these
|
||||
terms.
|
||||
|
||||
**Your company** is any legal entity, sole proprietorship,
|
||||
or other kind of organization that you work for, plus all
|
||||
organizations that have control over, are under the control of,
|
||||
or are under common control with that organization. **Control**
|
||||
means ownership of substantially all the assets of an entity,
|
||||
or the power to direct its management and policies by vote,
|
||||
contract, or otherwise. Control can be direct or indirect.
|
||||
|
||||
**Your licenses** are all the licenses granted to you for the
|
||||
software under these terms.
|
||||
|
||||
**Use** means anything you do with the software requiring one
|
||||
of your licenses.
|
32
README.md
32
README.md
@ -10,19 +10,18 @@
|
||||
[](https://reddit.com/r/getumbrel)
|
||||
|
||||
|
||||
# ☂️ middleware
|
||||
# ☂️ bitcoin
|
||||
|
||||
Middleware runs by-default on [Umbrel OS](https://github.com/getumbrel/umbrel-os) as a containerized service. It wraps [Bitcoin Core](https://github.com/bitcoin/bitcoin)'s RPC and [LND](https://github.com/lightningnetwork/lnd)'s gRPC, and exposes them via a RESTful API.
|
||||
The official bitcoin application for [Umbrel OS](https://github.com/getumbrel/umbrel-os). It wraps [Bitcoin Core](https://github.com/bitcoin/bitcoin)'s RPC exposes it via a RESTful API.
|
||||
|
||||
Umbrel OS's [web dashboard](https://github.com/getumbrel/umbrel-dashboard) uses middleware to interact with both Bitcoin and Lightning Network.
|
||||
A variety of applications that run on Umbrel use bitcoin to interact with the bitcoin network.
|
||||
|
||||
## 🚀 Getting started
|
||||
|
||||
If you are looking to run Umbrel on your hardware, you do not need to run this service on it's own. Just download [Umbrel OS](https://github.com/getumbrel/umbrel-os/releases) and you're good to go.
|
||||
This application can be installed with one click via Umbrel's app store.
|
||||
|
||||
## 🛠 Running middleware
|
||||
|
||||
Make sure a [`bitcoind`](https://github.com/bitcoin/bitcoin) and [`lnd`](https://github.com/lightningnetwork/lnd) instance is running and available on the same machine.
|
||||
Make sure a [`bitcoind`](https://github.com/bitcoin/bitcoin) instance is running and available on the same machine.
|
||||
## 🛠 Running bitcoin
|
||||
|
||||
### Step 1. Install dependencies
|
||||
```sh
|
||||
@ -39,20 +38,19 @@ Set the following environment variables directly or by placing them in `.env` fi
|
||||
| `BITCOIN_HOST` | IP or domain where `bitcoind` RPC is listening | `127.0.0.1` |
|
||||
| `RPC_USER` | `bitcoind` RPC username | |
|
||||
| `RPC_PASSWORD` | `bitcoind` RPC password | |
|
||||
| `LND_HOST` | IP or domain where `lnd` RPC is listening | `127.0.0.1` |
|
||||
| `TLS_FILE` | Path to `lnd`'s TLS certificate | `/lnd/tls.cert` |
|
||||
| `LND_PORT` | Port where `lnd` RPC is listening | `10009` |
|
||||
| `LND_NETWORK` | The chain `bitcoind` is running on (mainnet, testnet, regtest, simnet) | `mainnet` |
|
||||
| `LND_WALLET_PASSWORD` | The password for the LND wallet which will be automatically unlocked on boot | ` ` |
|
||||
| `MACAROON_DIR` | Path to `lnd`'s macaroon directory | `/lnd/data/chain/bitcoin/mainnet/` |
|
||||
| `JWT_PUBLIC_KEY_FILE` | Path to the JWT public key created by [`umbrel-manager`](https://github.com/getumbrel/umbrel-manager) | `/jwt-public-key/jwt.pem` |
|
||||
|
||||
### Step 3. Run middleware
|
||||
### Step 3. build the web interface
|
||||
```sh
|
||||
yarn install:ui
|
||||
yarn build:ui
|
||||
```
|
||||
|
||||
### Step 4. Run bitcoin
|
||||
```sh
|
||||
yarn start
|
||||
```
|
||||
|
||||
You can browse through the available API endpoints [here](https://github.com/getumbrel/umbrel-middleware/tree/master/routes/v1).
|
||||
You can access the web interface by visiting `http://localhost:8080/`
|
||||
|
||||
---
|
||||
|
||||
@ -70,7 +68,7 @@ If you're looking for a bigger challenge, before opening a pull request please [
|
||||
|
||||
## 🙏 Acknowledgements
|
||||
|
||||
Umbrel Middleware is built upon the work done by [Casa](https://github.com/casa) on its open-source [API](https://github.com/Casa/Casa-Node-API).
|
||||
Umbrel's bitcoin app is built upon the work done by [Casa](https://github.com/casa) on its open-source [API](https://github.com/Casa/Casa-Node-API).
|
||||
|
||||
---
|
||||
|
||||
|
38
app.js
38
app.js
@ -6,54 +6,37 @@ const express = require('express');
|
||||
const path = require('path');
|
||||
const morgan = require('morgan');
|
||||
const bodyParser = require('body-parser');
|
||||
const passport = require('passport');
|
||||
const cors = require('cors');
|
||||
|
||||
const constants = require('utils/const.js');
|
||||
|
||||
// Keep requestCorrelationId middleware as the first middleware. Otherwise we risk losing logs.
|
||||
const requestCorrelationMiddleware = require('middlewares/requestCorrelationId.js'); // eslint-disable-line id-length
|
||||
const camelCaseReqMiddleware = require('middlewares/camelCaseRequest.js').camelCaseRequest;
|
||||
const corsOptions = require('middlewares/cors.js').corsOptions;
|
||||
const errorHandleMiddleware = require('middlewares/errorHandling.js');
|
||||
require('middlewares/auth.js');
|
||||
|
||||
const logger = require('utils/logger.js');
|
||||
|
||||
const bitcoind = require('routes/v1/bitcoind/info.js');
|
||||
const address = require('routes/v1/lnd/address.js');
|
||||
const channel = require('routes/v1/lnd/channel.js');
|
||||
const info = require('routes/v1/lnd/info.js');
|
||||
const lightning = require('routes/v1/lnd/lightning.js');
|
||||
const transaction = require('routes/v1/lnd/transaction.js');
|
||||
const util = require('routes/v1/lnd/util.js');
|
||||
const wallet = require('routes/v1/lnd/wallet.js');
|
||||
const pages = require('routes/v1/pages.js');
|
||||
const charts = require('routes/v1/bitcoind/charts.js');
|
||||
const system = require('routes/v1/bitcoind/system.js');
|
||||
const ping = require('routes/ping.js');
|
||||
const app = express();
|
||||
|
||||
// Handles CORS
|
||||
app.use(cors(corsOptions));
|
||||
app.use(cors());
|
||||
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
app.use(requestCorrelationMiddleware);
|
||||
app.use(camelCaseReqMiddleware);
|
||||
app.use(morgan(logger.morganConfiguration));
|
||||
|
||||
app.use('/', express.static('./ui/dist'));
|
||||
|
||||
app.use('/v1/bitcoind/info', bitcoind);
|
||||
app.use('/v1/lnd/address', address);
|
||||
app.use('/v1/lnd/channel', channel);
|
||||
app.use('/v1/lnd/info', info);
|
||||
app.use('/v1/lnd/lightning', lightning);
|
||||
app.use('/v1/lnd/transaction', transaction);
|
||||
app.use('/v1/lnd/wallet', wallet);
|
||||
app.use('/v1/lnd/util', util);
|
||||
app.use('/v1/pages', pages);
|
||||
app.use('/v1/bitcoind/info', charts);
|
||||
app.use('/v1/bitcoind/system', system);
|
||||
app.use('/ping', ping);
|
||||
|
||||
app.use(errorHandleMiddleware);
|
||||
@ -62,10 +45,3 @@ app.use((req, res) => {
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
|
||||
// LND Unlocker
|
||||
if (constants.LND_WALLET_PASSWORD) {
|
||||
const LndUnlocker = require('logic/lnd-unlocker');
|
||||
lndUnlocker = new LndUnlocker(constants.LND_WALLET_PASSWORD);
|
||||
lndUnlocker.start();
|
||||
}
|
||||
|
15
bitcoin-entrypoint.sh
Executable file
15
bitcoin-entrypoint.sh
Executable file
@ -0,0 +1,15 @@
|
||||
#!/opt/homebrew/bin/bash bash
|
||||
|
||||
CORE_PORT=8333
|
||||
CORE_RPCPORT=8332
|
||||
|
||||
arguments=""
|
||||
for env in "${!CORE_@}"
|
||||
do
|
||||
value=${!env}
|
||||
uppercase_flag=${env#CORE_}
|
||||
lowercase_flag=${uppercase_flag,,}
|
||||
arguments="${arguments} -${lowercase_flag,,}=${!env}"
|
||||
done
|
||||
echo $arguments
|
||||
# -port=8333 -rpcport=8332
|
35
docker-compose.prod.yml
Normal file
35
docker-compose.prod.yml
Normal file
@ -0,0 +1,35 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
bitcoind:
|
||||
image: lncm/bitcoind:v22.0@sha256:37a1adb29b3abc9f972f0d981f45e41e5fca2e22816a023faa9fdc0084aa4507
|
||||
volumes:
|
||||
- ${PWD}/bitcoin:/data/.bitcoin
|
||||
restart: on-failure
|
||||
stop_grace_period: 15m30s
|
||||
ports:
|
||||
- "18443:18443"
|
||||
# - "8333:8333" # "$BITCOIN_P2P_PORT:$BITCOIN_P2P_PORT"
|
||||
server:
|
||||
build: .
|
||||
depends_on: [bitcoind]
|
||||
command: ["npm", "start"]
|
||||
restart: on-failure
|
||||
ports:
|
||||
- "3005:3005"
|
||||
environment:
|
||||
PORT: "3005"
|
||||
BITCOIN_HOST: "bitcoind"
|
||||
RPC_PORT: $BITCOIN_RPC_PORT
|
||||
RPC_USER: $BITCOIN_RPC_USER
|
||||
RPC_PASSWORD: $BITCOIN_RPC_PASS
|
||||
BITCOIN_RPC_HIDDEN_SERVICE: "/var/lib/tor/bitcoin-rpc/hostname"
|
||||
BITCOIN_P2P_HIDDEN_SERVICE: "/var/lib/tor/bitcoin-p2p/hostname"
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: umbrel_main_network
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: "$NETWORK_IP/24"
|
28
docker-compose.yml
Normal file
28
docker-compose.yml
Normal file
@ -0,0 +1,28 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
bitcoind:
|
||||
image: lncm/bitcoind:v22.0@sha256:37a1adb29b3abc9f972f0d981f45e41e5fca2e22816a023faa9fdc0084aa4507
|
||||
command: -regtest -rpcbind=0.0.0.0 -rpcallowip=0.0.0.0/0 -rpcauth=umbrel:5071d8b3ba93e53e414446ff9f1b7d7b$$375e9731abd2cd2c2c44d2327ec19f4f2644256fdeaf4fc5229bf98b778aafec
|
||||
volumes:
|
||||
- ${PWD}/data/bitcoin:/data/.bitcoin
|
||||
restart: on-failure
|
||||
stop_grace_period: 15m30s
|
||||
ports:
|
||||
- "18443:18443" # regtest
|
||||
server:
|
||||
build: .
|
||||
depends_on: [bitcoind]
|
||||
command: ["npm", "start"]
|
||||
restart: on-failure
|
||||
ports:
|
||||
- "3005:3005"
|
||||
environment:
|
||||
PORT: "3005"
|
||||
BITCOIN_HOST: "bitcoind"
|
||||
RPC_PORT: "18443" # - regtest
|
||||
RPC_USER: "umbrel"
|
||||
RPC_PASSWORD: "moneyprintergobrrr"
|
||||
BITCOIN_RPC_HIDDEN_SERVICE: "somehiddenservice.onion"
|
||||
BITCOIN_P2P_HIDDEN_SERVICE: "anotherhiddenservice.onion"
|
||||
DEVICE_DOMAIN_NAME: "test.local"
|
@ -1,20 +0,0 @@
|
||||
const bashService = require('services/bash.js');
|
||||
|
||||
const LND_DATA_SOURCE_DIRECTORY = '/lnd/';
|
||||
const LND_BACKUP_DEST_DIRECTORY = '/lndBackup';
|
||||
const CHANNEL_BACKUP_FILE = process.env.CHANNEL_BACKUP_FILE || '/lnd/data/chain/bitcoin/' + process.env.LND_NETWORK + '/channel.backup'
|
||||
|
||||
async function lndBackup() {
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
await bashService.exec('rsync', ['-r', '--delete', LND_DATA_SOURCE_DIRECTORY, LND_BACKUP_DEST_DIRECTORY]);
|
||||
}
|
||||
|
||||
async function lndChannnelBackup() {
|
||||
return CHANNEL_BACKUP_FILE;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
lndBackup,
|
||||
lndChannnelBackup
|
||||
};
|
@ -4,7 +4,7 @@ const BitcoindError = require('models/errors.js').BitcoindError;
|
||||
async function getBlockCount() {
|
||||
const blockCount = await bitcoindService.getBlockCount();
|
||||
|
||||
return { blockCount: blockCount.result };
|
||||
return {blockCount: blockCount.result};
|
||||
}
|
||||
|
||||
async function getConnectionsCount() {
|
||||
@ -13,7 +13,7 @@ async function getConnectionsCount() {
|
||||
var outBoundConnections = 0;
|
||||
var inBoundConnections = 0;
|
||||
|
||||
peerInfo.result.forEach(function (peer) {
|
||||
peerInfo.result.forEach(function(peer) {
|
||||
if (peer.inbound === false) {
|
||||
outBoundConnections++;
|
||||
|
||||
@ -35,10 +35,10 @@ async function getStatus() {
|
||||
try {
|
||||
await bitcoindService.help();
|
||||
|
||||
return { operational: true };
|
||||
return {operational: true};
|
||||
} catch (error) {
|
||||
if (error instanceof BitcoindError) {
|
||||
return { operational: false };
|
||||
return {operational: false};
|
||||
}
|
||||
|
||||
throw error;
|
||||
@ -53,7 +53,7 @@ async function getMaxSyncHeader() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const maxPeer = peerInfo.reduce(function (prev, current) {
|
||||
const maxPeer = peerInfo.reduce(function(prev, current) {
|
||||
return prev.syncedHeaders > current.syncedHeaders ? prev : current;
|
||||
});
|
||||
|
||||
@ -70,7 +70,7 @@ async function getLocalSyncInfo() {
|
||||
var blockChainInfo = info.result;
|
||||
var chain = blockChainInfo.chain;
|
||||
var blockCount = blockChainInfo.blocks;
|
||||
var headerCount = blockChainInfo.headers;
|
||||
var headerCount = blockChainInfo.headers;
|
||||
var percent = blockChainInfo.verificationprogress;
|
||||
|
||||
return {
|
||||
@ -99,13 +99,14 @@ async function getVersion() {
|
||||
// Remove all non-digits or decimals.
|
||||
const version = unformattedVersion.replace(/[^\d.]/g, '');
|
||||
|
||||
return { version: version }; // eslint-disable-line object-shorthand
|
||||
return {version: version}; // eslint-disable-line object-shorthand
|
||||
}
|
||||
|
||||
async function getTransaction(txid) {
|
||||
const transactionObj = await bitcoindService.getTransaction(txid);
|
||||
|
||||
return {
|
||||
txid: txid,
|
||||
txid,
|
||||
timestamp: transactionObj.result.time,
|
||||
confirmations: transactionObj.result.confirmations,
|
||||
blockhash: transactionObj.result.blockhash,
|
||||
@ -113,7 +114,7 @@ async function getTransaction(txid) {
|
||||
input: transactionObj.result.vin.txid,
|
||||
utxo: transactionObj.result.vout,
|
||||
rawtx: transactionObj.result.hex
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function getNetworkInfo() {
|
||||
@ -124,6 +125,7 @@ async function getNetworkInfo() {
|
||||
|
||||
async function getBlock(hash) {
|
||||
const blockObj = await bitcoindService.getBlock(hash);
|
||||
|
||||
return {
|
||||
block: hash,
|
||||
confirmations: blockObj.result.confirmations,
|
||||
@ -133,52 +135,91 @@ async function getBlock(hash) {
|
||||
prevblock: blockObj.result.previousblockhash,
|
||||
nextblock: blockObj.result.nextblockhash,
|
||||
transactions: blockObj.result.tx
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const memoizedGetFormattedBlock = () => {
|
||||
const cache = {};
|
||||
|
||||
return async blockHeight => {
|
||||
// cache cleanup
|
||||
// 6 blocks/hr * 24 hrs/day * 7 days = 1008 blocks over 7 days
|
||||
// plus some wiggle room in case weird difficulty adjustment or period of faster blocks
|
||||
const CACHE_LIMIT = 1100;
|
||||
while(Object.keys(cache).length > CACHE_LIMIT) {
|
||||
const cacheItemToDelete = Object.keys(cache)[0];
|
||||
delete cache[cacheItemToDelete];
|
||||
}
|
||||
|
||||
if (blockHeight in cache) {
|
||||
return cache[blockHeight];
|
||||
} else {
|
||||
let blockHash;
|
||||
try {
|
||||
({result: blockHash} = await bitcoindService.getBlockHash(blockHeight));
|
||||
} catch (error) {
|
||||
if (error instanceof BitcoindError) {
|
||||
return error;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const {result: block} = await bitcoindService.getBlock(blockHash);
|
||||
|
||||
cache[blockHeight] = {
|
||||
hash: block.hash,
|
||||
height: block.height,
|
||||
numTransactions: block.tx.length,
|
||||
confirmations: block.confirmations,
|
||||
time: block.time,
|
||||
size: block.size,
|
||||
previousblockhash: block.previousblockhash
|
||||
};
|
||||
|
||||
return cache[blockHeight];
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const initializedMemoizedGetFormattedBlock = memoizedGetFormattedBlock();
|
||||
|
||||
|
||||
async function getBlockRangeTransactionChunks(fromHeight, toHeight, blocksPerChunk) {
|
||||
const {blocks} = await getBlocks(fromHeight, toHeight);
|
||||
const chunks = [];
|
||||
blocks.forEach((block, index) => {
|
||||
const chunkIndex = Math.floor(index / blocksPerChunk);
|
||||
if (!chunks[chunkIndex]) {
|
||||
chunks[chunkIndex] = {
|
||||
time: block.time,
|
||||
numTransactions: 0,
|
||||
};
|
||||
}
|
||||
chunks[chunkIndex].numTransactions += block.numTransactions;
|
||||
});
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
async function getBlocks(fromHeight, toHeight) {
|
||||
|
||||
|
||||
let startingBlockHashRaw;
|
||||
|
||||
try {
|
||||
startingBlockHashRaw = await bitcoindService.getBlockHash(toHeight);
|
||||
} catch (error) {
|
||||
if (error instanceof BitcoindError) {
|
||||
return error;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
let currentHash = startingBlockHashRaw.result;
|
||||
|
||||
const blocks = [];
|
||||
|
||||
//loop from 'to height' till 'from Height'
|
||||
// loop from 'to height' till 'from Height'
|
||||
for (let currentHeight = toHeight; currentHeight >= fromHeight; currentHeight--) {
|
||||
|
||||
const blockRaw = await bitcoindService.getBlock(currentHash);
|
||||
const block = blockRaw.result;
|
||||
|
||||
const formattedBlock = {
|
||||
hash: block.hash,
|
||||
height: block.height,
|
||||
numTransactions: block.tx.length,
|
||||
confirmations: block.confirmations,
|
||||
time: block.time,
|
||||
size: block.size
|
||||
};
|
||||
|
||||
blocks.push(formattedBlock);
|
||||
|
||||
currentHash = block.previousblockhash;
|
||||
//terminate loop if we reach the genesis block
|
||||
if (!currentHash) {
|
||||
// terminate loop if we reach the genesis block
|
||||
if (currentHeight === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const formattedBlock = await initializedMemoizedGetFormattedBlock(currentHeight);
|
||||
blocks.push(formattedBlock);
|
||||
} catch(e) {
|
||||
console.error('Error memoizing formatted blocks')
|
||||
}
|
||||
}
|
||||
|
||||
return { blocks: blocks };
|
||||
return {blocks};
|
||||
}
|
||||
|
||||
async function getBlockHash(height) {
|
||||
@ -186,7 +227,7 @@ async function getBlockHash(height) {
|
||||
|
||||
return {
|
||||
hash: getBlockHashObj.result
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function nodeStatusDump() {
|
||||
@ -200,7 +241,7 @@ async function nodeStatusDump() {
|
||||
network_info: networkInfo.result,
|
||||
mempool: mempoolInfo.result,
|
||||
mining_info: miningInfo.result
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function nodeStatusSummary() {
|
||||
@ -215,7 +256,7 @@ async function nodeStatusSummary() {
|
||||
mempool: mempoolInfo.result.bytes,
|
||||
connections: networkInfo.result.connections,
|
||||
networkhashps: miningInfo.result.networkhashps
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@ -224,6 +265,7 @@ module.exports = {
|
||||
getBlock,
|
||||
getBlockCount,
|
||||
getBlocks,
|
||||
getBlockRangeTransactionChunks,
|
||||
getConnectionsCount,
|
||||
getNetworkInfo,
|
||||
getMempoolInfo,
|
||||
|
@ -1,16 +0,0 @@
|
||||
const constants = require('utils/const.js');
|
||||
const diskService = require('services/disk');
|
||||
|
||||
function readManagedChannelsFile() {
|
||||
return diskService.readJsonFile(constants.MANAGED_CHANNELS_FILE)
|
||||
.catch(() => Promise.resolve({}));
|
||||
}
|
||||
|
||||
function writeManagedChannelsFile(data) {
|
||||
return diskService.writeJsonFile(constants.MANAGED_CHANNELS_FILE, data);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
readManagedChannelsFile,
|
||||
writeManagedChannelsFile,
|
||||
};
|
@ -1,865 +0,0 @@
|
||||
/**
|
||||
* All Lightning business logic.
|
||||
*/
|
||||
|
||||
/* eslint-disable id-length, max-lines, max-statements */
|
||||
|
||||
const LndError = require('models/errors.js').LndError;
|
||||
const NodeError = require('models/errors.js').NodeError;
|
||||
|
||||
const lndService = require('services/lnd.js');
|
||||
const diskLogic = require('logic/disk');
|
||||
const bitcoindLogic = require('logic/bitcoind.js');
|
||||
|
||||
const constants = require('utils/const.js');
|
||||
const convert = require('utils/convert.js');
|
||||
|
||||
const UNIMPLEMENTED_CODE = 12;
|
||||
|
||||
const PENDING_OPEN_CHANNELS = 'pendingOpenChannels';
|
||||
const PENDING_CLOSING_CHANNELS = 'pendingClosingChannels';
|
||||
const PENDING_FORCE_CLOSING_CHANNELS = 'pendingForceClosingChannels';
|
||||
const WAITING_CLOSE_CHANNELS = 'waitingCloseChannels';
|
||||
const PENDING_CHANNEL_TYPES = [PENDING_OPEN_CHANNELS, PENDING_CLOSING_CHANNELS, PENDING_FORCE_CLOSING_CHANNELS,
|
||||
WAITING_CLOSE_CHANNELS];
|
||||
|
||||
const MAINNET_GENESIS_BLOCK_TIMESTAMP = 1231035305;
|
||||
const TESTNET_GENESIS_BLOCK_TIMESTAMP = 1296717402;
|
||||
|
||||
const FAST_BLOCK_CONF_TARGET = 1;
|
||||
const NORMAL_BLOCK_CONF_TARGET = 6;
|
||||
const SLOW_BLOCK_CONF_TARGET = 24;
|
||||
const CHEAPEST_BLOCK_CONF_TARGET = 144;
|
||||
|
||||
const OPEN_CHANNEL_EXTRA_WEIGHT = 10;
|
||||
|
||||
const FEE_RATE_TOO_LOW_ERROR = {
|
||||
code: 'FEE_RATE_TOO_LOW',
|
||||
text: 'Mempool reject low fee transaction. Increase fee rate.',
|
||||
};
|
||||
|
||||
const INSUFFICIENT_FUNDS_ERROR = {
|
||||
code: 'INSUFFICIENT_FUNDS',
|
||||
text: 'Lower amount or increase confirmation target.'
|
||||
};
|
||||
|
||||
const INVALID_ADDRESS = {
|
||||
code: 'INVALID_ADDRESS',
|
||||
text: 'Please validate the Bitcoin address is correct.'
|
||||
};
|
||||
|
||||
const OUTPUT_IS_DUST_ERROR = {
|
||||
code: 'OUTPUT_IS_DUST',
|
||||
text: 'Transaction output is dust.'
|
||||
};
|
||||
|
||||
// Converts a byte object into a hex string.
|
||||
function toHexString(byteObject) {
|
||||
const bytes = Object.values(byteObject);
|
||||
|
||||
return bytes.map(function (byte) {
|
||||
|
||||
return ('00' + (byte & 0xFF).toString(16)).slice(-2); // eslint-disable-line no-magic-numbers
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Creates a new invoice; more commonly known as a payment request.
|
||||
async function addInvoice(amt, memo) {
|
||||
const invoice = await lndService.addInvoice(amt, memo);
|
||||
invoice.rHashStr = toHexString(invoice.rHash);
|
||||
|
||||
return invoice;
|
||||
}
|
||||
|
||||
// Creates a new managed channel.
|
||||
// async function addManagedChannel(channelPoint, name, purpose) {
|
||||
// const managedChannels = await getManagedChannels();
|
||||
|
||||
// // Create a new managed channel. If one exists, it will be rewritten.
|
||||
// // However, Lnd should guarantee chanId is always unique.
|
||||
// managedChannels[channelPoint] = {
|
||||
// name: name, // eslint-disable-line object-shorthand
|
||||
// purpose: purpose, // eslint-disable-line object-shorthand
|
||||
// };
|
||||
|
||||
// await setManagedChannels(managedChannels);
|
||||
// }
|
||||
|
||||
// Change your lnd password. Wallet must exist and be unlocked.
|
||||
async function changePassword(currentPassword, newPassword) {
|
||||
return await lndService.changePassword(currentPassword, newPassword);
|
||||
}
|
||||
|
||||
// Closes the channel that corresponds to the given channelPoint. Force close is optional.
|
||||
async function closeChannel(txHash, index, force) {
|
||||
return await lndService.closeChannel(txHash, index, force);
|
||||
}
|
||||
|
||||
// Decode the payment request into useful information.
|
||||
function decodePaymentRequest(paymentRequest) {
|
||||
return lndService.decodePaymentRequest(paymentRequest);
|
||||
}
|
||||
|
||||
// Estimate the cost of opening a channel. We do this by repurposing the existing estimateFee grpc route from lnd. We
|
||||
// generate our own unused address and then feed that into the existing call. Then we add an extra 10 sats per
|
||||
// feerateSatPerByte. This is because the actual cost is slightly more than the default one output estimate.
|
||||
async function estimateChannelOpenFee(amt, confTarget, sweep) {
|
||||
const address = (await generateAddress()).address;
|
||||
const baseFeeEstimate = await estimateFee(address, amt, confTarget, sweep);
|
||||
|
||||
if (confTarget === 0) {
|
||||
const keys = Object.keys(baseFeeEstimate);
|
||||
|
||||
for (const key of keys) {
|
||||
|
||||
if (baseFeeEstimate[key].feeSat) {
|
||||
baseFeeEstimate[key].feeSat = String(parseInt(baseFeeEstimate[key].feeSat, 10) + OPEN_CHANNEL_EXTRA_WEIGHT
|
||||
* baseFeeEstimate[key].feerateSatPerByte);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else if (baseFeeEstimate.feeSat) {
|
||||
baseFeeEstimate.feeSat = String(parseInt(baseFeeEstimate.feeSat, 10) + OPEN_CHANNEL_EXTRA_WEIGHT
|
||||
* baseFeeEstimate.feerateSatPerByte);
|
||||
}
|
||||
|
||||
return baseFeeEstimate;
|
||||
}
|
||||
|
||||
// Estimate an on chain transaction fee.
|
||||
async function estimateFee(address, amt, confTarget, sweep) {
|
||||
const mempoolInfo = (await bitcoindLogic.getMempoolInfo()).result;
|
||||
|
||||
if (sweep) {
|
||||
|
||||
const balance = parseInt((await lndService.getWalletBalance()).confirmedBalance, 10);
|
||||
const amtToEstimate = balance;
|
||||
|
||||
if (confTarget === 0) {
|
||||
return await estimateFeeGroupSweep(address, amtToEstimate, mempoolInfo.mempoolminfee);
|
||||
}
|
||||
|
||||
return await estimateFeeSweep(address, amtToEstimate, mempoolInfo.mempoolminfee, confTarget, 0, amtToEstimate);
|
||||
} else {
|
||||
|
||||
try {
|
||||
if (confTarget === 0) {
|
||||
return await estimateFeeGroup(address, amt, mempoolInfo.mempoolminfee);
|
||||
}
|
||||
|
||||
return await estimateFeeWrapper(address, amt, mempoolInfo.mempoolminfee, confTarget);
|
||||
} catch (error) {
|
||||
return handleEstimateFeeError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use binary search strategy to determine the largest amount that can be sent.
|
||||
async function estimateFeeSweep(address, fullAmtToEstimate, mempoolMinFee, confTarget, l, r) {
|
||||
|
||||
const amtToEstimate = l + Math.floor((r - l) / 2); // eslint-disable-line no-magic-numbers
|
||||
|
||||
try {
|
||||
const successfulEstimate = await lndService.estimateFee(address, amtToEstimate, confTarget);
|
||||
|
||||
// Return after we have completed our search.
|
||||
if (l === amtToEstimate) {
|
||||
successfulEstimate.sweepAmount = amtToEstimate;
|
||||
|
||||
const estimatedFeeSatPerKiloByte = successfulEstimate.feerateSatPerByte * 1000;
|
||||
|
||||
if (estimatedFeeSatPerKiloByte < convert(mempoolMinFee, 'btc', 'sat', 'Number')) {
|
||||
throw new NodeError('FEE_RATE_TOO_LOW');
|
||||
}
|
||||
|
||||
return successfulEstimate;
|
||||
}
|
||||
|
||||
return await estimateFeeSweep(address, fullAmtToEstimate, mempoolMinFee, confTarget, amtToEstimate, r);
|
||||
|
||||
} catch (error) {
|
||||
|
||||
// Return after we have completed our search.
|
||||
if (l === amtToEstimate) {
|
||||
return handleEstimateFeeError(error);
|
||||
}
|
||||
|
||||
return await estimateFeeSweep(address, fullAmtToEstimate, mempoolMinFee, confTarget, l, amtToEstimate);
|
||||
}
|
||||
}
|
||||
|
||||
async function estimateFeeGroupSweep(address, amt, mempoolMinFee) {
|
||||
const calls = [estimateFeeSweep(address, amt, mempoolMinFee, FAST_BLOCK_CONF_TARGET, 0, amt),
|
||||
estimateFeeSweep(address, amt, mempoolMinFee, NORMAL_BLOCK_CONF_TARGET, 0, amt),
|
||||
estimateFeeSweep(address, amt, mempoolMinFee, SLOW_BLOCK_CONF_TARGET, 0, amt),
|
||||
estimateFeeSweep(address, amt, mempoolMinFee, CHEAPEST_BLOCK_CONF_TARGET, 0, amt),
|
||||
];
|
||||
|
||||
const [fast, normal, slow, cheapest]
|
||||
= await Promise.all(calls.map(p => p.catch(error => handleEstimateFeeError(error))));
|
||||
|
||||
return {
|
||||
fast: fast, // eslint-disable-line object-shorthand
|
||||
normal: normal, // eslint-disable-line object-shorthand
|
||||
slow: slow, // eslint-disable-line object-shorthand
|
||||
cheapest: cheapest, // eslint-disable-line object-shorthand
|
||||
};
|
||||
}
|
||||
|
||||
async function estimateFeeWrapper(address, amt, mempoolMinFee, confTarget) {
|
||||
const estimate = await lndService.estimateFee(address, amt, confTarget);
|
||||
|
||||
const estimatedFeeSatPerKiloByte = estimate.feerateSatPerByte * 1000;
|
||||
|
||||
if (estimatedFeeSatPerKiloByte < convert(mempoolMinFee, 'btc', 'sat', 'Number')) {
|
||||
throw new NodeError('FEE_RATE_TOO_LOW');
|
||||
}
|
||||
|
||||
return estimate;
|
||||
}
|
||||
|
||||
async function estimateFeeGroup(address, amt, mempoolMinFee) {
|
||||
const calls = [estimateFeeWrapper(address, amt, mempoolMinFee, FAST_BLOCK_CONF_TARGET),
|
||||
estimateFeeWrapper(address, amt, mempoolMinFee, NORMAL_BLOCK_CONF_TARGET),
|
||||
estimateFeeWrapper(address, amt, mempoolMinFee, SLOW_BLOCK_CONF_TARGET),
|
||||
estimateFeeWrapper(address, amt, mempoolMinFee, CHEAPEST_BLOCK_CONF_TARGET),
|
||||
];
|
||||
|
||||
const [fast, normal, slow, cheapest]
|
||||
= await Promise.all(calls.map(p => p.catch(error => handleEstimateFeeError(error))));
|
||||
|
||||
return {
|
||||
fast: fast, // eslint-disable-line object-shorthand
|
||||
normal: normal, // eslint-disable-line object-shorthand
|
||||
slow: slow, // eslint-disable-line object-shorthand
|
||||
cheapest: cheapest, // eslint-disable-line object-shorthand
|
||||
};
|
||||
}
|
||||
|
||||
function handleEstimateFeeError(error) {
|
||||
|
||||
if (error.message === 'FEE_RATE_TOO_LOW') {
|
||||
return FEE_RATE_TOO_LOW_ERROR;
|
||||
} else if (error.error.details === 'transaction output is dust') {
|
||||
return OUTPUT_IS_DUST_ERROR;
|
||||
} else if (error.error.details === 'insufficient funds available to construct transaction') {
|
||||
return INSUFFICIENT_FUNDS_ERROR;
|
||||
}
|
||||
|
||||
return INVALID_ADDRESS;
|
||||
}
|
||||
|
||||
// Generates a new on chain segwit bitcoin address.
|
||||
async function generateAddress() {
|
||||
return await lndService.generateAddress();
|
||||
}
|
||||
|
||||
// Generates a new 24 word seed phrase.
|
||||
async function generateSeed() {
|
||||
|
||||
const lndStatus = await getStatus();
|
||||
|
||||
if (lndStatus.operational) {
|
||||
const response = await lndService.generateSeed();
|
||||
|
||||
return { seed: response.cipherSeedMnemonic };
|
||||
}
|
||||
|
||||
throw new LndError('Lnd is not operational, therefore a seed cannot be created.');
|
||||
}
|
||||
|
||||
// Returns the total funds in channels and the total pending funds in channels.
|
||||
function getChannelBalance() {
|
||||
return lndService.getChannelBalance();
|
||||
}
|
||||
|
||||
// Returns a count of all open channels.
|
||||
function getChannelCount() {
|
||||
return lndService.getOpenChannels()
|
||||
.then(response => ({ count: response.length }));
|
||||
}
|
||||
|
||||
function getChannelPolicy() {
|
||||
return lndService.getFeeReport()
|
||||
.then(feeReport => feeReport.channelFees);
|
||||
}
|
||||
|
||||
function getForwardingEvents(startTime, endTime, indexOffset) {
|
||||
return lndService.getForwardingEvents(startTime, endTime, indexOffset);
|
||||
}
|
||||
|
||||
// Returns a list of all invoices.
|
||||
async function getInvoices() {
|
||||
const invoices = await lndService.getInvoices();
|
||||
|
||||
const reversedInvoices = [];
|
||||
for (const invoice of invoices.invoices) {
|
||||
reversedInvoices.unshift(invoice);
|
||||
}
|
||||
|
||||
return reversedInvoices;
|
||||
}
|
||||
|
||||
// Return all managed channels. Managed channels are channels the user has manually created.
|
||||
// TODO: how to handle if file becomes corrupt? Suggest simply wiping the file. The channel will still exist.
|
||||
// function getManagedChannels() {
|
||||
// return diskLogic.readManagedChannelsFile();
|
||||
// }
|
||||
|
||||
// Returns a list of all on chain transactions.
|
||||
async function getOnChainTransactions() {
|
||||
const transactions = await lndService.getOnChainTransactions();
|
||||
const openChannels = await lndService.getOpenChannels();
|
||||
const closedChannels = await lndService.getClosedChannels();
|
||||
const pendingChannelRPC = await lndService.getPendingChannels();
|
||||
|
||||
const pendingOpeningChannelTransactions = [];
|
||||
for (const pendingChannel of pendingChannelRPC.pendingOpenChannels) {
|
||||
const pendingTransaction = pendingChannel.channel.channelPoint.split(':').shift();
|
||||
pendingOpeningChannelTransactions.push(pendingTransaction);
|
||||
}
|
||||
|
||||
const pendingClosingChannelTransactions = [];
|
||||
for (const pendingGroup of [
|
||||
pendingChannelRPC.pendingClosingChannels,
|
||||
pendingChannelRPC.pendingForceClosingChannels,
|
||||
pendingChannelRPC.waitingCloseChannels]) {
|
||||
|
||||
if (pendingGroup.length === 0) {
|
||||
continue;
|
||||
}
|
||||
for (const pendingChannel of pendingGroup) {
|
||||
pendingClosingChannelTransactions.push(pendingChannel.closingTxid);
|
||||
}
|
||||
}
|
||||
|
||||
const openChannelTransactions = [];
|
||||
for (const channel of openChannels) {
|
||||
const openTransaction = channel.channelPoint.split(':').shift();
|
||||
openChannelTransactions.push(openTransaction);
|
||||
}
|
||||
|
||||
const closedChannelTransactions = [];
|
||||
for (const channel of closedChannels) {
|
||||
const closedTransaction = channel.closingTxHash.split(':').shift();
|
||||
closedChannelTransactions.push(closedTransaction);
|
||||
|
||||
const openTransaction = channel.channelPoint.split(':').shift();
|
||||
openChannelTransactions.push(openTransaction);
|
||||
}
|
||||
|
||||
const reversedTransactions = [];
|
||||
for (const transaction of transactions) {
|
||||
const txHash = transaction.txHash;
|
||||
|
||||
if (openChannelTransactions.includes(txHash)) {
|
||||
transaction.type = 'CHANNEL_OPEN';
|
||||
} else if (closedChannelTransactions.includes(txHash)) {
|
||||
transaction.type = 'CHANNEL_CLOSE';
|
||||
} else if (pendingOpeningChannelTransactions.includes(txHash)) {
|
||||
transaction.type = 'PENDING_OPEN';
|
||||
} else if (pendingClosingChannelTransactions.includes(txHash)) {
|
||||
transaction.type = 'PENDING_CLOSE';
|
||||
} else if (transaction.amount < 0) {
|
||||
transaction.type = 'ON_CHAIN_TRANSACTION_SENT';
|
||||
} else if (transaction.amount > 0 && transaction.destAddresses.length > 0) {
|
||||
transaction.type = 'ON_CHAIN_TRANSACTION_RECEIVED';
|
||||
|
||||
// Positive amounts are either incoming transactions or a WaitingCloseChannel. There is no way to determine which
|
||||
// until the transaction has at least one confirmation. Then a WaitingCloseChannel will become a pending Closing
|
||||
// channel and will have an associated tx id.
|
||||
} else if (transaction.amount > 0 && transaction.destAddresses.length === 0) {
|
||||
transaction.type = 'PENDING_CLOSE';
|
||||
} else {
|
||||
transaction.type = 'UNKNOWN';
|
||||
}
|
||||
|
||||
reversedTransactions.unshift(transaction);
|
||||
}
|
||||
|
||||
return reversedTransactions;
|
||||
}
|
||||
|
||||
function getTxnHashFromChannelPoint(channelPoint) {
|
||||
return channelPoint.split(':')[0];
|
||||
}
|
||||
|
||||
// Returns a list of all open channels.
|
||||
const getChannels = async () => {
|
||||
// const managedChannelsCall = getManagedChannels();
|
||||
const openChannelsCall = lndService.getOpenChannels();
|
||||
const pendingChannels = await lndService.getPendingChannels();
|
||||
|
||||
const allChannels = [];
|
||||
|
||||
// Combine all pending channel types
|
||||
for (const channel of pendingChannels.waitingCloseChannels) {
|
||||
channel.type = 'WAITING_CLOSING_CHANNEL';
|
||||
allChannels.push(channel);
|
||||
}
|
||||
|
||||
for (const channel of pendingChannels.pendingForceClosingChannels) {
|
||||
channel.type = 'FORCE_CLOSING_CHANNEL';
|
||||
allChannels.push(channel);
|
||||
}
|
||||
|
||||
for (const channel of pendingChannels.pendingClosingChannels) {
|
||||
channel.type = 'PENDING_CLOSING_CHANNEL';
|
||||
allChannels.push(channel);
|
||||
}
|
||||
|
||||
for (const channel of pendingChannels.pendingOpenChannels) {
|
||||
channel.type = 'PENDING_OPEN_CHANNEL';
|
||||
|
||||
// Make our best guess as to if this channel was created by us.
|
||||
if (channel.channel.remoteBalance === '0') {
|
||||
channel.initiator = true;
|
||||
} else {
|
||||
channel.initiator = false;
|
||||
}
|
||||
|
||||
// Include commitFee in balance. This helps us avoid the leaky sats issue by making balances more consistent.
|
||||
if (channel.initiator) {
|
||||
channel.channel.localBalance
|
||||
= String(parseInt(channel.channel.localBalance, 10) + parseInt(channel.commitFee, 10));
|
||||
} else {
|
||||
channel.channel.remoteBalance
|
||||
= String(parseInt(channel.channel.remoteBalance, 10) + parseInt(channel.commitFee, 10));
|
||||
}
|
||||
|
||||
allChannels.push(channel);
|
||||
}
|
||||
|
||||
// If we have any pending channels, we need to call get chain transactions to determine how many confirmations are
|
||||
// left for each pending channel. This gets the entire history of on chain transactions.
|
||||
// TODO: Once pagination is available, we should develop a different strategy.
|
||||
let chainTxnCall = null;
|
||||
let chainTxns = null;
|
||||
if (allChannels.length > 0) {
|
||||
chainTxnCall = lndService.getOnChainTransactions();
|
||||
}
|
||||
|
||||
// Combine open channels
|
||||
const openChannels = await openChannelsCall;
|
||||
|
||||
for (const channel of openChannels) {
|
||||
channel.type = 'OPEN';
|
||||
|
||||
// Include commitFee in balance. This helps us avoid the leaky sats issue by making balances more consistent.
|
||||
if (channel.initiator) {
|
||||
channel.localBalance
|
||||
= String(parseInt(channel.localBalance, 10) + parseInt(channel.commitFee, 10));
|
||||
} else {
|
||||
channel.remoteBalance
|
||||
= String(parseInt(channel.remoteBalance, 10) + parseInt(channel.commitFee, 10));
|
||||
}
|
||||
|
||||
allChannels.push(channel);
|
||||
}
|
||||
|
||||
// Add additional managed channel data if it exists
|
||||
// Call this async, because it reads from disk
|
||||
// const managedChannels = await managedChannelsCall;
|
||||
|
||||
if (chainTxnCall !== null) {
|
||||
const chainTxnList = await chainTxnCall;
|
||||
|
||||
// Convert list to object for efficient searching
|
||||
chainTxns = {};
|
||||
for (const txn of chainTxnList) {
|
||||
chainTxns[txn.txHash] = txn;
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate through all channels
|
||||
for (const channel of allChannels) {
|
||||
|
||||
// Pending channels have an inner channel object.
|
||||
if (channel.channel) {
|
||||
// Use remotePubkey for consistency with open channels
|
||||
channel.remotePubkey = channel.channel.remoteNodePub;
|
||||
channel.channelPoint = channel.channel.channelPoint;
|
||||
channel.capacity = channel.channel.capacity;
|
||||
channel.localBalance = channel.channel.localBalance;
|
||||
channel.remoteBalance = channel.channel.remoteBalance;
|
||||
|
||||
delete channel.channel;
|
||||
|
||||
// Determine the number of confirmation remaining for this channel
|
||||
|
||||
// We might have invalid channels that dne in the onChainTxList. Skip these channels
|
||||
const knownChannel = chainTxns[getTxnHashFromChannelPoint(channel.channelPoint)];
|
||||
if (!knownChannel) {
|
||||
channel.managed = false;
|
||||
channel.name = '';
|
||||
channel.purpose = '';
|
||||
|
||||
continue;
|
||||
}
|
||||
const numConfirmations = knownChannel.numConfirmations;
|
||||
|
||||
if (channel.type === 'FORCE_CLOSING_CHANNEL') {
|
||||
|
||||
// BlocksTilMaturity is provided by Lnd for forced closing channels once they have one confirmation
|
||||
channel.remainingConfirmations = channel.blocksTilMaturity;
|
||||
} else if (channel.type === 'PENDING_CLOSING_CHANNEL') {
|
||||
|
||||
// Lnd seams to be clearing these channels after just one confirmation and thus they never exist in this state.
|
||||
// Defaulting to 1 just in case.
|
||||
channel.remainingConfirmations = 1;
|
||||
} else if (channel.type === 'PENDING_OPEN_CHANNEL') {
|
||||
|
||||
channel.remainingConfirmations = constants.LN_REQUIRED_CONFIRMATIONS - numConfirmations;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Fetch remote node alias and set it
|
||||
const { alias } = await getNodeAlias(channel.remotePubkey);
|
||||
channel.remoteAlias = alias || "";
|
||||
|
||||
// If a managed channel exists, set the name and purpose
|
||||
// if (Object.prototype.hasOwnProperty.call(managedChannels, channel.channelPoint)) {
|
||||
// channel.managed = true;
|
||||
// channel.name = managedChannels[channel.channelPoint].name;
|
||||
// channel.purpose = managedChannels[channel.channelPoint].purpose;
|
||||
// } else {
|
||||
// channel.managed = false;
|
||||
// channel.name = '';
|
||||
// channel.purpose = '';
|
||||
// }
|
||||
}
|
||||
|
||||
return allChannels;
|
||||
};
|
||||
|
||||
// Returns a list of all outgoing payments.
|
||||
async function getPayments() {
|
||||
const payments = await lndService.getPayments();
|
||||
|
||||
const reversedPayments = [];
|
||||
for (const payment of payments.payments) {
|
||||
reversedPayments.unshift(payment);
|
||||
}
|
||||
|
||||
return reversedPayments;
|
||||
}
|
||||
|
||||
// Returns the full channel details of a pending channel.
|
||||
async function getPendingChannelDetails(channelType, pubKey) {
|
||||
const pendingChannels = await getPendingChannels();
|
||||
|
||||
// make sure correct type is used
|
||||
if (!PENDING_CHANNEL_TYPES.includes(channelType)) {
|
||||
throw Error('unknown pending channel type: ' + channelType);
|
||||
}
|
||||
|
||||
const typePendingChannel = pendingChannels[channelType];
|
||||
|
||||
for (let index = 0; index < typePendingChannel.length; index++) {
|
||||
const curChannel = typePendingChannel[index];
|
||||
if (curChannel.channel && curChannel.channel.remoteNodePub && curChannel.channel.remoteNodePub === pubKey) {
|
||||
return curChannel.channel;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Could not find a pending channel for pubKey: ' + pubKey);
|
||||
}
|
||||
|
||||
// Returns a list of all pending channels.
|
||||
function getPendingChannels() {
|
||||
return lndService.getPendingChannels();
|
||||
}
|
||||
|
||||
// Returns all associated public uris for this node.
|
||||
function getPublicUris() {
|
||||
return lndService.getInfo()
|
||||
.then(info => info.uris);
|
||||
}
|
||||
|
||||
function getGeneralInfo() {
|
||||
return lndService.getInfo();
|
||||
}
|
||||
|
||||
// Returns the status on lnd syncing to the current chain.
|
||||
// LND info returns "best_header_timestamp" from getInfo which is the timestamp of the latest Bitcoin block processed
|
||||
// by LND. Using known date of the genesis block to roughly calculate a percent processed.
|
||||
async function getSyncStatus() {
|
||||
const info = await lndService.getInfo();
|
||||
|
||||
let percentSynced = null;
|
||||
let processedBlocks = null;
|
||||
|
||||
if (!info.syncedToChain) {
|
||||
const genesisTimestamp = info.testnet ? TESTNET_GENESIS_BLOCK_TIMESTAMP : MAINNET_GENESIS_BLOCK_TIMESTAMP;
|
||||
|
||||
const currentTime = Math.floor(new Date().getTime() / 1000); // eslint-disable-line no-magic-numbers
|
||||
|
||||
percentSynced = ((info.bestHeaderTimestamp - genesisTimestamp) / (currentTime - genesisTimestamp))
|
||||
.toFixed(4); // eslint-disable-line no-magic-numbers
|
||||
|
||||
// let's not return a value over the 100% or when processedBlocks > blockHeight
|
||||
if (percentSynced < 1.0) {
|
||||
processedBlocks = Math.floor(percentSynced * info.blockHeight);
|
||||
} else {
|
||||
processedBlocks = info.blockHeight;
|
||||
percentSynced = (1).toFixed(4);
|
||||
}
|
||||
|
||||
} else {
|
||||
percentSynced = (1).toFixed(4); // eslint-disable-line no-magic-numbers
|
||||
processedBlocks = info.blockHeight;
|
||||
}
|
||||
|
||||
return {
|
||||
percent: percentSynced,
|
||||
knownBlockCount: info.blockHeight,
|
||||
processedBlocks: processedBlocks, // eslint-disable-line object-shorthand
|
||||
};
|
||||
}
|
||||
|
||||
// Returns the wallet balance and pending confirmation balance.
|
||||
function getWalletBalance() {
|
||||
return lndService.getWalletBalance();
|
||||
}
|
||||
|
||||
// Creates and initialized a Lightning wallet.
|
||||
async function initializeWallet(password, seed) {
|
||||
|
||||
const lndStatus = await getStatus();
|
||||
|
||||
if (lndStatus.operational) {
|
||||
|
||||
await lndService.initWallet({
|
||||
mnemonic: seed,
|
||||
password: password // eslint-disable-line object-shorthand
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw new LndError('Lnd is not operational, therefore a wallet cannot be created.');
|
||||
}
|
||||
|
||||
// Opens a channel to the node with the given public key with the given amount.
|
||||
async function openChannel(pubKey, ip, port, amt, satPerByte, name, purpose) { // eslint-disable-line max-params
|
||||
|
||||
var peers = await lndService.getPeers();
|
||||
|
||||
var existingPeer = false;
|
||||
|
||||
for (const peer of peers) {
|
||||
if (peer.pubKey === pubKey) {
|
||||
existingPeer = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!existingPeer) {
|
||||
await lndService.connectToPeer(pubKey, ip, port);
|
||||
}
|
||||
|
||||
// only returns a transactions id
|
||||
// TODO: Can we get the channel index from here? The channel point is transaction id:index. It could save us a call
|
||||
// to pendingChannelDetails.
|
||||
const channel = await lndService.openChannel(pubKey, amt, satPerByte);
|
||||
|
||||
// Lnd only allows one channel to be created with a node per block. By searching pending open channels, we can find
|
||||
// a unique identifier for the newly created channe. We will use ChannelPoint.
|
||||
const pendingChannel = await getPendingChannelDetails(PENDING_OPEN_CHANNELS, pubKey);
|
||||
|
||||
//No need for disk logic for now
|
||||
// await addManagedChannel(pendingChannel.channelPoint, name, purpose);
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
// Pays the given invoice.
|
||||
async function payInvoice(paymentRequest, amt) {
|
||||
const invoice = await decodePaymentRequest(paymentRequest);
|
||||
|
||||
if (invoice.numSatoshis !== '0' && amt) { // numSatoshis is returned from lnd as a string
|
||||
throw Error('Payment Request with non zero amount and amt value supplied.');
|
||||
}
|
||||
|
||||
if (invoice.numSatoshis === '0' && !amt) { // numSatoshis is returned from lnd as a string
|
||||
throw Error('Payment Request with zero amount requires an amt value supplied.');
|
||||
}
|
||||
|
||||
return await lndService.sendPaymentSync(paymentRequest, amt);
|
||||
}
|
||||
|
||||
// Removes a managed channel.
|
||||
// TODO: Figure out when an appropriate time to cleanup closed managed channel data. We need it during the closing
|
||||
// process to display to users.
|
||||
/*
|
||||
async function removeManagedChannel(fundingTxId, index) {
|
||||
const managedChannels = await getManagedChannels();
|
||||
|
||||
const channelPoint = fundingTxId + ':' + index;
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(managedChannels, channelPoint)) {
|
||||
delete managedChannels[channelPoint];
|
||||
}
|
||||
|
||||
return await setManagedChannels(managedChannels);
|
||||
}
|
||||
*/
|
||||
|
||||
// Send bitcoins on chain to the given address with the given amount. Sats per byte is optional.
|
||||
function sendCoins(addr, amt, satPerByte, sendAll) {
|
||||
|
||||
// Lnd requires we ignore amt if sendAll is true.
|
||||
if (sendAll) {
|
||||
return lndService.sendCoins(addr, undefined, satPerByte, sendAll);
|
||||
}
|
||||
|
||||
return lndService.sendCoins(addr, amt, satPerByte, sendAll);
|
||||
}
|
||||
|
||||
// Sets the managed channel data store.
|
||||
// TODO: How to prevent this from getting out of data with multiple calling threads?
|
||||
// perhaps create a mutex for reading and writing?
|
||||
// function setManagedChannels(managedChannelsObject) {
|
||||
// return diskLogic.writeManagedChannelsFile(managedChannelsObject);
|
||||
// }
|
||||
|
||||
// Returns if lnd is operation and if the wallet is unlocked.
|
||||
async function getStatus() {
|
||||
const bitcoindStatus = await bitcoindLogic.getStatus();
|
||||
|
||||
// lnd requires bitcoind to be operational.
|
||||
if (!bitcoindStatus.operational) {
|
||||
|
||||
return {
|
||||
operational: false,
|
||||
unlocked: false
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// The getInfo function requires that the wallet be unlocked in order to succeed. Lnd requires this for all
|
||||
// encrypted wallets.
|
||||
await lndService.getInfo();
|
||||
|
||||
return {
|
||||
operational: true,
|
||||
unlocked: true
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
// lnd might be active, but not possible to contact
|
||||
// using RPC if the wallet is encrypted or not yet created.
|
||||
if (error instanceof LndError) {
|
||||
const operationalErrors = [
|
||||
'wallet locked, unlock it to enable full RPC access',
|
||||
'wallet not created, create one to enable full RPC access',
|
||||
];
|
||||
|
||||
if (error.error && operationalErrors.includes(error.error.details)) {
|
||||
return {
|
||||
operational: true,
|
||||
unlocked: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
operational: false,
|
||||
unlocked: false
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Unlock and existing wallet.
|
||||
async function unlockWallet(password) {
|
||||
const lndStatus = await getStatus();
|
||||
|
||||
if (lndStatus.operational) {
|
||||
try {
|
||||
await lndService.unlockWallet(password);
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
// If it's a command for the UnlockerService (like
|
||||
// 'create' or 'unlock') but the wallet is already
|
||||
// unlocked, then these methods aren't recognized any
|
||||
// more because this service is shut down after
|
||||
// successful unlock. That's why the code
|
||||
// 'Unimplemented' means something different for these
|
||||
// two commands.
|
||||
if (error instanceof LndError) {
|
||||
|
||||
// wallet is already unlocked
|
||||
if (error.error && error.error.code === UNIMPLEMENTED_CODE) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
throw new LndError('Lnd is not operational, therefore the wallet cannot be unlocked.');
|
||||
}
|
||||
|
||||
async function getVersion() {
|
||||
const info = await lndService.getInfo();
|
||||
const unformattedVersion = info.version;
|
||||
|
||||
// Remove all beta/commit info. Fragile, LND may one day GA.
|
||||
const version = unformattedVersion.split('-', 1)[0];
|
||||
|
||||
return { version: version }; // eslint-disable-line object-shorthand
|
||||
}
|
||||
|
||||
async function getNodeAlias(pubkey) {
|
||||
const includeChannels = false;
|
||||
let nodeInfo;
|
||||
try {
|
||||
nodeInfo = await lndService.getNodeInfo(pubkey, includeChannels);
|
||||
} catch (error) {
|
||||
return { alias: "" };
|
||||
}
|
||||
return { alias: nodeInfo.node.alias }; // eslint-disable-line object-shorthand
|
||||
}
|
||||
|
||||
function updateChannelPolicy(global, fundingTxid, outputIndex, baseFeeMsat, feeRate, timeLockDelta) {
|
||||
return lndService.updateChannelPolicy(global, fundingTxid, outputIndex, baseFeeMsat, feeRate, timeLockDelta);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addInvoice,
|
||||
changePassword,
|
||||
closeChannel,
|
||||
decodePaymentRequest,
|
||||
estimateChannelOpenFee,
|
||||
estimateFee,
|
||||
generateAddress,
|
||||
generateSeed,
|
||||
getNodeAlias,
|
||||
getChannelBalance,
|
||||
getChannelPolicy,
|
||||
getChannelCount,
|
||||
getInvoices,
|
||||
getChannels,
|
||||
getForwardingEvents,
|
||||
getOnChainTransactions,
|
||||
getPayments,
|
||||
getPendingChannels,
|
||||
getPublicUris,
|
||||
getStatus,
|
||||
getSyncStatus,
|
||||
getWalletBalance,
|
||||
initializeWallet,
|
||||
openChannel,
|
||||
payInvoice,
|
||||
sendCoins,
|
||||
unlockWallet,
|
||||
getGeneralInfo,
|
||||
getVersion,
|
||||
updateChannelPolicy,
|
||||
};
|
@ -1,49 +0,0 @@
|
||||
const lightningLogic = require('logic/lightning');
|
||||
|
||||
const SECONDS = 1000;
|
||||
const MINUTES = 60 * SECONDS;
|
||||
|
||||
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
module.exports = class LndUnlocker {
|
||||
constructor(password) {
|
||||
this.password = password;
|
||||
this.running = false;
|
||||
this.unlocked = false;
|
||||
}
|
||||
|
||||
async unlock() {
|
||||
try {
|
||||
await lightningLogic.getGeneralInfo();
|
||||
if (!this.unlocked) {
|
||||
console.log('LndUnlocker: Wallet unlocked!');
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
try {
|
||||
await lightningLogic.unlockWallet(this.password);
|
||||
console.log('LndUnlocker: Wallet unlocked!');
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log('LndUnlocker: Wallet failed to unlock!');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async start() {
|
||||
if (this.running) {
|
||||
throw new Error('Already running');
|
||||
}
|
||||
this.running = true;
|
||||
while (this.running) {
|
||||
this.unlocked = await this.unlock();
|
||||
await delay(this.unlocked ? 1 * MINUTES : 10 * SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.running = false;
|
||||
this.unlocked = false;
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
const lightningLogic = require('logic/lightning.js');
|
||||
const networkLogic = require('logic/network.js');
|
||||
|
||||
async function lndDetails() {
|
||||
|
||||
const calls = [networkLogic.getBitcoindAddresses(),
|
||||
lightningLogic.getChannelBalance(),
|
||||
lightningLogic.getWalletBalance(),
|
||||
lightningLogic.getChannels(),
|
||||
lightningLogic.getGeneralInfo()
|
||||
];
|
||||
|
||||
// prevent fail fast, ux will expect a null on failed calls
|
||||
const [externalIP, channelBalance, walletBalance, channels, lightningInfo]
|
||||
= await Promise.all(calls.map(p => p.catch(err => null))); // eslint-disable-line
|
||||
|
||||
return {
|
||||
externalIP: externalIP, // eslint-disable-line object-shorthand
|
||||
balance: {
|
||||
wallet: walletBalance,
|
||||
channel: channelBalance,
|
||||
},
|
||||
channels: channels, // eslint-disable-line object-shorthand
|
||||
lightningInfo: lightningInfo // eslint-disable-line object-shorthand
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
lndDetails
|
||||
};
|
45
logic/system.js
Normal file
45
logic/system.js
Normal file
@ -0,0 +1,45 @@
|
||||
const constants = require('utils/const.js');
|
||||
const NodeError = require('models/errors.js').NodeError;
|
||||
|
||||
function getBitcoinP2PConnectionDetails() {
|
||||
const torAddress = constants.BITCOIN_P2P_HIDDEN_SERVICE;
|
||||
const port = constants.BITCOIN_P2P_PORT;
|
||||
const torConnectionString = `${torAddress}:${port}`;
|
||||
const localAddress = constants.DEVICE_DOMAIN_NAME;
|
||||
const localConnectionString = `${localAddress}:${port}`;
|
||||
|
||||
return {
|
||||
torAddress,
|
||||
port,
|
||||
torConnectionString,
|
||||
localAddress,
|
||||
localConnectionString
|
||||
};
|
||||
}
|
||||
|
||||
function getBitcoinRPCConnectionDetails() {
|
||||
const hiddenService = constants.BITCOIN_RPC_HIDDEN_SERVICE;
|
||||
const label = 'My Umbrel';
|
||||
const rpcuser = constants.BITCOIN_RPC_USER;
|
||||
const rpcpassword = constants.BITCOIN_RPC_PASSWORD;
|
||||
const torAddress = hiddenService;
|
||||
const port = constants.BITCOIN_RPC_PORT;
|
||||
const torConnectionString = `btcrpc://${rpcuser}:${rpcpassword}@${torAddress}:${port}?label=${encodeURIComponent(label)}`;
|
||||
const localAddress = constants.DEVICE_DOMAIN_NAME;
|
||||
const localConnectionString = `btcrpc://${rpcuser}:${rpcpassword}@${localAddress}:${port}?label=${encodeURIComponent(label)}`;
|
||||
|
||||
return {
|
||||
rpcuser,
|
||||
rpcpassword,
|
||||
torAddress,
|
||||
port,
|
||||
torConnectionString,
|
||||
localAddress,
|
||||
localConnectionString
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getBitcoinP2PConnectionDetails,
|
||||
getBitcoinRPCConnectionDetails,
|
||||
};
|
@ -1,53 +0,0 @@
|
||||
const passport = require('passport');
|
||||
const passportJWT = require('passport-jwt');
|
||||
const constants = require('utils/const.js');
|
||||
const NodeError = require('models/errors.js').NodeError;
|
||||
const diskService = require('services/disk.js');
|
||||
|
||||
var JwtStrategy = passportJWT.Strategy;
|
||||
var ExtractJwt = passportJWT.ExtractJwt;
|
||||
|
||||
const JWT_AUTH = 'jwt';
|
||||
|
||||
passport.serializeUser(function (user, done) {
|
||||
return done(null, user.id);
|
||||
});
|
||||
|
||||
async function createJwtOptions() {
|
||||
const pubKey = await diskService.readFile(constants.JWT_PUBLIC_KEY_FILE);
|
||||
return {
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('jwt'),
|
||||
secretOrKey: pubKey,
|
||||
algorithm: 'RS256'
|
||||
};
|
||||
}
|
||||
|
||||
createJwtOptions().then(function (data) {
|
||||
|
||||
const jwtOptions = data;
|
||||
|
||||
passport.use(JWT_AUTH, new JwtStrategy(jwtOptions, function (jwtPayload, done) {
|
||||
return done(null, { id: jwtPayload.id });
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
function jwt(req, res, next) {
|
||||
passport.authenticate(JWT_AUTH, { session: false }, function (error, user) {
|
||||
if (error || user === false) {
|
||||
return next(new NodeError('Invalid JWT', 401)); // eslint-disable-line no-magic-numbers
|
||||
}
|
||||
req.logIn(user, function (err) {
|
||||
if (err) {
|
||||
return next(new NodeError('Unable to authenticate', 401)); // eslint-disable-line no-magic-numbers
|
||||
}
|
||||
|
||||
return next(null, user);
|
||||
});
|
||||
})(req, res, next);
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
jwt,
|
||||
};
|
@ -1,21 +0,0 @@
|
||||
const corsOptions = {
|
||||
origin: (origin, callback) => {
|
||||
const whitelist = [
|
||||
'http://localhost:3000',
|
||||
'http://localhost:8080',
|
||||
'http://localhost',
|
||||
'http://umbrel.local',
|
||||
...process.env.DEVICE_HOSTS.split(",")
|
||||
];
|
||||
|
||||
if (whitelist.indexOf(origin) !== -1 || !origin) {
|
||||
return callback(null, true);
|
||||
} else {
|
||||
return callback(new Error('Not allowed by CORS'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
corsOptions,
|
||||
};
|
@ -1,6 +1,5 @@
|
||||
/* eslint-disable no-unused-vars, no-magic-numbers */
|
||||
const logger = require('utils/logger.js');
|
||||
const LndError = require('models/errors.js').LndError;
|
||||
|
||||
function handleError(error, req, res, next) {
|
||||
|
||||
@ -8,18 +7,6 @@ function handleError(error, req, res, next) {
|
||||
var route = req.url || '';
|
||||
var message = error.message || '';
|
||||
|
||||
if (error instanceof LndError) {
|
||||
if (error.error && error.error.code === 12) {
|
||||
statusCode = 403;
|
||||
message = 'Must unlock wallet';
|
||||
|
||||
// add additional details if available
|
||||
} else if (error.error && error.error.details) {
|
||||
// this may be too much information to return
|
||||
message += ', ' + error.error.details;
|
||||
}
|
||||
}
|
||||
|
||||
logger.error(message, route, error.stack);
|
||||
|
||||
res.status(statusCode).json(message);
|
||||
|
@ -16,15 +16,6 @@ function BitcoindError(message, error, statusCode) {
|
||||
}
|
||||
require('util').inherits(BitcoindError, Error);
|
||||
|
||||
function LndError(message, error, statusCode) {
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
this.name = this.constructor.name;
|
||||
this.message = message;
|
||||
this.error = error;
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
require('util').inherits(LndError, Error);
|
||||
|
||||
function ValidationError(message, statusCode) {
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
this.name = this.constructor.name;
|
||||
@ -36,7 +27,6 @@ require('util').inherits(ValidationError, Error);
|
||||
module.exports = {
|
||||
NodeError,
|
||||
BitcoindError,
|
||||
LndError,
|
||||
ValidationError
|
||||
};
|
||||
|
||||
|
@ -1,11 +1,14 @@
|
||||
{
|
||||
"name": "umbrel-middleware",
|
||||
"version": "0.1.15",
|
||||
"version": "0.2.1",
|
||||
"description": "Middleware for Umbrel Node",
|
||||
"author": "Umbrel",
|
||||
"scripts": {
|
||||
"lint": "eslint",
|
||||
"start": "node ./bin/www",
|
||||
"install:ui": "cd ui && yarn",
|
||||
"build:ui": "cd ui && yarn build",
|
||||
"serve:ui": "cd ui && yarn serve",
|
||||
"test": "mocha --file test.setup 'test/**/*.js'",
|
||||
"coverage": "nyc --all mocha --file test.setup 'test/**/*.js'",
|
||||
"postcoverage": "codecov"
|
||||
@ -21,14 +24,10 @@
|
||||
"debug": "^2.6.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.16.3",
|
||||
"grpc": "^1.8.0",
|
||||
"module-alias": "^2.1.0",
|
||||
"morgan": "^1.9.0",
|
||||
"passport": "^0.4.0",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"request-promise": "^4.2.2",
|
||||
"uuid": "^3.3.2",
|
||||
"validator": "^9.2.0",
|
||||
"winston": "^3.0.0-rc5",
|
||||
"winston-daily-rotate-file": "^3.1.3"
|
||||
},
|
||||
|
2600
resources/rpc.proto
2600
resources/rpc.proto
File diff suppressed because it is too large
Load Diff
75
routes/v1/bitcoind/charts.js
Normal file
75
routes/v1/bitcoind/charts.js
Normal file
@ -0,0 +1,75 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const bitcoind = require('logic/bitcoind.js');
|
||||
const bitcoindService = require('services/bitcoind.js');
|
||||
const safeHandler = require('utils/safeHandler');
|
||||
|
||||
const aggregates = {
|
||||
'1hr': [],
|
||||
'6hr': [],
|
||||
'12hr': [],
|
||||
'1d': [],
|
||||
'3d': [],
|
||||
'7d': [],
|
||||
};
|
||||
|
||||
const setAggregatesValues = async() => {
|
||||
const {result: blockchainInfo} = await bitcoindService.getBlockChainInfo();
|
||||
const syncPercent = blockchainInfo.verificationprogress;
|
||||
|
||||
// only start caching once sync is getting close to complete
|
||||
if (syncPercent > 0.98) {
|
||||
const currentBlock = blockchainInfo.blocks;
|
||||
|
||||
const ONE_HOUR_AS_BLOCKS = 6;
|
||||
const SIX_HOURS_AS_BLOCKS = 36;
|
||||
const TWELVE_HOURS_AS_BLOCKS = 72;
|
||||
const ONE_DAY_AS_BLOCKS = 144;
|
||||
const THREE_DAY_AS_BLOCKS = 432;
|
||||
const SEVEN_DAY_AS_BLOCKS = 1008;
|
||||
|
||||
const ranges = await Promise.all([
|
||||
bitcoind.getBlockRangeTransactionChunks(currentBlock - ONE_HOUR_AS_BLOCKS, currentBlock, 1), // 1hr
|
||||
bitcoind.getBlockRangeTransactionChunks(currentBlock - SIX_HOURS_AS_BLOCKS, currentBlock, 6), // 6hr
|
||||
bitcoind.getBlockRangeTransactionChunks(currentBlock - TWELVE_HOURS_AS_BLOCKS, currentBlock, 36), // 12hr
|
||||
bitcoind.getBlockRangeTransactionChunks(currentBlock - ONE_DAY_AS_BLOCKS, currentBlock, 72), // 1d
|
||||
bitcoind.getBlockRangeTransactionChunks(currentBlock - THREE_DAY_AS_BLOCKS, currentBlock, 144), // 3d
|
||||
bitcoind.getBlockRangeTransactionChunks(currentBlock - SEVEN_DAY_AS_BLOCKS, currentBlock, 432) // 7d
|
||||
]);
|
||||
|
||||
aggregates['1hr'] = ranges[0];
|
||||
aggregates['6hr'] = ranges[1];
|
||||
aggregates['12hr'] = ranges[2];
|
||||
aggregates['1d'] = ranges[3];
|
||||
aggregates['3d'] = ranges[4];
|
||||
aggregates['7d'] = ranges[5];
|
||||
|
||||
|
||||
return aggregates;
|
||||
}
|
||||
};
|
||||
|
||||
// Disable indexing as we don't show charts for now
|
||||
|
||||
// const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
// const MINUTE_AS_MILLISECONDS = 60000;
|
||||
|
||||
// (async () => {
|
||||
// while (true) {
|
||||
// console.log('Building transaction cache...');
|
||||
// try {
|
||||
// await setAggregatesValues();
|
||||
// } catch (error) {
|
||||
// console.log(`Failed to build transaction index: "${error.message}"`);
|
||||
// }
|
||||
// await delay(MINUTE_AS_MILLISECONDS);
|
||||
// }
|
||||
// })();
|
||||
|
||||
|
||||
router.get('/charts', safeHandler((req, res) => {
|
||||
res.json(aggregates);
|
||||
}
|
||||
));
|
||||
|
||||
module.exports = router;
|
@ -2,57 +2,54 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const networkLogic = require('logic/network.js');
|
||||
const bitcoind = require('logic/bitcoind.js');
|
||||
const auth = require('middlewares/auth.js');
|
||||
const safeHandler = require('utils/safeHandler');
|
||||
|
||||
router.get('/mempool', auth.jwt, safeHandler((req, res) =>
|
||||
router.get('/mempool', safeHandler((req, res) =>
|
||||
bitcoind.getMempoolInfo()
|
||||
.then(mempool => res.json(mempool.result))
|
||||
));
|
||||
|
||||
router.get('/addresses', auth.jwt, safeHandler((req, res) =>
|
||||
router.get('/addresses', safeHandler((req, res) =>
|
||||
networkLogic.getBitcoindAddresses()
|
||||
.then(addresses => res.json(addresses))
|
||||
));
|
||||
|
||||
router.get('/blockcount', auth.jwt, safeHandler((req, res) =>
|
||||
router.get('/blockcount', safeHandler((req, res) =>
|
||||
bitcoind.getBlockCount()
|
||||
.then(blockCount => res.json(blockCount))
|
||||
));
|
||||
|
||||
router.get('/connections', auth.jwt, safeHandler((req, res) =>
|
||||
router.get('/connections', safeHandler((req, res) =>
|
||||
bitcoind.getConnectionsCount()
|
||||
.then(connections => res.json(connections))
|
||||
));
|
||||
|
||||
//requires no authentication as it is used to fetch loading status
|
||||
//which could be fetched at login/signup page
|
||||
router.get('/status', safeHandler((req, res) =>
|
||||
bitcoind.getStatus()
|
||||
.then(status => res.json(status))
|
||||
));
|
||||
|
||||
router.get('/sync', auth.jwt, safeHandler((req, res) =>
|
||||
router.get('/sync', safeHandler((req, res) =>
|
||||
bitcoind.getSyncStatus()
|
||||
.then(status => res.json(status))
|
||||
));
|
||||
|
||||
router.get('/version', auth.jwt, safeHandler((req, res) =>
|
||||
router.get('/version', safeHandler((req, res) =>
|
||||
bitcoind.getVersion()
|
||||
.then(version => res.json(version))
|
||||
));
|
||||
|
||||
router.get('/statsDump', auth.jwt, safeHandler((req, res) =>
|
||||
router.get('/statsDump', safeHandler((req, res) =>
|
||||
bitcoind.nodeStatusDump()
|
||||
.then(statusdump => res.json(statusdump))
|
||||
));
|
||||
|
||||
router.get('/stats', auth.jwt, safeHandler((req, res) =>
|
||||
router.get('/stats', safeHandler((req, res) =>
|
||||
bitcoind.nodeStatusSummary()
|
||||
.then(statussumarry => res.json(statussumarry))
|
||||
));
|
||||
|
||||
router.get('/block', auth.jwt, safeHandler((req, res) => {
|
||||
router.get('/block', safeHandler((req, res) => {
|
||||
if (req.query.hash !== undefined && req.query.hash !== null) {
|
||||
bitcoind.getBlock(req.query.hash)
|
||||
.then(blockhash => res.json(blockhash))
|
||||
@ -64,20 +61,26 @@ router.get('/block', auth.jwt, safeHandler((req, res) => {
|
||||
));
|
||||
|
||||
// /v1/bitcoind/info/block/<hash>
|
||||
router.get('/block/:id', auth.jwt, safeHandler((req, res) =>
|
||||
router.get('/block/:id', safeHandler((req, res) =>
|
||||
bitcoind.getBlock(req.params.id)
|
||||
.then(blockhash => res.json(blockhash))
|
||||
));
|
||||
|
||||
router.get('/blocks', auth.jwt, safeHandler((req, res) => {
|
||||
router.get('/blocks', safeHandler((req, res) => {
|
||||
const fromHeight = parseInt(req.query.from);
|
||||
const toHeight = parseInt(req.query.to);
|
||||
|
||||
if (toHeight - fromHeight > 500) {
|
||||
res.status(500).json('Range query must be less than 500');
|
||||
return;
|
||||
}
|
||||
|
||||
bitcoind.getBlocks(fromHeight, toHeight)
|
||||
.then(blocks => res.json(blocks))
|
||||
}
|
||||
));
|
||||
|
||||
router.get('/txid/:id', auth.jwt, safeHandler((req, res) =>
|
||||
router.get('/txid/:id', safeHandler((req, res) =>
|
||||
bitcoind.getTransaction(req.params.id)
|
||||
.then(txhash => res.json(txhash))
|
||||
));
|
||||
|
19
routes/v1/bitcoind/system.js
Normal file
19
routes/v1/bitcoind/system.js
Normal file
@ -0,0 +1,19 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
const systemLogic = require('logic/system.js');
|
||||
const safeHandler = require('utils/safeHandler');
|
||||
|
||||
router.get('/bitcoin-p2p-connection-details', safeHandler(async(req, res) => {
|
||||
const connectionDetails = systemLogic.getBitcoinP2PConnectionDetails();
|
||||
|
||||
return res.json(connectionDetails);
|
||||
}));
|
||||
|
||||
router.get('/bitcoin-rpc-connection-details', safeHandler(async(req, res) => {
|
||||
const connectionDetails = systemLogic.getBitcoinRPCConnectionDetails();
|
||||
|
||||
return res.json(connectionDetails);
|
||||
}));
|
||||
|
||||
module.exports = router;
|
@ -1,12 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const lightningLogic = require('logic/lightning.js');
|
||||
const auth = require('middlewares/auth.js');
|
||||
const safeHandler = require('utils/safeHandler');
|
||||
|
||||
router.get('/', auth.jwt, safeHandler((req, res) =>
|
||||
lightningLogic.generateAddress()
|
||||
.then(address => res.json(address))
|
||||
));
|
||||
|
||||
module.exports = router;
|
@ -1,141 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const lightningLogic = require('logic/lightning.js');
|
||||
const auth = require('middlewares/auth.js');
|
||||
const ValidationError = require('models/errors.js').ValidationError;
|
||||
const safeHandler = require('utils/safeHandler');
|
||||
const validator = require('utils/validator.js');
|
||||
|
||||
const DEFAULT_TIME_LOCK_DELTA = 144; // eslint-disable-line no-magic-numbers
|
||||
|
||||
router.get('/', auth.jwt, safeHandler((req, res) =>
|
||||
lightningLogic.getChannels()
|
||||
.then(channels => res.json(channels))
|
||||
));
|
||||
|
||||
router.get('/estimateFee', auth.jwt, safeHandler(async (req, res, next) => {
|
||||
|
||||
const amt = req.query.amt; // Denominated in Satoshi
|
||||
const confTarget = req.query.confTarget;
|
||||
const sweep = req.query.sweep === 'true';
|
||||
|
||||
try {
|
||||
validator.isPositiveIntegerOrZero(confTarget);
|
||||
validator.isPositiveInteger(amt);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
return await lightningLogic.estimateChannelOpenFee(parseInt(amt, 10), parseInt(confTarget, 10), sweep)
|
||||
.then(response => res.json(response));
|
||||
}));
|
||||
|
||||
router.get('/pending', auth.jwt, safeHandler((req, res) =>
|
||||
lightningLogic.getPendingChannels()
|
||||
.then(channels => res.json(channels))
|
||||
));
|
||||
|
||||
router.get('/policy', auth.jwt, safeHandler((req, res) =>
|
||||
lightningLogic.getChannelPolicy()
|
||||
.then(policies => res.json(policies))
|
||||
));
|
||||
|
||||
router.put('/policy', auth.jwt, safeHandler((req, res, next) => {
|
||||
const global = req.body.global || false;
|
||||
const chanPoint = req.body.chanPoint;
|
||||
const baseFeeMsat = req.body.baseFeeMsat;
|
||||
const feeRate = req.body.feeRate;
|
||||
const timeLockDelta = req.body.timeLockDelta || DEFAULT_TIME_LOCK_DELTA;
|
||||
let fundingTxid;
|
||||
let outputIndex;
|
||||
|
||||
try {
|
||||
validator.isBoolean(global);
|
||||
|
||||
if (!global) {
|
||||
[fundingTxid, outputIndex] = chanPoint.split(':');
|
||||
|
||||
if (fundingTxid === undefined || outputIndex === undefined) {
|
||||
throw new ValidationError('Invalid channelPoint.');
|
||||
}
|
||||
|
||||
validator.isAlphanumeric(fundingTxid);
|
||||
validator.isPositiveIntegerOrZero(outputIndex);
|
||||
}
|
||||
|
||||
validator.isPositiveIntegerOrZero(baseFeeMsat);
|
||||
validator.isDecimal(feeRate + '');
|
||||
validator.isPositiveInteger(timeLockDelta);
|
||||
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
return lightningLogic.updateChannelPolicy(global, fundingTxid, parseInt(outputIndex, 10), baseFeeMsat, feeRate,
|
||||
timeLockDelta)
|
||||
.then(res.json());
|
||||
}));
|
||||
|
||||
router.delete('/close', auth.jwt, safeHandler((req, res, next) => {
|
||||
|
||||
const channelPoint = req.body.channelPoint;
|
||||
const force = req.body.force;
|
||||
|
||||
const parts = channelPoint.split(':');
|
||||
|
||||
if (parts.length !== 2) { // eslint-disable-line no-magic-numbers
|
||||
return next(new Error('Invalid channel point: ' + channelPoint));
|
||||
}
|
||||
|
||||
var fundingTxId;
|
||||
var index;
|
||||
|
||||
try {
|
||||
// TODO: fundingTxId, index
|
||||
fundingTxId = parts[0];
|
||||
index = parseInt(parts[1], 10);
|
||||
|
||||
validator.isBoolean(force);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
return lightningLogic.closeChannel(fundingTxId, index, force)
|
||||
.then(channel => res.json(channel));
|
||||
}));
|
||||
|
||||
router.get('/count', auth.jwt, safeHandler((req, res) =>
|
||||
lightningLogic.getChannelCount()
|
||||
.then(count => res.json(count))
|
||||
));
|
||||
|
||||
router.post('/open', auth.jwt, safeHandler((req, res, next) => {
|
||||
|
||||
const pubKey = req.body.pubKey;
|
||||
const ip = req.body.ip || '127.0.0.1';
|
||||
const port = req.body.port || 9735; // eslint-disable-line no-magic-numbers
|
||||
const amt = req.body.amt;
|
||||
const satPerByte = req.body.satPerByte;
|
||||
const name = req.body.name;
|
||||
const purpose = req.body.purpose;
|
||||
|
||||
try {
|
||||
|
||||
// TODO validate ip address as ip4 or ip6 address
|
||||
validator.isAlphanumeric(pubKey);
|
||||
validator.isPositiveInteger(port);
|
||||
validator.isPositiveInteger(amt);
|
||||
if (satPerByte) {
|
||||
validator.isPositiveInteger(satPerByte);
|
||||
}
|
||||
validator.isAlphanumericAndSpaces(name);
|
||||
validator.isAlphanumericAndSpaces(purpose);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
return lightningLogic.openChannel(pubKey, ip, port, amt, satPerByte, name, purpose)
|
||||
.then(channel => res.json(channel));
|
||||
}));
|
||||
|
||||
module.exports = router;
|
@ -1,45 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
const auth = require('middlewares/auth.js');
|
||||
const lightning = require('logic/lightning.js');
|
||||
const safeHandler = require('utils/safeHandler');
|
||||
const validator = require('utils/validator.js');
|
||||
|
||||
router.get('/uris', auth.jwt, safeHandler((req, res) =>
|
||||
lightning.getPublicUris()
|
||||
.then(uris => res.json(uris))
|
||||
));
|
||||
|
||||
//requires no authentication as it is used to fetch loading status
|
||||
//which could be fetched at login/signup page
|
||||
router.get('/status', safeHandler((req, res) =>
|
||||
lightning.getStatus()
|
||||
.then(status => res.json(status))
|
||||
));
|
||||
|
||||
router.get('/sync', auth.jwt, safeHandler((req, res) =>
|
||||
lightning.getSyncStatus()
|
||||
.then(status => res.json(status))
|
||||
));
|
||||
|
||||
router.get('/version', auth.jwt, safeHandler((req, res) =>
|
||||
lightning.getVersion()
|
||||
.then(version => res.json(version))
|
||||
));
|
||||
|
||||
router.get('/alias', auth.jwt, safeHandler((req, res, next) => {
|
||||
|
||||
const pubkey = req.query.pubkey;
|
||||
|
||||
try {
|
||||
validator.isAlphanumeric(pubkey);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
return lightning.getNodeAlias(pubkey)
|
||||
.then(alias => res.json(alias));
|
||||
}));
|
||||
|
||||
module.exports = router;
|
@ -1,92 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
const auth = require('middlewares/auth.js');
|
||||
const lightningLogic = require('logic/lightning.js');
|
||||
const validator = require('utils/validator.js');
|
||||
const safeHandler = require('utils/safeHandler');
|
||||
|
||||
router.post('/addInvoice', auth.jwt, safeHandler(async(req, res, next) => {
|
||||
|
||||
const amt = req.body.amt; // Denominated in Satoshi
|
||||
const memo = req.body.memo || '';
|
||||
|
||||
try {
|
||||
validator.isPositiveIntegerOrZero(amt);
|
||||
validator.isValidMemoLength(memo);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
return await lightningLogic.addInvoice(amt, memo)
|
||||
.then(invoice => res.json(invoice));
|
||||
}));
|
||||
|
||||
router.get('/forwardingEvents', auth.jwt, safeHandler((req, res, next) => {
|
||||
|
||||
const startTime = req.query.startTime;
|
||||
const endTime = req.query.endTime;
|
||||
const indexOffset = req.query.indexOffset;
|
||||
|
||||
try {
|
||||
if (startTime) {
|
||||
validator.isPositiveIntegerOrZero(startTime);
|
||||
}
|
||||
if (endTime) {
|
||||
validator.isPositiveIntegerOrZero(endTime);
|
||||
}
|
||||
if (indexOffset) {
|
||||
validator.isPositiveIntegerOrZero(indexOffset);
|
||||
}
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
return lightningLogic.getForwardingEvents(startTime, endTime, indexOffset)
|
||||
.then(events => res.json(events));
|
||||
}));
|
||||
|
||||
router.get('/invoice', auth.jwt, safeHandler((req, res, next) => {
|
||||
|
||||
const paymentRequest = req.query.paymentRequest;
|
||||
|
||||
try {
|
||||
validator.isAlphanumeric(paymentRequest);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
return lightningLogic.decodePaymentRequest(paymentRequest)
|
||||
.then(invoice => res.json(invoice));
|
||||
}));
|
||||
|
||||
router.get('/invoices', auth.jwt, safeHandler((req, res) =>
|
||||
lightningLogic.getInvoices()
|
||||
.then(invoices => res.json(invoices))
|
||||
));
|
||||
|
||||
router.post('/payInvoice', auth.jwt, safeHandler(async(req, res, next) => {
|
||||
|
||||
const paymentRequest = req.body.paymentRequest;
|
||||
const amt = req.body.amt;
|
||||
|
||||
try {
|
||||
validator.isAlphanumeric(paymentRequest);
|
||||
|
||||
if (amt) {
|
||||
validator.isPositiveIntegerOrZero(amt);
|
||||
}
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
return await lightningLogic.payInvoice(paymentRequest, amt)
|
||||
.then(invoice => res.json(invoice));
|
||||
}));
|
||||
|
||||
router.get('/payments', auth.jwt, safeHandler((req, res) =>
|
||||
lightningLogic.getPayments()
|
||||
.then(payments => res.json(payments))
|
||||
));
|
||||
|
||||
module.exports = router;
|
@ -1,57 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const validator = require('utils/validator.js');
|
||||
const lightningLogic = require('logic/lightning.js');
|
||||
const auth = require('middlewares/auth.js');
|
||||
const safeHandler = require('utils/safeHandler');
|
||||
|
||||
router.get('/', auth.jwt, safeHandler((req, res) =>
|
||||
lightningLogic.getOnChainTransactions()
|
||||
.then(transactions => res.json(transactions))
|
||||
));
|
||||
|
||||
router.post('/', auth.jwt, safeHandler((req, res, next) => {
|
||||
|
||||
const addr = req.body.addr;
|
||||
const amt = req.body.amt;
|
||||
const satPerByte = req.body.satPerByte;
|
||||
const sendAll = req.body.sendAll === true;
|
||||
|
||||
try {
|
||||
// TODO: addr
|
||||
validator.isPositiveInteger(amt);
|
||||
validator.isBoolean(sendAll);
|
||||
if (satPerByte) {
|
||||
validator.isPositiveInteger(satPerByte);
|
||||
}
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
return lightningLogic.sendCoins(addr, amt, satPerByte, sendAll)
|
||||
.then(transaction => res.json(transaction));
|
||||
}));
|
||||
|
||||
router.get('/estimateFee', auth.jwt, safeHandler(async(req, res, next) => {
|
||||
|
||||
const address = req.query.address;
|
||||
const amt = req.query.amt; // Denominated in Satoshi
|
||||
const confTarget = req.query.confTarget;
|
||||
const sweep = req.query.sweep === 'true';
|
||||
|
||||
try {
|
||||
validator.isAlphanumeric(address);
|
||||
validator.isPositiveIntegerOrZero(confTarget);
|
||||
|
||||
if (!sweep) {
|
||||
validator.isPositiveInteger(amt);
|
||||
}
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
return await lightningLogic.estimateFee(address, parseInt(amt, 10), parseInt(confTarget, 10), sweep)
|
||||
.then(response => res.json(response));
|
||||
}));
|
||||
|
||||
module.exports = router;
|
@ -1,19 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
const auth = require('middlewares/auth.js');
|
||||
const applicationLogic = require('logic/application.js');
|
||||
const safeHandler = require('utils/safeHandler');
|
||||
|
||||
router.post('/backup', auth.jwt, safeHandler((req, res) =>
|
||||
applicationLogic.lndBackup()
|
||||
.then(response => res.json(response))
|
||||
));
|
||||
|
||||
router.get('/download-channel-backup', auth.jwt, safeHandler((req, res) =>
|
||||
applicationLogic.lndChannnelBackup()
|
||||
.then(backupFile => res.download(backupFile, 'channel.backup'))
|
||||
));
|
||||
|
||||
|
||||
module.exports = router;
|
@ -1,86 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const lightningLogic = require('logic/lightning.js');
|
||||
const auth = require('middlewares/auth.js');
|
||||
const safeHandler = require('utils/safeHandler');
|
||||
const constants = require('utils/const.js');
|
||||
const logger = require('utils/logger.js');
|
||||
const validator = require('utils/validator.js');
|
||||
const LndError = require('models/errors.js').LndError;
|
||||
|
||||
router.get('/btc', auth.jwt, safeHandler((req, res) =>
|
||||
lightningLogic.getWalletBalance()
|
||||
.then(balance => res.json(balance))
|
||||
));
|
||||
|
||||
// API endpoint to change your lnd password.
|
||||
router.post('/changePassword', auth.jwt, safeHandler(async(req, res, next) => {
|
||||
|
||||
const currentPassword = req.body.currentPassword;
|
||||
const newPassword = req.body.newPassword;
|
||||
|
||||
try {
|
||||
validator.isString(currentPassword);
|
||||
validator.isMinPasswordLength(currentPassword);
|
||||
validator.isString(newPassword);
|
||||
validator.isMinPasswordLength(newPassword);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
|
||||
try {
|
||||
await lightningLogic.changePassword(currentPassword, newPassword);
|
||||
|
||||
return res.status(constants.STATUS_CODES.OK).json();
|
||||
} catch (error) {
|
||||
if (error instanceof LndError && error.message === 'Unable to change password') {
|
||||
|
||||
logger.info(error, 'changePassword');
|
||||
|
||||
// Invalid passphrase for master public key
|
||||
if (error.error.code === constants.LND_STATUS_CODES.UNKNOWN) {
|
||||
|
||||
return res.status(constants.STATUS_CODES.FORBIDDEN).json();
|
||||
|
||||
// Connect Failed (lnd is probably restarting)
|
||||
} else if (error.error.code === constants.LND_STATUS_CODES.UNAVAILABLE) {
|
||||
|
||||
return res.status(constants.STATUS_CODES.BAD_GATEWAY).json();
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
}));
|
||||
|
||||
// Should not include auth because the user isn't registered yet. Once the user initializes a wallet, that wallet is
|
||||
// locked and cannot be updated unless a full system reset is initiated.
|
||||
router.post('/init', safeHandler((req, res) => {
|
||||
|
||||
const password = req.body.password;
|
||||
const seed = req.body.seed;
|
||||
|
||||
if (seed.length !== 24) { // eslint-disable-line no-magic-numbers
|
||||
throw new Error('Invalid seed length');
|
||||
}
|
||||
|
||||
// TODO validate password requirements
|
||||
|
||||
return lightningLogic.initializeWallet(password, seed)
|
||||
.then(response => res.json(response));
|
||||
}));
|
||||
|
||||
router.get('/lightning', auth.jwt, safeHandler((req, res) =>
|
||||
lightningLogic.getChannelBalance()
|
||||
.then(balance => res.json(balance))
|
||||
));
|
||||
|
||||
// Should not include auth because the user isn't registered yet. The user can get a seed phrase as many times as they
|
||||
// would like until the wallet has been initialized.
|
||||
router.get('/seed', safeHandler((req, res) =>
|
||||
lightningLogic.generateSeed()
|
||||
.then(seed => res.json(seed))
|
||||
));
|
||||
|
||||
module.exports = router;
|
@ -1,12 +0,0 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const pagesLogic = require('logic/pages.js');
|
||||
const auth = require('middlewares/auth.js');
|
||||
const safeHandler = require('utils/safeHandler');
|
||||
|
||||
router.get('/lnd', auth.jwt, safeHandler((req, res) =>
|
||||
pagesLogic.lndDetails()
|
||||
.then(address => res.json(address))
|
||||
));
|
||||
|
||||
module.exports = router;
|
@ -1,68 +0,0 @@
|
||||
/**
|
||||
* Generic disk functions.
|
||||
*/
|
||||
|
||||
const logger = require('utils/logger');
|
||||
const fs = require('fs');
|
||||
const crypto = require('crypto');
|
||||
const uint32Bytes = 4;
|
||||
|
||||
// Reads a file. Wraps fs.readFile into a native promise
|
||||
function readFile(filePath, encoding) {
|
||||
return new Promise((resolve, reject) => fs.readFile(filePath, encoding, (err, str) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(str);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Reads a file as a utf8 string. Wraps fs.readFile into a native promise
|
||||
function readUtf8File(filePath) {
|
||||
return readFile(filePath, 'utf8');
|
||||
}
|
||||
|
||||
function readJsonFile(filePath) {
|
||||
return readUtf8File(filePath).then(JSON.parse);
|
||||
}
|
||||
|
||||
// Writes a string to a file. Wraps fs.writeFile into a native promise
|
||||
// This is _not_ concurrency safe, so don't export it without making it like writeJsonFile
|
||||
function writeFile(filePath, data, encoding) {
|
||||
return new Promise((resolve, reject) => fs.writeFile(filePath, data, encoding, err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function writeJsonFile(filePath, obj) {
|
||||
const tempFileName = `${filePath}.${crypto.randomBytes(uint32Bytes).readUInt32LE(0)}`;
|
||||
|
||||
return writeFile(tempFileName, JSON.stringify(obj), 'utf8')
|
||||
.then(() => new Promise((resolve, reject) => fs.rename(tempFileName, filePath, err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
})))
|
||||
.catch(err => {
|
||||
if (err) {
|
||||
fs.unlink(tempFileName, err => {
|
||||
logger.warn('Error removing temporary file after error', 'disk', {err, tempFileName});
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
readFile,
|
||||
readUtf8File,
|
||||
readJsonFile,
|
||||
writeJsonFile,
|
||||
};
|
457
services/lnd.js
457
services/lnd.js
@ -1,457 +0,0 @@
|
||||
/* eslint-disable camelcase, max-lines */
|
||||
const grpc = require('grpc');
|
||||
const camelizeKeys = require('camelize-keys');
|
||||
|
||||
const diskService = require('services/disk');
|
||||
const LndError = require('models/errors.js').LndError;
|
||||
|
||||
const LND_HOST = process.env.LND_HOST || '127.0.0.1';
|
||||
const TLS_FILE = process.env.TLS_FILE || '/lnd/tls.cert';
|
||||
const PROTO_FILE = process.env.PROTO_FILE || './resources/rpc.proto';
|
||||
const LND_PORT = process.env.LND_PORT || 10009; // eslint-disable-line no-magic-numbers
|
||||
const LND_NETWORK = process.env.LND_NETWORK || 'mainnet';
|
||||
|
||||
// LND changed the macaroon path to ~/.lnd/data/chain/{chain}/{network}/admin.macaroon. We are currently only
|
||||
// supporting bitcoind and have that hard coded. However, we are leaving the ability to switch between testnet and
|
||||
// mainnet. This can be done with the /reset route. LND_NETWORK will be defaulted in /usr/local/casa/applications/.env.
|
||||
// LND_NETWORK will be overwritten in the settings file.
|
||||
let MACAROON_FILE = '/lnd/data/chain/bitcoin/' + LND_NETWORK + '/admin.macaroon';
|
||||
|
||||
// Developers should overwrite MACAROON_DIR in their .env file or ide. We recommend 'os.homedir() + /lightning-node/'.
|
||||
if (process.env.MACAROON_DIR) {
|
||||
MACAROON_FILE = process.env.MACAROON_DIR + 'admin.macaroon';
|
||||
}
|
||||
|
||||
// TODO move this to volume
|
||||
const lnrpcDescriptor = grpc.load(PROTO_FILE);
|
||||
const lnrpc = lnrpcDescriptor.lnrpc;
|
||||
|
||||
const DEFAULT_RECOVERY_WINDOW = 250;
|
||||
|
||||
// Initialize RPC client will attempt to connect to the lnd rpc with a tls.cert and admin.macaroon. If the wallet has
|
||||
// not bee created yet, then the client will only be initialized with the tls.cert. There may be times when lnd wallet
|
||||
// is reset and the tls.cert and admin.macaroon will change.
|
||||
async function initializeRPCClient() {
|
||||
return diskService.readFile(TLS_FILE)
|
||||
.then(lndCert => {
|
||||
const sslCreds = grpc.credentials.createSsl(lndCert);
|
||||
|
||||
return diskService.readFile(MACAROON_FILE)
|
||||
.then(macaroon => {
|
||||
// build meta data credentials
|
||||
const metadata = new grpc.Metadata();
|
||||
metadata.add('macaroon', macaroon.toString('hex'));
|
||||
const macaroonCreds = grpc.credentials.createFromMetadataGenerator((_args, callback) => {
|
||||
callback(null, metadata);
|
||||
});
|
||||
|
||||
// combine the cert credentials and the macaroon auth credentials
|
||||
// such that every call is properly encrypted and authenticated
|
||||
return {
|
||||
credentials: grpc.credentials.combineChannelCredentials(sslCreds, macaroonCreds),
|
||||
state: true
|
||||
};
|
||||
})
|
||||
.catch(() => ({ credentials: sslCreds, state: 'WALLET_CREATION_ONLY' }));
|
||||
})
|
||||
.then(({ credentials, state }) => ({
|
||||
lightning: new lnrpc.Lightning(LND_HOST + ':' + LND_PORT, credentials),
|
||||
walletUnlocker: new lnrpc.WalletUnlocker(LND_HOST + ':' + LND_PORT, credentials),
|
||||
state: state // eslint-disable-line object-shorthand
|
||||
}));
|
||||
}
|
||||
|
||||
async function promiseify(rpcObj, rpcFn, payload, description) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
rpcFn.call(rpcObj, payload, (error, grpcResponse) => {
|
||||
if (error) {
|
||||
reject(new LndError(`Unable to ${description}`, error));
|
||||
} else {
|
||||
resolve(camelizeKeys(grpcResponse, '_'));
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// an amount, an options memo, and can only be paid to node that created it.
|
||||
async function addInvoice(amount, memo) {
|
||||
const rpcPayload = {
|
||||
value: amount,
|
||||
memo: memo, // eslint-disable-line object-shorthand
|
||||
expiry: 3600 // Should we make this ENV specific for ease of testing?
|
||||
};
|
||||
|
||||
const conn = await initializeRPCClient();
|
||||
|
||||
const grpcResponse = await promiseify(conn.lightning, conn.lightning.addInvoice, rpcPayload, 'create new invoice');
|
||||
|
||||
if (grpcResponse && grpcResponse.paymentRequest) {
|
||||
return {
|
||||
rHash: grpcResponse.rHash,
|
||||
paymentRequest: grpcResponse.paymentRequest,
|
||||
};
|
||||
} else {
|
||||
throw new LndError('Unable to parse invoice from lnd');
|
||||
}
|
||||
}
|
||||
|
||||
// Change your lnd password.
|
||||
async function changePassword(currentPassword, newPassword) {
|
||||
|
||||
const currentPasswordBuff = Buffer.from(currentPassword, 'utf8');
|
||||
const newPasswordBuff = Buffer.from(newPassword, 'utf8');
|
||||
|
||||
const rpcPayload = {
|
||||
current_password: currentPasswordBuff,
|
||||
new_password: newPasswordBuff,
|
||||
};
|
||||
|
||||
const conn = await initializeRPCClient();
|
||||
|
||||
return await promiseify(conn.walletUnlocker, conn.walletUnlocker.changePassword, rpcPayload, 'change password');
|
||||
}
|
||||
|
||||
function closeChannel(fundingTxId, index, force) {
|
||||
const rpcPayload = {
|
||||
channel_point: {
|
||||
funding_txid_str: fundingTxId,
|
||||
output_index: index
|
||||
},
|
||||
force: force // eslint-disable-line object-shorthand
|
||||
};
|
||||
|
||||
return initializeRPCClient().then(({ lightning }) => new Promise((resolve, reject) => {
|
||||
try {
|
||||
const call = lightning.CloseChannel(rpcPayload);
|
||||
|
||||
call.on('data', chan => {
|
||||
if (chan.update === 'close_pending') {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
call.on('error', error => {
|
||||
reject(new LndError('Unable to close channel', error));
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Connects this lnd node to a peer.
|
||||
function connectToPeer(pubKey, ip, port) {
|
||||
const rpcPayload = {
|
||||
addr: {
|
||||
pubkey: pubKey,
|
||||
host: ip + ':' + port
|
||||
}
|
||||
};
|
||||
|
||||
return initializeRPCClient()
|
||||
.then(({ lightning }) => promiseify(lightning, lightning.ConnectPeer, rpcPayload, 'connect to peer'));
|
||||
}
|
||||
|
||||
function decodePaymentRequest(paymentRequest) {
|
||||
const rpcPayload = {
|
||||
pay_req: paymentRequest
|
||||
};
|
||||
|
||||
return initializeRPCClient()
|
||||
.then(({ lightning }) => promiseify(lightning, lightning.decodePayReq, rpcPayload, 'decode payment request'))
|
||||
.then(invoice => {
|
||||
// add on payment request for extra details
|
||||
invoice.paymentRequest = paymentRequest;
|
||||
|
||||
return invoice;
|
||||
});
|
||||
}
|
||||
|
||||
async function estimateFee(address, amt, confTarget) {
|
||||
const addrToAmount = {};
|
||||
addrToAmount[address] = amt;
|
||||
|
||||
const rpcPayload = {
|
||||
AddrToAmount: addrToAmount,
|
||||
target_conf: confTarget,
|
||||
};
|
||||
|
||||
const conn = await initializeRPCClient();
|
||||
|
||||
return await promiseify(conn.lightning, conn.lightning.estimateFee, rpcPayload, 'estimate fee request');
|
||||
}
|
||||
|
||||
async function generateAddress() {
|
||||
const rpcPayload = {
|
||||
type: 0
|
||||
};
|
||||
|
||||
const conn = await initializeRPCClient();
|
||||
|
||||
return await promiseify(conn.lightning, conn.lightning.NewAddress, rpcPayload, 'generate address');
|
||||
}
|
||||
|
||||
function generateSeed() {
|
||||
return initializeRPCClient().then(({ walletUnlocker, state }) => {
|
||||
if (state === true) {
|
||||
throw new LndError('Macaroon exists, therefore wallet already exists');
|
||||
}
|
||||
|
||||
return promiseify(walletUnlocker, walletUnlocker.GenSeed, {}, 'generate seed');
|
||||
});
|
||||
}
|
||||
|
||||
function getChannelBalance() {
|
||||
return initializeRPCClient()
|
||||
.then(({ lightning }) => promiseify(lightning, lightning.ChannelBalance, {}, 'get channel balance'));
|
||||
}
|
||||
|
||||
function getFeeReport() {
|
||||
return initializeRPCClient()
|
||||
.then(({ lightning }) => promiseify(lightning, lightning.FeeReport, {}, 'get fee report'));
|
||||
}
|
||||
|
||||
function getForwardingEvents(startTime, endTime, indexOffset) {
|
||||
const rpcPayload = {
|
||||
start_time: startTime,
|
||||
end_time: endTime,
|
||||
index_offset: indexOffset,
|
||||
};
|
||||
|
||||
return initializeRPCClient()
|
||||
.then(({ lightning }) => promiseify(lightning, lightning.ForwardingHistory, rpcPayload, 'get forwarding events'));
|
||||
}
|
||||
|
||||
function getInfo() {
|
||||
return initializeRPCClient()
|
||||
.then(({ lightning }) => promiseify(lightning, lightning.GetInfo, {}, 'get lnd information'));
|
||||
}
|
||||
|
||||
function getNodeInfo(pubkey, includeChannels) {
|
||||
const rpcPayload = {
|
||||
pub_key: pubkey,
|
||||
include_channels: includeChannels
|
||||
};
|
||||
return initializeRPCClient()
|
||||
.then(({ lightning }) => promiseify(lightning, lightning.GetNodeInfo, rpcPayload, 'get node information'));
|
||||
}
|
||||
|
||||
// Returns a list of lnd's currently open channels. Channels are considered open by this node and it's directly
|
||||
// connected peer after three confirmation. After six confirmations, the channel is broadcasted by this node and it's
|
||||
// directly connected peer to the broader lightning network.
|
||||
function getOpenChannels() {
|
||||
return initializeRPCClient()
|
||||
.then(({ lightning }) => promiseify(lightning, lightning.ListChannels, {}, 'list channels'))
|
||||
.then(grpcResponse => grpcResponse.channels);
|
||||
}
|
||||
|
||||
function getClosedChannels() {
|
||||
return initializeRPCClient()
|
||||
.then(({ lightning }) => promiseify(lightning, lightning.ClosedChannels, {}, 'closed channels'))
|
||||
.then(grpcResponse => grpcResponse.channels);
|
||||
}
|
||||
|
||||
// Returns a list of all outgoing payments.
|
||||
function getPayments() {
|
||||
// Limit to 1k to prevent crazy response sizes
|
||||
// https://github.com/getumbrel/umbrel/issues/1245
|
||||
const rpcPayload = {
|
||||
max_payments: 1000,
|
||||
};
|
||||
return initializeRPCClient()
|
||||
.then(({ lightning }) => promiseify(lightning, lightning.ListPayments, rpcPayload, 'get payments'));
|
||||
}
|
||||
|
||||
// Returns a list of all lnd's currently connected and active peers.
|
||||
function getPeers() {
|
||||
return initializeRPCClient()
|
||||
.then(({ lightning }) => promiseify(lightning, lightning.ListPeers, {}, 'get peer information'))
|
||||
.then(grpcResponse => {
|
||||
if (grpcResponse && grpcResponse.peers) {
|
||||
return grpcResponse.peers;
|
||||
} else {
|
||||
throw new LndError('Unable to parse peer information');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Returns a list of lnd's currently pending channels. Pending channels include, channels that are in the process of
|
||||
// being opened, but have not reached three confirmations. Channels that are pending closed, but have not reached
|
||||
// one confirmation. Forced close channels that require potentially hundreds of confirmations.
|
||||
function getPendingChannels() {
|
||||
return initializeRPCClient()
|
||||
.then(({ lightning }) => promiseify(lightning, lightning.PendingChannels, {}, 'list pending channels'));
|
||||
}
|
||||
|
||||
function getWalletBalance() {
|
||||
return initializeRPCClient()
|
||||
.then(({ lightning }) => promiseify(lightning, lightning.WalletBalance, {}, 'get wallet balance'));
|
||||
}
|
||||
|
||||
function initWallet(options) {
|
||||
const passwordBuff = Buffer.from(options.password, 'utf8');
|
||||
|
||||
const rpcPayload = {
|
||||
wallet_password: passwordBuff,
|
||||
cipher_seed_mnemonic: options.mnemonic,
|
||||
recovery_window: DEFAULT_RECOVERY_WINDOW
|
||||
};
|
||||
|
||||
return initializeRPCClient().then(({ walletUnlocker, state }) => {
|
||||
if (state === true) {
|
||||
throw new LndError('Macaroon exists, therefore wallet already exists');
|
||||
}
|
||||
|
||||
return promiseify(walletUnlocker, walletUnlocker.InitWallet, rpcPayload, 'initialize wallet')
|
||||
.then(() => options.mnemonic);
|
||||
});
|
||||
}
|
||||
|
||||
// Returns a list of all invoices.
|
||||
function getInvoices() {
|
||||
const rpcPayload = {
|
||||
reversed: true, // Returns most recent
|
||||
num_max_invoices: 100,
|
||||
};
|
||||
|
||||
return initializeRPCClient()
|
||||
.then(({ lightning }) => promiseify(lightning, lightning.ListInvoices, rpcPayload, 'list invoices'));
|
||||
}
|
||||
|
||||
// Returns a list of all on chain transactions.
|
||||
function getOnChainTransactions() {
|
||||
return initializeRPCClient()
|
||||
.then(({ lightning }) => promiseify(lightning, lightning.GetTransactions, {}, 'list on-chain transactions'))
|
||||
.then(grpcResponse => grpcResponse.transactions);
|
||||
}
|
||||
|
||||
async function listUnspent() {
|
||||
const rpcPayload = {
|
||||
min_confs: 1,
|
||||
max_confs: 10000000, // Use arbitrarily high maximum confirmation limit.
|
||||
};
|
||||
|
||||
const conn = await initializeRPCClient();
|
||||
|
||||
return await promiseify(conn.lightning, conn.lightning.listUnspent, rpcPayload, 'estimate fee request');
|
||||
}
|
||||
|
||||
function openChannel(pubKey, amt, satPerByte) {
|
||||
const rpcPayload = {
|
||||
node_pubkey_string: pubKey,
|
||||
local_funding_amount: amt,
|
||||
};
|
||||
|
||||
if (satPerByte) {
|
||||
rpcPayload.sat_per_byte = satPerByte;
|
||||
} else {
|
||||
rpcPayload.target_conf = 6;
|
||||
}
|
||||
|
||||
return initializeRPCClient()
|
||||
.then(({ lightning }) => promiseify(lightning, lightning.OpenChannelSync, rpcPayload, 'open channel'));
|
||||
}
|
||||
|
||||
function sendCoins(addr, amt, satPerByte, sendAll) {
|
||||
const rpcPayload = {
|
||||
addr: addr, // eslint-disable-line object-shorthand
|
||||
amount: amt,
|
||||
send_all: sendAll,
|
||||
};
|
||||
|
||||
if (satPerByte) {
|
||||
rpcPayload.sat_per_byte = satPerByte;
|
||||
} else {
|
||||
rpcPayload.target_conf = 6;
|
||||
}
|
||||
|
||||
return initializeRPCClient()
|
||||
.then(({ lightning }) => promiseify(lightning, lightning.SendCoins, rpcPayload, 'send coins'));
|
||||
}
|
||||
|
||||
function sendPaymentSync(paymentRequest, amt) {
|
||||
const rpcPayload = {
|
||||
payment_request: paymentRequest,
|
||||
amt: amt, // eslint-disable-line object-shorthand
|
||||
};
|
||||
|
||||
return initializeRPCClient()
|
||||
.then(({ lightning }) => promiseify(lightning, lightning.SendPaymentSync, rpcPayload, 'send lightning payment'))
|
||||
.then(response => {
|
||||
// sometimes the error comes in on the response...
|
||||
if (response.paymentError) {
|
||||
throw new LndError(`Unable to send lightning payment: ${response.paymentError}`);
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
function unlockWallet(password) {
|
||||
const passwordBuff = Buffer.from(password, 'utf8');
|
||||
|
||||
const rpcPayload = {
|
||||
wallet_password: passwordBuff
|
||||
};
|
||||
|
||||
// TODO how to determine if wallet is already unlocked?
|
||||
// This will throw code 12 unimplemented, which is not very helpful
|
||||
return initializeRPCClient()
|
||||
.then(({ walletUnlocker }) => promiseify(walletUnlocker, walletUnlocker.UnlockWallet, rpcPayload, 'unlock wallet'));
|
||||
}
|
||||
|
||||
function updateChannelPolicy(global, fundingTxid, outputIndex, baseFeeMsat, feeRate, timeLockDelta) {
|
||||
const rpcPayload = {
|
||||
base_fee_msat: baseFeeMsat,
|
||||
fee_rate: feeRate,
|
||||
time_lock_delta: timeLockDelta,
|
||||
};
|
||||
|
||||
if (global) {
|
||||
rpcPayload.global = global;
|
||||
} else {
|
||||
rpcPayload.chan_point = {
|
||||
funding_txid_str: fundingTxid,
|
||||
output_index: outputIndex,
|
||||
};
|
||||
}
|
||||
|
||||
return initializeRPCClient()
|
||||
.then(({ lightning }) => promiseify(lightning, lightning.UpdateChannelPolicy, rpcPayload,
|
||||
'update channel policy coins'));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addInvoice,
|
||||
changePassword,
|
||||
closeChannel,
|
||||
connectToPeer,
|
||||
decodePaymentRequest,
|
||||
estimateFee,
|
||||
getChannelBalance,
|
||||
getClosedChannels,
|
||||
getFeeReport,
|
||||
getForwardingEvents,
|
||||
getInfo,
|
||||
getNodeInfo,
|
||||
getInvoices,
|
||||
getOpenChannels,
|
||||
getPayments,
|
||||
getPeers,
|
||||
getPendingChannels,
|
||||
getWalletBalance,
|
||||
generateAddress,
|
||||
generateSeed,
|
||||
getOnChainTransactions,
|
||||
initWallet,
|
||||
listUnspent,
|
||||
openChannel,
|
||||
sendCoins,
|
||||
sendPaymentSync,
|
||||
unlockWallet,
|
||||
updateChannelPolicy,
|
||||
};
|
@ -1,15 +0,0 @@
|
||||
// This file contains things that must happen before the app is imported (ie. things that happen on import)
|
||||
/* eslint-disable max-len */
|
||||
process.env.MACAROON_FILE = './test/fixtures/lnd/admin.macaroon';
|
||||
process.env.TLS_FILE = './test/fixtures/lnd/tls.cert';
|
||||
process.env.RPC_USER = 'test-user';
|
||||
process.env.RPC_PASSWORD = 'test-pass';
|
||||
process.env.JWT_PUBLIC_KEY = '2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d4677774451594a4b6f5a496876634e41514542425141445377417753414a42414a6949444e682b6770544f3937627135574748657476323267465a47736f4a0a6e6b54665058774335726a61674b4d56455a4a4a47584e6d51544e7441596e53615a31754a6e692f48356b4b32594e614a333933326730434177454141513d3d0a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d';
|
||||
|
||||
const sinon = require('sinon');
|
||||
global.Lightning = sinon.stub();
|
||||
global.WalletUnlocker = sinon.stub();
|
||||
sinon.stub(require('grpc'), 'load').returns({lnrpc: {
|
||||
Lightning: global.Lightning,
|
||||
WalletUnlocker: global.WalletUnlocker
|
||||
}});
|
17
test.sh
Executable file
17
test.sh
Executable file
@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# function cleanup() {
|
||||
# docker-compose down
|
||||
# echo "Nuking data dirs..."
|
||||
# rm -rf data
|
||||
# }
|
||||
# trap cleanup EXIT
|
||||
|
||||
docker-compose up &
|
||||
|
||||
while true
|
||||
do
|
||||
sleep 10
|
||||
echo "Generating a block..."
|
||||
docker-compose exec bitcoind bitcoin-cli -regtest generatetoaddress 1 "bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw"
|
||||
done
|
@ -1,226 +0,0 @@
|
||||
/* eslint-disable max-len,id-length */
|
||||
/* globals requester, reset */
|
||||
const sinon = require('sinon');
|
||||
const LndError = require('../../../../models/errors.js').LndError;
|
||||
|
||||
const bitcoindMocks = require('../../../mocks/bitcoind.js');
|
||||
const lndMocks = require('../../../mocks/lnd.js');
|
||||
|
||||
describe('v1/lnd/channel endpoints', () => {
|
||||
let token;
|
||||
|
||||
before(async() => {
|
||||
reset();
|
||||
|
||||
token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InRlc3QtdXNlciIsImlhdCI6MTU3NTIyNjQxMn0.N06esl2dhN1mFqn-0o4KQmmAaDW9OsHA39calpp_N9B3Ig3aXWgl064XAR9YVK0qwX7zMOnK9UrJ48KUZ-Sb4A';
|
||||
});
|
||||
|
||||
describe('/estimateFee GET', function() {
|
||||
|
||||
let bitcoindMempoolInfo;
|
||||
let lndEstimateFee;
|
||||
let lndGenerateAddress;
|
||||
let lndUnspentUtxos;
|
||||
let lndWalletBalance;
|
||||
|
||||
afterEach(() => {
|
||||
bitcoindMempoolInfo.restore();
|
||||
|
||||
lndEstimateFee.restore();
|
||||
lndGenerateAddress.restore();
|
||||
|
||||
if (lndUnspentUtxos) {
|
||||
lndUnspentUtxos.restore();
|
||||
}
|
||||
|
||||
if (lndWalletBalance) {
|
||||
lndWalletBalance.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('should return a fee estimate', done => {
|
||||
|
||||
const mempoolInfo = bitcoindMocks.getMempoolInfo();
|
||||
|
||||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo')
|
||||
.resolves(mempoolInfo);
|
||||
|
||||
const estimateFee = lndMocks.getEstimateFee();
|
||||
const generateAddress = lndMocks.generateAddress();
|
||||
|
||||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee')
|
||||
.resolves(estimateFee);
|
||||
lndGenerateAddress = sinon.stub(require('../../../../services/lnd.js'), 'generateAddress')
|
||||
.resolves(generateAddress);
|
||||
|
||||
requester
|
||||
.get('/v1/lnd/channel/estimateFee?amt=100000&confTarget=1')
|
||||
.set('authorization', `JWT ${token}`)
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(200);
|
||||
res.should.be.json;
|
||||
res.body.should.not.have.property('fast');
|
||||
res.body.should.not.have.property('normal');
|
||||
res.body.should.not.have.property('slow');
|
||||
res.body.should.not.have.property('cheapest');
|
||||
res.body.should.have.property('feeSat');
|
||||
res.body.should.have.property('feerateSatPerByte');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a fee estimate, group2', done => {
|
||||
|
||||
const mempoolInfo = bitcoindMocks.getMempoolInfo();
|
||||
|
||||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo')
|
||||
.resolves(mempoolInfo);
|
||||
|
||||
const estimateFee = lndMocks.getEstimateFee();
|
||||
const generateAddress = lndMocks.generateAddress();
|
||||
|
||||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee')
|
||||
.resolves(estimateFee);
|
||||
lndGenerateAddress = sinon.stub(require('../../../../services/lnd.js'), 'generateAddress')
|
||||
.resolves(generateAddress);
|
||||
|
||||
requester
|
||||
.get('/v1/lnd/channel/estimateFee?amt=100000&confTarget=0')
|
||||
.set('authorization', `JWT ${token}`)
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(200);
|
||||
res.should.be.json;
|
||||
res.body.should.have.property('fast');
|
||||
res.body.fast.should.have.property('feeSat');
|
||||
res.body.fast.should.have.property('feerateSatPerByte');
|
||||
res.body.should.have.property('normal');
|
||||
res.body.normal.should.have.property('feeSat');
|
||||
res.body.normal.should.have.property('feerateSatPerByte');
|
||||
res.body.should.have.property('slow');
|
||||
res.body.slow.should.have.property('feeSat');
|
||||
res.body.slow.should.have.property('feerateSatPerByte');
|
||||
res.body.should.have.property('cheapest');
|
||||
res.body.cheapest.should.have.property('feeSat');
|
||||
res.body.cheapest.should.have.property('feerateSatPerByte');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return insufficient funds', done => {
|
||||
|
||||
const mempoolInfo = bitcoindMocks.getMempoolInfo();
|
||||
|
||||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo')
|
||||
.resolves(mempoolInfo);
|
||||
|
||||
const generateAddress = lndMocks.generateAddress();
|
||||
|
||||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee')
|
||||
.throws(new LndError('Unable to estimate fee request', {details: 'insufficient funds available to construct transaction'}));
|
||||
lndGenerateAddress = sinon.stub(require('../../../../services/lnd.js'), 'generateAddress')
|
||||
.resolves(generateAddress);
|
||||
|
||||
requester
|
||||
.get('/v1/lnd/channel/estimateFee?amt=100000&confTarget=1')
|
||||
.set('authorization', `JWT ${token}`)
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(200);
|
||||
res.should.be.json;
|
||||
res.body.should.have.property('code');
|
||||
res.body.code.should.equal('INSUFFICIENT_FUNDS');
|
||||
res.body.should.have.property('text');
|
||||
res.body.text.should.equal('Lower amount or increase confirmation target.');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return output is dust', done => {
|
||||
|
||||
const mempoolInfo = bitcoindMocks.getMempoolInfo();
|
||||
|
||||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo')
|
||||
.resolves(mempoolInfo);
|
||||
|
||||
const generateAddress = lndMocks.generateAddress();
|
||||
|
||||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee')
|
||||
.throws(new LndError('Unable to estimate fee request', {details: 'transaction output is dust'}));
|
||||
lndGenerateAddress = sinon.stub(require('../../../../services/lnd.js'), 'generateAddress')
|
||||
.resolves(generateAddress);
|
||||
|
||||
requester
|
||||
.get('/v1/lnd/channel/estimateFee?amt=100000&confTarget=1')
|
||||
.set('authorization', `JWT ${token}`)
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(200);
|
||||
res.should.be.json;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/channel/policy GET', function() {
|
||||
let lndGetFeeReport;
|
||||
|
||||
afterEach(() => {
|
||||
lndGetFeeReport.restore();
|
||||
});
|
||||
|
||||
it('should return all channel policies', done => {
|
||||
|
||||
lndGetFeeReport = sinon.stub(require('../../../../services/lnd.js'), 'getFeeReport')
|
||||
.resolves(lndMocks.getFeeReport());
|
||||
|
||||
requester
|
||||
.get('/v1/lnd/channel/policy')
|
||||
.set('authorization', `JWT ${token}`)
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(200);
|
||||
res.should.be.json;
|
||||
res.body.should.be.an('array');
|
||||
|
||||
let channelPolicy = res.body[0];
|
||||
channelPolicy.should.have.property('channelPoint');
|
||||
channelPolicy.channelPoint.should.equal('231e0634b9d283200c1f59f5f4be1ba04464130c788ab97ba6ec2f7270e50167:0');
|
||||
channelPolicy.should.have.property('baseFeeMsat');
|
||||
channelPolicy.baseFeeMsat.should.equal('1000');
|
||||
channelPolicy.should.have.property('feePerMil');
|
||||
channelPolicy.feePerMil.should.equal('1');
|
||||
channelPolicy.should.have.property('feeRate');
|
||||
channelPolicy.feeRate.should.equal(0.000001);
|
||||
|
||||
channelPolicy = res.body[1];
|
||||
channelPolicy.should.have.property('channelPoint');
|
||||
channelPolicy.channelPoint.should.equal('d93d83c28a719e1a8689948a87a7025497643757d8cd23746e7af4d2710da09d:1');
|
||||
channelPolicy.should.have.property('baseFeeMsat');
|
||||
channelPolicy.baseFeeMsat.should.equal('2000');
|
||||
channelPolicy.should.have.property('feePerMil');
|
||||
channelPolicy.feePerMil.should.equal('2');
|
||||
channelPolicy.should.have.property('feeRate');
|
||||
channelPolicy.feeRate.should.equal(0.000002);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,94 +0,0 @@
|
||||
/* eslint-disable max-len,id-length */
|
||||
/* globals requester, reset */
|
||||
const sinon = require('sinon');
|
||||
const LndError = require('../../../../models/errors.js').LndError;
|
||||
const lndMocks = require('../../../mocks/lnd.js');
|
||||
|
||||
describe('v1/lnd/lightning endpoints', () => {
|
||||
let token;
|
||||
|
||||
before(async() => {
|
||||
reset();
|
||||
|
||||
token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InRlc3QtdXNlciIsImlhdCI6MTU3NTIyNjQxMn0.N06esl2dhN1mFqn-0o4KQmmAaDW9OsHA39calpp_N9B3Ig3aXWgl064XAR9YVK0qwX7zMOnK9UrJ48KUZ-Sb4A';
|
||||
});
|
||||
|
||||
describe('/forwardingEvents GET', function() {
|
||||
let lndForwardingHistory;
|
||||
|
||||
afterEach(() => {
|
||||
lndForwardingHistory.restore();
|
||||
});
|
||||
|
||||
it('should return forwarding events', done => {
|
||||
|
||||
lndForwardingHistory = sinon.stub(require('../../../../services/lnd.js'), 'getForwardingEvents')
|
||||
.resolves(lndMocks.getForwardingEvents());
|
||||
|
||||
requester
|
||||
.get('/v1/lnd/lightning/forwardingEvents?startTime=1548178729853&endTime=1548178729853&indexOffset=1548178729853')
|
||||
.set('authorization', `JWT ${token}`)
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(200);
|
||||
res.should.be.json;
|
||||
res.body.should.have.property('forwardingEvents');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should 400 with invalid query parameters', done => {
|
||||
|
||||
lndForwardingHistory = sinon.stub(require('../../../../services/lnd.js'), 'getForwardingEvents')
|
||||
.resolves(lndMocks.getForwardingEvents());
|
||||
|
||||
requester
|
||||
.get('/v1/lnd/lightning/forwardingEvents?startTime=beginingOfUniverse')
|
||||
.set('authorization', `JWT ${token}`)
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
|
||||
res.should.have.status(400);
|
||||
res.should.be.json;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should 401 without a valid token', done => {
|
||||
requester
|
||||
.get('/v1/lnd/lightning/forwardingEvents')
|
||||
.set('authorization', 'JWT invalid')
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should 500 on lnd error', done => {
|
||||
|
||||
lndForwardingHistory = sinon.stub(require('../../../../services/lnd.js'), 'getForwardingEvents')
|
||||
.throws(new LndError('error getting forwarding events'));
|
||||
|
||||
requester
|
||||
.get('/v1/lnd/lightning/forwardingEvents?startTime=1548178729853&endTime=1548178729853&indexOffset=1548178729853')
|
||||
.set('authorization', `JWT ${token}`)
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(500);
|
||||
res.should.be.json;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,400 +0,0 @@
|
||||
/* eslint-disable max-len,id-length */
|
||||
/* globals requester, reset */
|
||||
const sinon = require('sinon');
|
||||
const LndError = require('../../../../models/errors.js').LndError;
|
||||
|
||||
const bitcoindMocks = require('../../../mocks/bitcoind.js');
|
||||
const lndMocks = require('../../../mocks/lnd.js');
|
||||
|
||||
describe('v1/lnd/transaction endpoints', () => {
|
||||
let token;
|
||||
|
||||
before(async() => {
|
||||
reset();
|
||||
|
||||
token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InRlc3QtdXNlciIsImlhdCI6MTU3NTIyNjQxMn0.N06esl2dhN1mFqn-0o4KQmmAaDW9OsHA39calpp_N9B3Ig3aXWgl064XAR9YVK0qwX7zMOnK9UrJ48KUZ-Sb4A';
|
||||
});
|
||||
|
||||
describe('/ GET', function() {
|
||||
|
||||
let lndListChainTxns;
|
||||
let lndOpenChannels;
|
||||
let lndClosedChannels;
|
||||
let lndPendingChannels;
|
||||
|
||||
afterEach(() => {
|
||||
lndListChainTxns.restore();
|
||||
lndOpenChannels.restore();
|
||||
lndClosedChannels.restore();
|
||||
lndPendingChannels.restore();
|
||||
});
|
||||
|
||||
it('should return one of each transaction type', done => {
|
||||
|
||||
const onChainRecieved = lndMocks.getOnChainTransaction();
|
||||
const onChainSent = lndMocks.getOnChainTransaction();
|
||||
onChainSent.amount = '-1000000';
|
||||
const onChainChannelClosed = lndMocks.getOnChainTransaction();
|
||||
const onChainChannelOpen = lndMocks.getOnChainTransaction();
|
||||
const onChainChannelPreviouslyOpen = lndMocks.getOnChainTransaction();
|
||||
const onChainPendingOpen = lndMocks.getOnChainTransaction('c0b7045595f4f5c024af22312055497e99ed8b7b62b0c7e181d16382a07ae58b');
|
||||
const onChainPendingClose = lndMocks.getOnChainTransaction('653c87589da62b5fef18538a62ecce154f94236f158d1148efab98136756ed36');
|
||||
|
||||
const openChannels = [lndMocks.getChannelOpen(onChainChannelOpen.txHash)];
|
||||
const closedChannel = [lndMocks.getChannelClosed(undefined, onChainChannelClosed.txHash),
|
||||
lndMocks.getChannelClosed(onChainChannelPreviouslyOpen.txHash, undefined)];
|
||||
const pendingChannels = lndMocks.getPendingChannels();
|
||||
|
||||
lndListChainTxns = sinon.stub(require('../../../../services/lnd.js'), 'getOnChainTransactions')
|
||||
.resolves([onChainChannelPreviouslyOpen, onChainPendingClose, onChainPendingOpen, onChainRecieved, onChainSent,
|
||||
onChainChannelClosed, onChainChannelOpen]);
|
||||
lndOpenChannels = sinon.stub(require('../../../../services/lnd.js'), 'getOpenChannels')
|
||||
.resolves(openChannels);
|
||||
lndClosedChannels = sinon.stub(require('../../../../services/lnd.js'), 'getClosedChannels')
|
||||
.resolves(closedChannel);
|
||||
lndPendingChannels = sinon.stub(require('../../../../services/lnd.js'), 'getPendingChannels')
|
||||
.resolves(pendingChannels);
|
||||
|
||||
requester
|
||||
.get('/v1/lnd/transaction')
|
||||
.set('authorization', `JWT ${token}`)
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(200);
|
||||
res.should.be.json;
|
||||
|
||||
res.body[0].type.should.equal('CHANNEL_OPEN');
|
||||
res.body[1].type.should.equal('CHANNEL_CLOSE');
|
||||
res.body[2].type.should.equal('ON_CHAIN_TRANSACTION_SENT');
|
||||
res.body[3].type.should.equal('ON_CHAIN_TRANSACTION_RECEIVED');
|
||||
res.body[4].type.should.equal('PENDING_OPEN');
|
||||
res.body[5].type.should.equal('PENDING_CLOSE');
|
||||
res.body[6].type.should.equal('CHANNEL_OPEN');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/estimateFee GET', function() {
|
||||
let bitcoindMempoolInfo;
|
||||
let lndEstimateFee;
|
||||
let lndUnspentUtxos;
|
||||
let lndWalletBalance;
|
||||
|
||||
afterEach(() => {
|
||||
bitcoindMempoolInfo.restore();
|
||||
lndEstimateFee.restore();
|
||||
|
||||
if (lndUnspentUtxos) {
|
||||
lndUnspentUtxos.restore();
|
||||
}
|
||||
|
||||
if (lndWalletBalance) {
|
||||
lndWalletBalance.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('should return a fee estimate', done => {
|
||||
|
||||
const mempoolInfo = bitcoindMocks.getMempoolInfo();
|
||||
|
||||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo')
|
||||
.resolves(mempoolInfo);
|
||||
|
||||
const estimateFee = lndMocks.getEstimateFee();
|
||||
|
||||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee')
|
||||
.resolves(estimateFee);
|
||||
|
||||
requester
|
||||
.get('/v1/lnd/transaction/estimateFee?address=2NFGwqm9N9LomEh9mzQgofr1WGqkwaxPuWg&amt=100000&confTarget=1&sweep=false')
|
||||
.set('authorization', `JWT ${token}`)
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(200);
|
||||
res.should.be.json;
|
||||
res.body.should.not.have.property('fast');
|
||||
res.body.should.not.have.property('normal');
|
||||
res.body.should.not.have.property('slow');
|
||||
res.body.should.not.have.property('cheapest');
|
||||
res.body.should.have.property('feeSat');
|
||||
res.body.should.have.property('feerateSatPerByte');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a fee estimate, group', done => {
|
||||
|
||||
const mempoolInfo = bitcoindMocks.getMempoolInfo();
|
||||
|
||||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo')
|
||||
.resolves(mempoolInfo);
|
||||
|
||||
const estimateFee = lndMocks.getEstimateFee();
|
||||
|
||||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee')
|
||||
.resolves(estimateFee);
|
||||
|
||||
requester
|
||||
.get('/v1/lnd/transaction/estimateFee?address=2NFGwqm9N9LomEh9mzQgofr1WGqkwaxPuWg&amt=100000&confTarget=0&sweep=false')
|
||||
.set('authorization', `JWT ${token}`)
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(200);
|
||||
res.should.be.json;
|
||||
res.body.should.have.property('fast');
|
||||
res.body.fast.should.have.property('feeSat');
|
||||
res.body.fast.should.have.property('feerateSatPerByte');
|
||||
res.body.should.have.property('normal');
|
||||
res.body.normal.should.have.property('feeSat');
|
||||
res.body.normal.should.have.property('feerateSatPerByte');
|
||||
res.body.should.have.property('slow');
|
||||
res.body.slow.should.have.property('feeSat');
|
||||
res.body.slow.should.have.property('feerateSatPerByte');
|
||||
res.body.should.have.property('cheapest');
|
||||
res.body.cheapest.should.have.property('feeSat');
|
||||
res.body.cheapest.should.have.property('feerateSatPerByte');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return insufficient funds', done => {
|
||||
|
||||
const mempoolInfo = bitcoindMocks.getMempoolInfo();
|
||||
|
||||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo')
|
||||
.resolves(mempoolInfo);
|
||||
|
||||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee')
|
||||
.throws(new LndError('Unable to estimate fee request', {details: 'insufficient funds available to construct transaction'}));
|
||||
|
||||
requester
|
||||
.get('/v1/lnd/transaction/estimateFee?address=2NFGwqm9N9LomEh9mzQgofr1WGqkwaxPuWg&amt=100000&confTarget=1&sweep=false')
|
||||
.set('authorization', `JWT ${token}`)
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(200);
|
||||
res.should.be.json;
|
||||
res.body.should.have.property('code');
|
||||
res.body.code.should.equal('INSUFFICIENT_FUNDS');
|
||||
res.body.should.have.property('text');
|
||||
res.body.text.should.equal('Lower amount or increase confirmation target.');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return output is dust', done => {
|
||||
|
||||
const mempoolInfo = bitcoindMocks.getMempoolInfo();
|
||||
|
||||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo')
|
||||
.resolves(mempoolInfo);
|
||||
|
||||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee')
|
||||
.throws(new LndError('Unable to estimate fee request', {details: 'transaction output is dust'}));
|
||||
|
||||
requester
|
||||
.get('/v1/lnd/transaction/estimateFee?address=2NFGwqm9N9LomEh9mzQgofr1WGqkwaxPuWg&amt=100000&confTarget=1&sweep=false')
|
||||
.set('authorization', `JWT ${token}`)
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(200);
|
||||
res.should.be.json;
|
||||
res.body.should.have.property('code');
|
||||
res.body.code.should.equal('OUTPUT_IS_DUST');
|
||||
res.body.should.have.property('text');
|
||||
res.body.text.should.equal('Transaction output is dust.');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return invalid address', done => {
|
||||
|
||||
const mempoolInfo = bitcoindMocks.getMempoolInfo();
|
||||
|
||||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo')
|
||||
.resolves(mempoolInfo);
|
||||
|
||||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee')
|
||||
.throws(new LndError('Unable to estimate fee request', {details: 'checksum mismatch'}));
|
||||
|
||||
requester
|
||||
.get('/v1/lnd/transaction/estimateFee?address=2NFGwqm9N9LomEh9mzQgofr1WGqkwaxPuWg&amt=100000&confTarget=1&sweep=false')
|
||||
.set('authorization', `JWT ${token}`)
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(200);
|
||||
res.should.be.json;
|
||||
res.body.should.have.property('code');
|
||||
res.body.code.should.equal('INVALID_ADDRESS');
|
||||
res.body.should.have.property('text');
|
||||
res.body.text.should.equal('Please validate the Bitcoin address is correct.');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a sweep estimate, group', done => {
|
||||
|
||||
const mempoolInfo = bitcoindMocks.getMempoolInfo();
|
||||
|
||||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo')
|
||||
.resolves(mempoolInfo);
|
||||
|
||||
const estimateFee = lndMocks.getEstimateFee();
|
||||
const walletBalance = lndMocks.getWalletBalance();
|
||||
|
||||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee')
|
||||
.resolves(estimateFee);
|
||||
|
||||
lndWalletBalance = sinon.stub(require('../../../../services/lnd.js'), 'getWalletBalance')
|
||||
.resolves(walletBalance);
|
||||
|
||||
requester
|
||||
.get('/v1/lnd/transaction/estimateFee?address=2NFGwqm9N9LomEh9mzQgofr1WGqkwaxPuWg&amt=100000&confTarget=0&sweep=true')
|
||||
.set('authorization', `JWT ${token}`)
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(200);
|
||||
res.should.be.json;
|
||||
res.body.should.have.property('fast');
|
||||
res.body.fast.should.have.property('feeSat');
|
||||
res.body.fast.should.have.property('feerateSatPerByte');
|
||||
res.body.should.have.property('normal');
|
||||
res.body.normal.should.have.property('feeSat');
|
||||
res.body.normal.should.have.property('feerateSatPerByte');
|
||||
res.body.should.have.property('slow');
|
||||
res.body.slow.should.have.property('feeSat');
|
||||
res.body.slow.should.have.property('feerateSatPerByte');
|
||||
res.body.should.have.property('cheapest');
|
||||
res.body.cheapest.should.have.property('feeSat');
|
||||
res.body.cheapest.should.have.property('feerateSatPerByte');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should return a sweep estimate', done => {
|
||||
|
||||
const mempoolInfo = bitcoindMocks.getMempoolInfo();
|
||||
|
||||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo')
|
||||
.resolves(mempoolInfo);
|
||||
|
||||
const estimateFee = lndMocks.getEstimateFee();
|
||||
const walletBalance = lndMocks.getWalletBalance();
|
||||
|
||||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee')
|
||||
.resolves(estimateFee);
|
||||
|
||||
lndWalletBalance = sinon.stub(require('../../../../services/lnd.js'), 'getWalletBalance')
|
||||
.resolves(walletBalance);
|
||||
|
||||
requester
|
||||
.get('/v1/lnd/transaction/estimateFee?address=2NFGwqm9N9LomEh9mzQgofr1WGqkwaxPuWg&amt=100000&confTarget=1&sweep=true')
|
||||
.set('authorization', `JWT ${token}`)
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(200);
|
||||
res.should.be.json;
|
||||
res.body.should.not.have.property('fast');
|
||||
res.body.should.not.have.property('normal');
|
||||
res.body.should.not.have.property('slow');
|
||||
res.body.should.not.have.property('cheapest');
|
||||
res.body.should.have.property('feeSat');
|
||||
res.body.should.have.property('feerateSatPerByte');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return insufficient funds for sweep estimate', done => {
|
||||
|
||||
const mempoolInfo = bitcoindMocks.getMempoolInfo();
|
||||
|
||||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo')
|
||||
.resolves(mempoolInfo);
|
||||
|
||||
const walletBalance = lndMocks.getWalletBalance();
|
||||
|
||||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee')
|
||||
.throws(new LndError('Unable to estimate fee request', {details: 'insufficient funds available to construct transaction'}));
|
||||
|
||||
lndWalletBalance = sinon.stub(require('../../../../services/lnd.js'), 'getWalletBalance')
|
||||
.resolves(walletBalance);
|
||||
|
||||
requester
|
||||
.get('/v1/lnd/transaction/estimateFee?address=2NFGwqm9N9LomEh9mzQgofr1WGqkwaxPuWg&amt=100000&confTarget=1&sweep=true')
|
||||
.set('authorization', `JWT ${token}`)
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(200);
|
||||
res.should.be.json;
|
||||
res.body.should.have.property('code');
|
||||
res.body.code.should.equal('INSUFFICIENT_FUNDS');
|
||||
res.body.should.have.property('text');
|
||||
res.body.text.should.equal('Lower amount or increase confirmation target.');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a fee rate too low error', done => {
|
||||
|
||||
const mempoolInfo = bitcoindMocks.getMempoolInfo();
|
||||
mempoolInfo.result.mempoolminfee = 0.01;
|
||||
|
||||
bitcoindMempoolInfo = sinon.stub(require('../../../../services/bitcoind.js'), 'getMempoolInfo')
|
||||
.resolves(mempoolInfo);
|
||||
|
||||
const estimateFee = lndMocks.getEstimateFee();
|
||||
|
||||
lndEstimateFee = sinon.stub(require('../../../../services/lnd.js'), 'estimateFee')
|
||||
.resolves(estimateFee);
|
||||
|
||||
requester
|
||||
.get('/v1/lnd/transaction/estimateFee?address=2NFGwqm9N9LomEh9mzQgofr1WGqkwaxPuWg&amt=100000&confTarget=1&sweep=false')
|
||||
.set('authorization', `JWT ${token}`)
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(200);
|
||||
res.should.be.json;
|
||||
res.body.should.not.have.property('fast');
|
||||
res.body.should.not.have.property('normal');
|
||||
res.body.should.not.have.property('slow');
|
||||
res.body.should.not.have.property('cheapest');
|
||||
res.body.should.have.property('code');
|
||||
res.body.code.should.equal('FEE_RATE_TOO_LOW');
|
||||
res.body.should.have.property('text');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -1,112 +0,0 @@
|
||||
/* eslint-disable max-len,id-length */
|
||||
/* globals requester, reset */
|
||||
const sinon = require('sinon');
|
||||
|
||||
const lndErrorMocks = require('../../../mocks/LndError.js');
|
||||
|
||||
describe('v1/lnd/wallet endpoints', () => {
|
||||
let token;
|
||||
|
||||
before(async() => {
|
||||
reset();
|
||||
|
||||
token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InRlc3QtdXNlciIsImlhdCI6MTU3NTIyNjQxMn0.N06esl2dhN1mFqn-0o4KQmmAaDW9OsHA39calpp_N9B3Ig3aXWgl064XAR9YVK0qwX7zMOnK9UrJ48KUZ-Sb4A';
|
||||
});
|
||||
|
||||
describe('/changePassword GET', function() {
|
||||
let lndChangePassword;
|
||||
|
||||
afterEach(() => {
|
||||
lndChangePassword.restore();
|
||||
});
|
||||
|
||||
it('should return 200 on success', done => {
|
||||
|
||||
lndChangePassword = sinon.stub(require('../../../../services/lnd.js'), 'changePassword')
|
||||
.resolves({});
|
||||
|
||||
requester
|
||||
.post('/v1/lnd/wallet/changePassword')
|
||||
.set('authorization', `JWT ${token}`)
|
||||
.send({currentPassword: 'currentPassword', newPassword: 'newPassword1'})
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(200);
|
||||
res.should.be.json;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should 400 with invalid currentPassword', done => {
|
||||
|
||||
requester
|
||||
.post('/v1/lnd/wallet/changePassword')
|
||||
.set('authorization', `JWT ${token}`)
|
||||
.send({currentPassword: undefined, newPassword: 'newPassword1'})
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(400);
|
||||
res.should.be.json;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should 401 without a valid token', done => {
|
||||
requester
|
||||
.post('/v1/lnd/wallet/changePassword')
|
||||
.set('authorization', 'JWT invalid')
|
||||
.send({currentPassword: 'currentPassword', newPassword: 'newPassword1'})
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(401);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should 403 when lnd says current password is wrong', done => {
|
||||
|
||||
lndChangePassword = sinon.stub(require('../../../../services/lnd.js'), 'changePassword')
|
||||
.throws(lndErrorMocks.invalidPasswordError());
|
||||
|
||||
requester
|
||||
.post('/v1/lnd/wallet/changePassword')
|
||||
.set('authorization', `JWT ${token}`)
|
||||
.send({currentPassword: 'currentPassword', newPassword: 'newPassword1'})
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(403);
|
||||
res.should.be.json;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should 502 then lnd is restarting', done => {
|
||||
|
||||
lndChangePassword = sinon.stub(require('../../../../services/lnd.js'), 'changePassword')
|
||||
.throws(lndErrorMocks.connectionFailedError());
|
||||
|
||||
requester
|
||||
.post('/v1/lnd/wallet/changePassword')
|
||||
.set('authorization', `JWT ${token}`)
|
||||
.send({currentPassword: 'currentPassword', newPassword: 'newPassword1'})
|
||||
.end((err, res) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
res.should.have.status(502);
|
||||
res.should.be.json;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
BIN
test/fixtures/lnd/admin.macaroon
vendored
BIN
test/fixtures/lnd/admin.macaroon
vendored
Binary file not shown.
13
test/fixtures/lnd/tls.cert
vendored
13
test/fixtures/lnd/tls.cert
vendored
@ -1,13 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB6zCCAZGgAwIBAgIRALnfzV970zEhf+9IFvkjeaswCgYIKoZIzj0EAwIwODEf
|
||||
MB0GA1UEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMMGJlNWJh
|
||||
MWJkMDM3MB4XDTE4MDkwMzIxNDYzNVoXDTE5MTAyOTIxNDYzNVowODEfMB0GA1UE
|
||||
ChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMMGJlNWJhMWJkMDM3
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjeinHi2dnFK7S/jgzb0xLtYHnQtB
|
||||
5A5v2446ZCOK7wgCXCv3lohLlqfrk1kmhCBOKKFWfUp4cHT74U0GWdq48qN8MHow
|
||||
DgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB/wQFMAMBAf8wVwYDVR0RBFAwToIMMGJl
|
||||
NWJhMWJkMDM3gglsb2NhbGhvc3SCA2xuZIIEdW5peIIKdW5peHBhY2tldIcEfwAA
|
||||
AYcQAAAAAAAAAAAAAAAAAAAAAYcErBUABTAKBggqhkjOPQQDAgNIADBFAiBe56v6
|
||||
p+bIyx5u01FApm17p5E5p5/ZD4OeW13RqXD2iQIhAJxNXD2vcgMbN8+pAsblqlTi
|
||||
0UwIDwe5Cvg3bibTv6to
|
||||
-----END CERTIFICATE-----
|
@ -1,24 +0,0 @@
|
||||
const LndError = require('../../models/errors.js').LndError;
|
||||
|
||||
function connectionFailedError() {
|
||||
return new LndError('Unable to change password', {
|
||||
Error: 2,
|
||||
UNKNOWN: 'Connect Failed',
|
||||
code: 14,
|
||||
details: 'Connect Failed'
|
||||
});
|
||||
}
|
||||
|
||||
function invalidPasswordError() {
|
||||
return new LndError('Unable to change password', {
|
||||
Error: 2,
|
||||
UNKNOWN: 'invalid passphrase for master public key',
|
||||
code: 2,
|
||||
details: 'invalid passphrase for master public key'
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
connectionFailedError,
|
||||
invalidPasswordError,
|
||||
};
|
@ -1,426 +0,0 @@
|
||||
/* eslint-disable camelcase, id-length, max-len */
|
||||
|
||||
function randomString(length, chars) {
|
||||
let result = '';
|
||||
for (let i = length; i > 0; --i) {
|
||||
result += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function randomTxId() {
|
||||
return randomString(64, '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
|
||||
}
|
||||
|
||||
function generateAddress() {
|
||||
return '2NFGwqm9N9LomEh9mzQgofr1WGqkwaxPuWg';
|
||||
}
|
||||
|
||||
function getChannelOpen(channelPoint) {
|
||||
|
||||
return {
|
||||
active: true,
|
||||
remotePubkey: '03311aebc4d9eb8a237d89ae771dec0d1b8a64aa31625c105800fdf5f934d824d2',
|
||||
channelPoint: (channelPoint || randomTxId()) + ':0',
|
||||
chanId: '440904162803712',
|
||||
capacity: '100000',
|
||||
localBalance: '40950',
|
||||
remoteBalance: '50000',
|
||||
commitFee: '9050',
|
||||
commitWeight: '724',
|
||||
feePerKw: '12500',
|
||||
unsettledBalance: '0',
|
||||
totalSatoshisSent: '0',
|
||||
totalSatoshisReceived: '0',
|
||||
numUpdates: '0',
|
||||
pendingHtlcs: [],
|
||||
csvDelay: 144,
|
||||
private: false,
|
||||
initiator: true,
|
||||
chan_status_flags: 'ChanStatusDefault',
|
||||
};
|
||||
}
|
||||
|
||||
function getChannelClosed(channelPoint, closingTxHash) {
|
||||
return {
|
||||
channelPoint: (channelPoint || randomTxId()) + ':0',
|
||||
chanId: '440904162803712',
|
||||
chainHash: '000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943',
|
||||
closingTxHash: closingTxHash || randomTxId(),
|
||||
remotePubkey: '03311aebc4d9eb8a237d89ae771dec0d1b8a64aa31625c105800fdf5f934d824d2',
|
||||
capacity: '100000',
|
||||
closeHeight: 1564209,
|
||||
settledBalance: '99817',
|
||||
timeLockedBalance: '0',
|
||||
closeType: 'COOPERATIVE_CLOSE',
|
||||
};
|
||||
}
|
||||
|
||||
function getChannelBalance() {
|
||||
return 42000;
|
||||
}
|
||||
|
||||
function getEstimateFee() {
|
||||
return {
|
||||
feeSat: '44115',
|
||||
feerateSatPerByte: '83',
|
||||
};
|
||||
}
|
||||
|
||||
function getFeeReport() {
|
||||
return {
|
||||
channelFees: [
|
||||
{
|
||||
channelPoint: '231e0634b9d283200c1f59f5f4be1ba04464130c788ab97ba6ec2f7270e50167:0',
|
||||
baseFeeMsat: '1000',
|
||||
feePerMil: '1',
|
||||
feeRate: 0.000001
|
||||
},
|
||||
{
|
||||
channelPoint: 'd93d83c28a719e1a8689948a87a7025497643757d8cd23746e7af4d2710da09d:1',
|
||||
baseFeeMsat: '2000',
|
||||
feePerMil: '2',
|
||||
feeRate: 0.000002
|
||||
},
|
||||
],
|
||||
dayFeeSum: '0',
|
||||
weekFeeSum: '0',
|
||||
monthFeeSum: '0',
|
||||
};
|
||||
}
|
||||
|
||||
function getForwardingEvents() {
|
||||
return {
|
||||
forwardingEvents: [
|
||||
{
|
||||
timestamp: '1548021945',
|
||||
chanIdIn: '614599512239964161',
|
||||
chanIdOut: '614438983628095489',
|
||||
amtIn: '2',
|
||||
amtOut: '1',
|
||||
fee: '1'
|
||||
}
|
||||
],
|
||||
lastOffsetIndex: 1,
|
||||
};
|
||||
}
|
||||
|
||||
function getInfo() {
|
||||
return {
|
||||
identity_pubkey: '036dfd60929cb57836a65daa763ceb36a26f4691c670fca91f9ee8b9bf2b445fb8',
|
||||
alias: 'nicks-node',
|
||||
num_pending_channels: 0,
|
||||
num_active_channels: 0,
|
||||
num_peers: 0,
|
||||
block_height: 1382511,
|
||||
block_hash: '0000000000000068cc4f6dccdd7efeecd718a19217025205515d3b3a898370c6',
|
||||
syncedToChain: false,
|
||||
testnet: true,
|
||||
chains: [
|
||||
'bitcoin'
|
||||
],
|
||||
uris: ['036dfd60929cb57836a65daa763ceb36a26f4691c670fca91f9ee8b9bf2b445fb8:192.168.0.2:10009'],
|
||||
best_header_timestamp: '1533778315',
|
||||
version: '0.4.2-beta commit=33a5567a0fef801800cd56267e2b264d32c93173'
|
||||
};
|
||||
}
|
||||
|
||||
function getWalletBalance() {
|
||||
return {
|
||||
totalBalance: '140000',
|
||||
confirmedBalance: '140000',
|
||||
unconfirmedBalance: '140000'
|
||||
};
|
||||
}
|
||||
|
||||
function getManagedChannelsFile() {
|
||||
return '{"6efe84b44bc9d184979f2527b73cbf0223a5549a3932e78a1460499166f2639e:0":{"name":"Test Node","purpose":"Much Lightning"}}';
|
||||
}
|
||||
|
||||
function getOpenChannels() {
|
||||
return [
|
||||
getChannelOpen('a6997a3b054265acb1a05e84f1b49f34e87a4758ea9b629839fe7311a0ac3c94'),
|
||||
getChannelOpen('47e615ba7d35b5c7e93a62e9adb84fddc11df43dc0790b7000a0a42be243e210'),
|
||||
];
|
||||
}
|
||||
|
||||
function getPendingChannels() {
|
||||
return {
|
||||
total_limbo_balance: '0',
|
||||
pendingOpenChannels: [
|
||||
{
|
||||
channel: {
|
||||
remoteNodePub: '03a13a469bae4785e27fae24e7664e648cfdb976b97f95c694dea5e55e7d302846',
|
||||
channelPoint: 'c0b7045595f4f5c024af22312055497e99ed8b7b62b0c7e181d16382a07ae58b:0',
|
||||
capacity: '10000000',
|
||||
localBalance: '9999817',
|
||||
remoteBalance: '0'
|
||||
},
|
||||
confirmationHeight: 0,
|
||||
commitFee: '183',
|
||||
commitWeight: '600',
|
||||
feePerKw: '253'
|
||||
},
|
||||
{
|
||||
channel: {
|
||||
remoteNodePub: '03a13a469bae4785e27fae24e7664e648cfdb976b97f95c694dea5e55e7d302846',
|
||||
channelPoint: 'c1b7045595f4f5c024af22287755b21f65e1ec7fbe11ee0181d16382a07ae58b:0',
|
||||
capacity: '10000000',
|
||||
localBalance: '9999817',
|
||||
remoteBalance: '0'
|
||||
},
|
||||
confirmationHeight: 0,
|
||||
commitFee: '183',
|
||||
commitWeight: '600',
|
||||
feePerKw: '253'
|
||||
}
|
||||
],
|
||||
pendingClosingChannels: [],
|
||||
pendingForceClosingChannels: [
|
||||
{
|
||||
channel: {
|
||||
remoteNodePub: '03ce542ac3320900154ea33c8dfb0e8faa5e6facd88d5de22b011d135e3f5e906f',
|
||||
channelPoint: '76cf2031469c8cd16114dc3dddf72e6fa845e433553bdc11388b7e3b0871b296:0',
|
||||
capacity: '100000',
|
||||
localBalance: '99817',
|
||||
remoteBalance: '0'
|
||||
},
|
||||
closingTxid: '653c87589da62b5fef18538a62ecce154f94236f158d1148efab98136756ed36',
|
||||
limboBalance: '99817',
|
||||
maturityHeight: 1564543,
|
||||
blocksTilMaturity: 144,
|
||||
recoveredBalance: '0',
|
||||
pendingHtlcs: [
|
||||
]
|
||||
}
|
||||
],
|
||||
waitingCloseChannels: []
|
||||
};
|
||||
}
|
||||
|
||||
function getOnChainTransaction(txHash) {
|
||||
return {
|
||||
txHash: txHash || randomTxId(),
|
||||
amount: '100000',
|
||||
numConfirmations: 21353,
|
||||
blockHash: '000000000000030984420cbf3cbbcdfe57f9cf9afa05b3b04ef8267a53f52c43',
|
||||
blockHeight: 1542864,
|
||||
timeStamp: 1560382362,
|
||||
totalFees: 0,
|
||||
destAddresses: [
|
||||
'2N9Dj2NCZhKZs4QaCHuXk5jYev4CHhQTywW',
|
||||
'2MvikCGP9D2hwz7ocRqWdJHnNFmExLn6Hw8'
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function getOnChainTransactions() {
|
||||
return [
|
||||
{
|
||||
active: true,
|
||||
remotePubKey: '0270685ca81a8e4d4d01beec5781f4cc924684072ae52c507f8ebe9daf0caaab7b',
|
||||
channelPoint: '9449c2cba3cb9a94bad58eeff3287755b21f65e1ec7fbe11ee0f485a6bb4094e:0',
|
||||
chanId: '1582956994904784896',
|
||||
capacity: '10000000',
|
||||
localBalance: '9739816',
|
||||
remoteBalance: '260000',
|
||||
commitFee: '184',
|
||||
commitWeight: '724',
|
||||
feePerKw: '253',
|
||||
unsettledBalance: '0',
|
||||
totalSatoshisSent: '260000',
|
||||
totalSatoshisReceived: '0',
|
||||
numUpdates: '10',
|
||||
pendingHtlcs: [
|
||||
],
|
||||
csvDelay: 1201,
|
||||
private: false
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
remotePubKey: '036b96e4713c5f84dcb8030592e1bd42a2d9a43d91fa2e535b9bfd05f2c5def9b9',
|
||||
channelPoint: '2786816bc527ec570c6fd249ce85fa4e6bddc70675b6a03f1a4a5eefaaae3663:0',
|
||||
chanId: '1582956994904915968',
|
||||
capacity: '10000000',
|
||||
localBalance: '9999817',
|
||||
remoteBalance: '0',
|
||||
commitFee: '183',
|
||||
commitWeight: '600',
|
||||
feePerKw: '253',
|
||||
unsettledBalance: '0',
|
||||
totalSatoshisSent: '0',
|
||||
totalSatoshisReceived: '0',
|
||||
numUpdates: '0',
|
||||
pendingHtlcs: [
|
||||
],
|
||||
csvDelay: 1201,
|
||||
private: false
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
remotePubKey: '03819ddbe246214d4c57b7ea4d28bfe5c09c03bb4308b40c26f1468532131e0cc0',
|
||||
channelPoint: 'bea04831d2f479de97a08cd12af688e930eadf2e470e7e6c1719cdf4d5982114:0',
|
||||
chanId: '1582956994904719360',
|
||||
capacity: '10000000',
|
||||
localBalance: '9999817',
|
||||
remoteBalance: '0',
|
||||
commitFee: '183',
|
||||
commitWeight: '600',
|
||||
feePerKw: '253',
|
||||
unsettledBalance: '0',
|
||||
totalSatoshisSent: '0',
|
||||
totalSatoshisReceived: '0',
|
||||
numUpdates: '0',
|
||||
pendingHtlcs: [
|
||||
],
|
||||
csvDelay: 1201,
|
||||
private: false
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
remotePubKey: '03adf1a17ab83438f23bc6c3b87ed8664757923988d5907c469840ddba1a7d1415',
|
||||
channelPoint: 'da6d80297ec79cf115140c4272a4e07b9c275bdd0692b85b3167c58b8c556328:0',
|
||||
chanId: '1582956994904850432',
|
||||
capacity: '10000000',
|
||||
localBalance: '9999817',
|
||||
remoteBalance: '0',
|
||||
commitFee: '183',
|
||||
commitWeight: '600',
|
||||
feePerKw: '253',
|
||||
unsettledBalance: '0',
|
||||
totalSatoshisSent: '0',
|
||||
totalSatoshisReceived: '0',
|
||||
numUpdates: '0',
|
||||
pendingHtlcs: [
|
||||
],
|
||||
csvDelay: 1201,
|
||||
private: false
|
||||
},
|
||||
{
|
||||
active: false,
|
||||
remotePubKey: '03c856d2dbec7454c48f311031f06bb99e3ca1ab15a9b9b35de14e139aa663b463',
|
||||
channelPoint: '12d3f818e82f448f780539c3b51616c23bc739f2b18bb8f6838200b111230d0e:0',
|
||||
chanId: '1583392401509449728',
|
||||
capacity: '3999000',
|
||||
localBalance: '2000000',
|
||||
remoteBalance: '1998817',
|
||||
commitFee: '183',
|
||||
commitWeight: '724',
|
||||
feePerKw: '253',
|
||||
unsettledBalance: '0',
|
||||
totalSatoshisSent: '0',
|
||||
totalSatoshisReceived: '0',
|
||||
numUpdates: '0',
|
||||
pendingHtlcs: [
|
||||
],
|
||||
csvDelay: 480,
|
||||
private: false
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
remotePubKey: '03c856d2dbec7454c48f311031f06bb99e3ca1ab15a9b9b35de14e139aa663b463',
|
||||
channelPoint: '6efe84b44bc9d184979f2527b73cbf0223a5549a3932e78a1460499166f2639e:0',
|
||||
chanId: '1582997676835799040',
|
||||
capacity: '15000000',
|
||||
localBalance: '14259822',
|
||||
remoteBalance: '739994',
|
||||
commitFee: '184',
|
||||
commitWeight: '724',
|
||||
feePerKw: '253',
|
||||
unsettledBalance: '0',
|
||||
totalSatoshisSent: '1500000',
|
||||
totalSatoshisReceived: '760005',
|
||||
numUpdates: '16',
|
||||
pendingHtlcs: [
|
||||
],
|
||||
csvDelay: 1802,
|
||||
private: false
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
remotePubKey: '03c856d2dbec7454c48f311031f06bb99e3ca1ab15a9b9b35de14e139aa663b463',
|
||||
channelPoint: 'c1b7045595f4f5c024af22287755b21f65e1ec7fbe11ee0181d16382a07ae58b:0',
|
||||
chanId: '1582997676835799040',
|
||||
capacity: '15000000',
|
||||
localBalance: '14259822',
|
||||
remoteBalance: '739994',
|
||||
commitFee: '184',
|
||||
commitWeight: '724',
|
||||
feePerKw: '253',
|
||||
unsettledBalance: '0',
|
||||
totalSatoshisSent: '1500000',
|
||||
totalSatoshisReceived: '760005',
|
||||
numUpdates: '16',
|
||||
pendingHtlcs: [
|
||||
],
|
||||
csvDelay: 1802,
|
||||
private: false
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function getUnspectUtxos() {
|
||||
return {
|
||||
utxos: [
|
||||
{
|
||||
address_type: 0,
|
||||
address: 'bc1qp6dxazrsju834sucudvcxy6t3tem3henkf9dfe',
|
||||
amountSat: 50000,
|
||||
pk_script: '00140e9a6e8870970f1ac398e35983134b8af3b8df33',
|
||||
outpoint: '75d342c126473f2bc26c23111b4cc4f8712532a0387d9bb70156d7efc8528efd:1',
|
||||
confirmations: 51
|
||||
},
|
||||
{
|
||||
address_type: 0,
|
||||
address: 'bc1qp6dxazrsju834sucudvcxy6t3tem3henkf9dfe',
|
||||
amountSat: 30000,
|
||||
pk_script: '00140e9a6e8870970f1ac398e35983134b8af3b8df33',
|
||||
outpoint: '75d342c126473f2bc26c23111b4cc4f8712532a0387d9bb70156d7efc8528efd:1',
|
||||
confirmations: 3307
|
||||
},
|
||||
{
|
||||
address_type: 0,
|
||||
address: 'bc1qp6dxazrsju834sucudvcxy6t3tem3henkf9dfe',
|
||||
amountSat: 20000,
|
||||
pk_script: '00140e9a6e8870970f1ac398e35983134b8af3b8df33',
|
||||
outpoint: '75d342c126473f2bc26c23111b4cc4f8712532a0387d9bb70156d7efc8528efd:1',
|
||||
confirmations: 3996
|
||||
},
|
||||
{
|
||||
address_type: 0,
|
||||
address: 'bc1qp6dxazrsju834sucudvcxy6t3tem3henkf9dfe',
|
||||
amountSat: 20000,
|
||||
pk_script: '00140e9a6e8870970f1ac398e35983134b8af3b8df33',
|
||||
outpoint: '75d342c126473f2bc26c23111b4cc4f8712532a0387d9bb70156d7efc8528efd:1',
|
||||
confirmations: 26309
|
||||
},
|
||||
{
|
||||
address_type: 1,
|
||||
address: '2NFGwqm9N9LomEh9mzQgofr1WGqkwaxPuWg',
|
||||
amountSat: 20000,
|
||||
pk_script: '00140e9a6e8870970f1ac398e35983134b8af3b8df33',
|
||||
outpoint: '75d342c126473f2bc26c23111b4cc4f8712532a0387d9bb70156d7efc8528efd:1',
|
||||
confirmations: 27335
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateAddress,
|
||||
getChannelOpen,
|
||||
getChannelClosed,
|
||||
getChannelBalance,
|
||||
getEstimateFee,
|
||||
getFeeReport,
|
||||
getForwardingEvents,
|
||||
getInfo,
|
||||
getWalletBalance,
|
||||
getOpenChannels,
|
||||
getOnChainTransaction,
|
||||
getOnChainTransactions,
|
||||
getPendingChannels,
|
||||
getManagedChannelsFile,
|
||||
getUnspectUtxos,
|
||||
};
|
@ -1,410 +0,0 @@
|
||||
/* globals reset, requester, expect, assert, Lightning */
|
||||
/* eslint-disable max-len */
|
||||
|
||||
const sinon = require('sinon');
|
||||
const proxyquire = require('proxyquire');
|
||||
const lndMocks = require('../mocks/lnd.js');
|
||||
|
||||
describe('lightning API', () => {
|
||||
let jwt;
|
||||
let bitcoindHelp;
|
||||
|
||||
before(async() => {
|
||||
reset();
|
||||
|
||||
bitcoindHelp = sinon.stub(require('bitcoind-rpc').prototype, 'help').callsFake(callback => callback(undefined, {}));
|
||||
|
||||
// TODO: expires Dec 1st, 2019
|
||||
jwt = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InRlc3QtdXNlciIsImlhdCI6MTU3NTIyNjQxMn0.N06esl2dhN1mFqn-0o4KQmmAaDW9OsHA39calpp_N9B3Ig3aXWgl064XAR9YVK0qwX7zMOnK9UrJ48KUZ-Sb4A';
|
||||
});
|
||||
|
||||
after(async() => {
|
||||
bitcoindHelp.restore();
|
||||
});
|
||||
|
||||
it('should look operational', async() => {
|
||||
|
||||
Lightning.prototype.GetInfo = sinon.stub().callsFake((_, callback) => callback(undefined, {}));
|
||||
|
||||
const status = await requester
|
||||
.get('/v1/lnd/info/status')
|
||||
.set('authorization', `jwt ${jwt}`)
|
||||
.then(res => {
|
||||
expect(res).to.have.status(200);
|
||||
expect(res).to.be.json;
|
||||
|
||||
return res.body;
|
||||
});
|
||||
assert.equal(status.operational, true);
|
||||
assert.isTrue(bitcoindHelp.called);
|
||||
assert.isTrue(Lightning.prototype.GetInfo.called);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lightningLogic', function() {
|
||||
|
||||
describe('getChannelBalance', function() {
|
||||
|
||||
it('should return channel balance', function(done) {
|
||||
|
||||
const originalChannelbalance = lndMocks.getChannelBalance();
|
||||
|
||||
const lndServiceStub = {
|
||||
'services/lnd.js': {
|
||||
getChannelBalance: () => Promise.resolve(originalChannelbalance)
|
||||
}
|
||||
};
|
||||
|
||||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub);
|
||||
|
||||
lightningLogic.getChannelBalance().then(function(channelBalance) {
|
||||
assert.equal(channelBalance, originalChannelbalance);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error', function(done) {
|
||||
|
||||
const lndServiceStub = {
|
||||
'services/lnd.js': {
|
||||
getChannelBalance: () => Promise.reject(new Error())
|
||||
}
|
||||
};
|
||||
|
||||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub);
|
||||
|
||||
lightningLogic.getChannelBalance().catch(function(error) {
|
||||
assert.isNotNull(error);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('generateAddress', function() {
|
||||
|
||||
it('should return a segwit address', function(done) {
|
||||
|
||||
const originalAddress = lndMocks.generateAddress();
|
||||
|
||||
const lndServiceStub = {
|
||||
'services/lnd.js': {
|
||||
generateAddress: () => Promise.resolve(originalAddress)
|
||||
}
|
||||
};
|
||||
|
||||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub);
|
||||
|
||||
lightningLogic.generateAddress()
|
||||
.then(function(address) {
|
||||
assert.equal(address, originalAddress);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error', function(done) {
|
||||
|
||||
const lndServiceStub = {
|
||||
'services/lnd.js': {
|
||||
generateAddress: () => Promise.reject(new Error())
|
||||
}
|
||||
};
|
||||
|
||||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub);
|
||||
|
||||
lightningLogic.generateAddress()
|
||||
.catch(function(error) {
|
||||
assert.isNotNull(error);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getChannelCount', function() {
|
||||
|
||||
it('should return channel count', function(done) {
|
||||
|
||||
const originalChannels = lndMocks.getOpenChannels();
|
||||
|
||||
const lndServiceStub = {
|
||||
'services/lnd.js': {
|
||||
getOpenChannels: () => Promise.resolve(originalChannels)
|
||||
}
|
||||
};
|
||||
|
||||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub);
|
||||
|
||||
lightningLogic.getChannelCount()
|
||||
.then(function(channelCount) {
|
||||
assert.equal(channelCount.count, originalChannels.length);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should throw an error', function(done) {
|
||||
|
||||
const lndServiceStub = {
|
||||
'services/lnd.js': {
|
||||
getOpenChannels: () => Promise.reject(new Error())
|
||||
}
|
||||
};
|
||||
|
||||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub);
|
||||
|
||||
lightningLogic.getChannelCount()
|
||||
.catch(function(error) {
|
||||
assert.isNotNull(error);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('getChannels', function() {
|
||||
it('should return a list of channels', function(done) {
|
||||
|
||||
const originalChannelList = lndMocks.getOpenChannels();
|
||||
const originalPendingChannelList = lndMocks.getPendingChannels(); // eslint-disable-line id-length
|
||||
const originalOnChainTransactions = lndMocks.getOnChainTransactions(); // eslint-disable-line id-length
|
||||
const managedChannelsFile = lndMocks.getManagedChannelsFile();
|
||||
|
||||
const lndServiceStub = {
|
||||
'services/lnd.js': {
|
||||
getOpenChannels: () => Promise.resolve(originalChannelList),
|
||||
getPendingChannels: () => Promise.resolve(originalPendingChannelList),
|
||||
getOnChainTransactions: () => Promise.resolve(originalOnChainTransactions)
|
||||
},
|
||||
'services/disk.js': {
|
||||
readJsonFile: () => Promise.resolve(managedChannelsFile)
|
||||
}
|
||||
};
|
||||
|
||||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub);
|
||||
|
||||
lightningLogic.getChannels()
|
||||
.then(function(channels) {
|
||||
assert.equal(channels.count, originalChannelList.count);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPublicUris', function() {
|
||||
|
||||
it('should return a list of uris', function(done) {
|
||||
|
||||
const originalGetInfo = lndMocks.getInfo();
|
||||
|
||||
const lndServiceStub = {
|
||||
'services/lnd.js': {
|
||||
getInfo: () => Promise.resolve(originalGetInfo)
|
||||
}
|
||||
};
|
||||
|
||||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub);
|
||||
|
||||
lightningLogic.getPublicUris()
|
||||
.then(function(uris) {
|
||||
assert.deepEqual(uris, originalGetInfo.uris);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should throw an error', function(done) {
|
||||
|
||||
const lndServiceStub = {
|
||||
'services/lnd.js': {
|
||||
getInfo: () => Promise.reject(new Error())
|
||||
}
|
||||
};
|
||||
|
||||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub);
|
||||
|
||||
lightningLogic.getPublicUris()
|
||||
.catch(function(error) {
|
||||
assert.isNotNull(error);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('getSyncStatus', function() {
|
||||
|
||||
it('should return the sync status', function(done) {
|
||||
const originalGetInfo = {
|
||||
synchedToChain: false,
|
||||
testnet: false,
|
||||
bestHeaderTimestamp: 1535905615,
|
||||
blockHeight: 1408630
|
||||
};
|
||||
|
||||
const lndServiceStub = {
|
||||
'services/lnd.js': {
|
||||
getInfo: () => Promise.resolve(originalGetInfo)
|
||||
}
|
||||
};
|
||||
|
||||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub);
|
||||
|
||||
lightningLogic.getSyncStatus()
|
||||
.then(function(status) {
|
||||
assert.property(status, 'percent');
|
||||
assert.property(status, 'knownBlockCount');
|
||||
assert.property(status, 'processedBlocks');
|
||||
assert.notEqual(status.percent, -1);
|
||||
assert.notEqual(status.processedBlocks, -1);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should return -1 if calculation is greater than 100%', function(done) {
|
||||
const invaildInfoData = {
|
||||
synchedToChain: false,
|
||||
testnet: false,
|
||||
bestHeaderTimestamp: 1845905615,
|
||||
blockHeight: 1408630
|
||||
};
|
||||
|
||||
const lndServiceStub = {
|
||||
'services/lnd.js': {
|
||||
getInfo: () => Promise.resolve(invaildInfoData)
|
||||
}
|
||||
};
|
||||
|
||||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub);
|
||||
|
||||
lightningLogic.getSyncStatus()
|
||||
.then(function(status) {
|
||||
assert.property(status, 'percent');
|
||||
assert.property(status, 'knownBlockCount');
|
||||
assert.property(status, 'processedBlocks');
|
||||
assert.equal(status.percent, -1);
|
||||
assert.equal(status.processedBlocks, -1);
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should thrown an error', function(done) {
|
||||
|
||||
const lndServiceStub = {
|
||||
'services/lnd.js': {
|
||||
getInfo: () => Promise.reject(new Error())
|
||||
}
|
||||
};
|
||||
|
||||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub);
|
||||
|
||||
lightningLogic.getSyncStatus()
|
||||
.catch(function(error) {
|
||||
assert.isNotNull(error);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWalletBalance', function() {
|
||||
|
||||
it('should return a wallet balance', function(done) {
|
||||
|
||||
const originalWalletBalance = lndMocks.getWalletBalance();
|
||||
|
||||
const lndServiceStub = {
|
||||
'services/lnd.js': {
|
||||
getWalletBalance: () => Promise.resolve(originalWalletBalance)
|
||||
}
|
||||
};
|
||||
|
||||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub);
|
||||
|
||||
lightningLogic.getWalletBalance()
|
||||
.then(function(walletBalance) {
|
||||
assert.deepEqual(walletBalance, originalWalletBalance);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should throw an error', function(done) {
|
||||
|
||||
const lndServiceStub = {
|
||||
'services/lnd.js': {
|
||||
getWalletBalance: () => Promise.reject(new Error())
|
||||
}
|
||||
};
|
||||
|
||||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub);
|
||||
|
||||
lightningLogic.getWalletBalance()
|
||||
.catch(function(error) {
|
||||
assert.isNotNull(error);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVersion', function() {
|
||||
|
||||
it('should return a version', function(done) {
|
||||
|
||||
const originalGetInfo = lndMocks.getInfo();
|
||||
|
||||
const lndServiceStub = {
|
||||
'services/lnd.js': {
|
||||
getInfo: () => Promise.resolve(originalGetInfo)
|
||||
}
|
||||
};
|
||||
|
||||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub);
|
||||
|
||||
lightningLogic.getVersion()
|
||||
.then(function(version) {
|
||||
assert.equal(version.version, '0.4.2');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should throw an error', function(done) {
|
||||
|
||||
const lndServiceStub = {
|
||||
'services/lnd.js': {
|
||||
getInfo: () => Promise.reject(new Error())
|
||||
}
|
||||
};
|
||||
|
||||
const lightningLogic = proxyquire('logic/lightning.js', lndServiceStub);
|
||||
|
||||
lightningLogic.getPublicUris()
|
||||
.catch(function(error) {
|
||||
assert.isNotNull(error);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
1
ui/.env.production
Normal file
1
ui/.env.production
Normal file
@ -0,0 +1 @@
|
||||
VUE_APP_API_BASE_URL=
|
27815
ui/package-lock.json
generated
Normal file
27815
ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
63
ui/package.json
Normal file
63
ui/package.json
Normal file
@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "umbrel-dashboard",
|
||||
"version": "0.3.29",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build --mode production",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"animate.css": "^3.7.2",
|
||||
"axios": "^0.19.2",
|
||||
"bignumber.js": "^9.0.0",
|
||||
"bootstrap-vue": "^2.11.0",
|
||||
"core-js": "^3.4.4",
|
||||
"countup.js": "^2.0.4",
|
||||
"highcharts": "^9.3.2",
|
||||
"highcharts-vue": "^1.4.0",
|
||||
"moment": "^2.24.0",
|
||||
"qrcode.vue": "^1.7.0",
|
||||
"vue": "^2.6.10",
|
||||
"vue-confetti": "^2.0.7",
|
||||
"vue-router": "^3.1.3",
|
||||
"vue-slider-component": "^3.1.2",
|
||||
"vuex": "^3.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^4.1.0",
|
||||
"@vue/cli-plugin-eslint": "^4.1.0",
|
||||
"@vue/cli-plugin-router": "^4.1.0",
|
||||
"@vue/cli-plugin-vuex": "^4.1.0",
|
||||
"@vue/cli-service": "^4.1.0",
|
||||
"@vue/eslint-config-prettier": "^5.0.0",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-plugin-prettier": "^3.1.1",
|
||||
"eslint-plugin-vue": "^5.0.0",
|
||||
"prettier": "^1.19.1",
|
||||
"sass": "^1.23.7",
|
||||
"sass-loader": "^8.0.0",
|
||||
"vue-template-compiler": "^2.6.10"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"@vue/prettier"
|
||||
],
|
||||
"rules": {
|
||||
"no-console": "off"
|
||||
},
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions"
|
||||
]
|
||||
}
|
BIN
ui/public/favicon.ico
Normal file
BIN
ui/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
ui/public/favicon.png
Normal file
BIN
ui/public/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 57 KiB |
BIN
ui/public/fonts/Inter-Black.woff
Normal file
BIN
ui/public/fonts/Inter-Black.woff
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-Black.woff2
Normal file
BIN
ui/public/fonts/Inter-Black.woff2
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-BlackItalic.woff
Normal file
BIN
ui/public/fonts/Inter-BlackItalic.woff
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-BlackItalic.woff2
Normal file
BIN
ui/public/fonts/Inter-BlackItalic.woff2
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-Bold.woff
Normal file
BIN
ui/public/fonts/Inter-Bold.woff
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-Bold.woff2
Normal file
BIN
ui/public/fonts/Inter-Bold.woff2
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-BoldItalic.woff
Normal file
BIN
ui/public/fonts/Inter-BoldItalic.woff
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-BoldItalic.woff2
Normal file
BIN
ui/public/fonts/Inter-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-ExtraBold.woff
Normal file
BIN
ui/public/fonts/Inter-ExtraBold.woff
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-ExtraBold.woff2
Normal file
BIN
ui/public/fonts/Inter-ExtraBold.woff2
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-ExtraBoldItalic.woff
Normal file
BIN
ui/public/fonts/Inter-ExtraBoldItalic.woff
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-ExtraBoldItalic.woff2
Normal file
BIN
ui/public/fonts/Inter-ExtraBoldItalic.woff2
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-ExtraLight.woff
Normal file
BIN
ui/public/fonts/Inter-ExtraLight.woff
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-ExtraLight.woff2
Normal file
BIN
ui/public/fonts/Inter-ExtraLight.woff2
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-ExtraLightItalic.woff
Normal file
BIN
ui/public/fonts/Inter-ExtraLightItalic.woff
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-ExtraLightItalic.woff2
Normal file
BIN
ui/public/fonts/Inter-ExtraLightItalic.woff2
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-Italic.woff
Normal file
BIN
ui/public/fonts/Inter-Italic.woff
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-Italic.woff2
Normal file
BIN
ui/public/fonts/Inter-Italic.woff2
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-Light.woff
Normal file
BIN
ui/public/fonts/Inter-Light.woff
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-Light.woff2
Normal file
BIN
ui/public/fonts/Inter-Light.woff2
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-LightItalic.woff
Normal file
BIN
ui/public/fonts/Inter-LightItalic.woff
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-LightItalic.woff2
Normal file
BIN
ui/public/fonts/Inter-LightItalic.woff2
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-Medium.woff
Normal file
BIN
ui/public/fonts/Inter-Medium.woff
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-Medium.woff2
Normal file
BIN
ui/public/fonts/Inter-Medium.woff2
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-MediumItalic.woff
Normal file
BIN
ui/public/fonts/Inter-MediumItalic.woff
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-MediumItalic.woff2
Normal file
BIN
ui/public/fonts/Inter-MediumItalic.woff2
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-Regular.woff
Normal file
BIN
ui/public/fonts/Inter-Regular.woff
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-Regular.woff2
Normal file
BIN
ui/public/fonts/Inter-Regular.woff2
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-SemiBold.woff
Normal file
BIN
ui/public/fonts/Inter-SemiBold.woff
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-SemiBold.woff2
Normal file
BIN
ui/public/fonts/Inter-SemiBold.woff2
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-SemiBoldItalic.woff
Normal file
BIN
ui/public/fonts/Inter-SemiBoldItalic.woff
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-SemiBoldItalic.woff2
Normal file
BIN
ui/public/fonts/Inter-SemiBoldItalic.woff2
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-Thin.woff
Normal file
BIN
ui/public/fonts/Inter-Thin.woff
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-Thin.woff2
Normal file
BIN
ui/public/fonts/Inter-Thin.woff2
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-ThinItalic.woff
Normal file
BIN
ui/public/fonts/Inter-ThinItalic.woff
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-ThinItalic.woff2
Normal file
BIN
ui/public/fonts/Inter-ThinItalic.woff2
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-italic.var.woff2
Normal file
BIN
ui/public/fonts/Inter-italic.var.woff2
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter-roman.var.woff2
Normal file
BIN
ui/public/fonts/Inter-roman.var.woff2
Normal file
Binary file not shown.
BIN
ui/public/fonts/Inter.var.woff2
Normal file
BIN
ui/public/fonts/Inter.var.woff2
Normal file
Binary file not shown.
24
ui/public/index.html
Normal file
24
ui/public/index.html
Normal file
@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<title>Bitcoin Node — Umbrel</title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.png">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but this app
|
||||
doesn't work properly without JavaScript enabled. Please enable it to
|
||||
continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
|
||||
</html>
|
143
ui/src/App.vue
Normal file
143
ui/src/App.vue
Normal file
@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div>
|
||||
<transition name="loading" mode>
|
||||
<div v-if="isIframe">
|
||||
<div
|
||||
class="d-flex flex-column align-items-center justify-content-center min-vh100 p-2"
|
||||
>
|
||||
<img alt="Umbrel" src="@/assets/icon.svg" class="mb-5 logo" />
|
||||
<span class="text-muted w-75 text-center">
|
||||
<small
|
||||
>For security reasons this app cannot be embedded in an
|
||||
iframe.</small
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<loading v-else-if="loading" :progress="loadingProgress"> </loading>
|
||||
<!-- component matched by the route will render here -->
|
||||
<router-view v-else></router-view>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/global-styles/design-system.scss";
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from "vuex";
|
||||
import Loading from "@/components/Loading";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
data() {
|
||||
return {
|
||||
isIframe: window.self !== window.top,
|
||||
loading: true,
|
||||
loadingProgress: 0,
|
||||
loadingPollInProgress: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
isApiOperational: state => {
|
||||
return state.system.api.operational;
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
//TODO: move this to the specific layout that needs this 100vh fix
|
||||
updateViewPortHeightCSS() {
|
||||
return document.documentElement.style.setProperty(
|
||||
"--vh100",
|
||||
`${window.innerHeight}px`
|
||||
);
|
||||
},
|
||||
async getLoadingStatus() {
|
||||
// Skip if previous poll in progress or if system is updating
|
||||
if (this.loadingPollInProgress || this.updating) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingPollInProgress = true;
|
||||
|
||||
// Then check if middleware api is up
|
||||
if (this.loadingProgress <= 40) {
|
||||
this.loadingProgress = 40;
|
||||
await this.$store.dispatch("system/getApi");
|
||||
if (!this.isApiOperational) {
|
||||
this.loading = true;
|
||||
this.loadingPollInProgress = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.loadingProgress = 100;
|
||||
this.loadingPollInProgress = false;
|
||||
|
||||
// Add slight delay so the progress bar makes
|
||||
// it to 100% before disappearing
|
||||
setTimeout(() => (this.loading = false), 300);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
//for 100vh consistency
|
||||
this.updateViewPortHeightCSS();
|
||||
window.addEventListener("resize", this.updateViewPortHeightCSS);
|
||||
},
|
||||
watch: {
|
||||
loading: {
|
||||
handler: function(isLoading) {
|
||||
window.clearInterval(this.loadingInterval);
|
||||
//if loading, check loading status every two seconds
|
||||
if (isLoading) {
|
||||
this.loadingInterval = window.setInterval(
|
||||
this.getLoadingStatus,
|
||||
2000
|
||||
);
|
||||
} else {
|
||||
//else check every 20s
|
||||
this.loadingInterval = window.setInterval(
|
||||
this.getLoadingStatus,
|
||||
20000
|
||||
);
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener("resize", this.updateViewPortHeightCSS);
|
||||
window.clearInterval(this.loadingInterval);
|
||||
},
|
||||
components: {
|
||||
Loading
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// Loading transitions
|
||||
|
||||
.loading-enter-active,
|
||||
.loading-leave-active {
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
.loading-enter {
|
||||
opacity: 0;
|
||||
// filter: blur(70px);
|
||||
}
|
||||
.loading-enter-to {
|
||||
opacity: 1;
|
||||
// filter: blur(0);
|
||||
}
|
||||
.loading-leave {
|
||||
opacity: 1;
|
||||
// filter: blur(0);
|
||||
}
|
||||
.loading-leave-to {
|
||||
opacity: 0;
|
||||
// filter: blur(70px);
|
||||
}
|
||||
</style>
|
7
ui/src/assets/icon-block.svg
Normal file
7
ui/src/assets/icon-block.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg width="60" height="84" viewBox="0 0 60 84" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="60" height="60" rx="30" fill="#EEEEFF"/>
|
||||
<rect x="29" y="60" width="1" height="24" fill="#EEEEFF"/>
|
||||
<path d="M39.3472 24.843C40.6806 24.0732 40.6806 22.1487 39.3472 21.3789L31.0304 16.5773C30.4116 16.2201 29.6493 16.2201 29.0305 16.5773L20.7139 21.3789C19.3805 22.1487 19.3805 24.0732 20.7138 24.843L29.0304 29.6447C29.6493 30.0019 30.4117 30.0019 31.0305 29.6447L39.3472 24.843Z" fill="#5351FB"/>
|
||||
<path d="M30.9243 42.5359C30.9243 44.0755 32.591 45.0378 33.9243 44.268L42.2443 39.4644C42.8631 39.1072 43.2443 38.4469 43.2443 37.7324V28.1218C43.2443 26.5822 41.5777 25.6199 40.2444 26.3897L31.9243 31.1931C31.3055 31.5504 30.9243 32.2106 30.9243 32.9252V42.5359Z" fill="#5351FB"/>
|
||||
<path d="M19.8164 26.3897C18.4831 25.6199 16.8164 26.5822 16.8164 28.1218V37.7324C16.8164 38.4469 17.1976 39.1072 17.8164 39.4644L26.1362 44.268C27.4695 45.0378 29.1362 44.0755 29.1362 42.5359V32.9252C29.1362 32.2106 28.755 31.5504 28.1362 31.1931L19.8164 26.3897Z" fill="#5351FB"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
14
ui/src/assets/icon.svg
Normal file
14
ui/src/assets/icon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 259 KiB |
21
ui/src/assets/umbrel-qr-icon.svg
Normal file
21
ui/src/assets/umbrel-qr-icon.svg
Normal file
@ -0,0 +1,21 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="68" height="68" fill="url(#paint0_linear)"/>
|
||||
<rect x="4" y="5" width="59" height="59" rx="10" fill="url(#paint1_linear)"/>
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M33.8281 54.5602C34.4842 54.5602 35.2691 54.4387 36.1829 54.1958C39.268 53.0361 40.8105 50.8028 40.8105 47.4961V38.0342C40.8105 37.854 40.7988 37.6972 40.7754 37.564C40.7754 37.5405 40.7734 37.5249 40.7695 37.517C40.7656 37.5092 40.7559 37.4935 40.7402 37.47C40.42 36.6629 39.9123 36.2593 39.2172 36.2593H39.1235L38.9243 36.2711H38.8189C38.7955 36.2711 38.7681 36.2829 38.7369 36.3064C37.9324 36.6198 37.5302 37.2819 37.5302 38.2928V47.9897C37.5302 48.5148 37.3974 49.028 37.1319 49.5295C36.3821 50.7127 35.2691 51.3043 33.793 51.3043C32.6761 51.3043 31.731 50.9556 30.9578 50.2582C30.3252 49.7411 30.001 48.789 29.9854 47.4021L29.962 37.8696C29.962 37.7834 29.9542 37.6933 29.9386 37.5993C29.9386 37.5679 29.9366 37.5464 29.9327 37.5346C29.9288 37.5229 29.9151 37.5013 29.8917 37.47C29.5637 36.6472 29.0443 36.2358 28.3335 36.2358C28.107 36.2593 27.9547 36.2868 27.8766 36.3181C27.0956 36.6316 26.7051 37.1605 26.7051 37.9049L26.7402 47.7899C26.748 49.2631 27.1542 50.5913 27.9586 51.7745C29.4582 53.6316 31.3991 54.5602 33.7812 54.5602H33.8281Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.6033 30.1349C20.8134 30.1349 19.7168 30.66 19.0195 31.4853C18.4482 32.1614 17.4389 32.2449 16.765 31.6718C16.0911 31.0987 16.0079 30.086 16.5791 29.4099C18.042 27.6786 20.148 26.9253 22.6033 26.9253C24.6666 26.9253 26.5577 27.451 28.236 28.5102C29.9748 27.4762 31.8069 26.9253 33.7155 26.9253C35.5815 26.9253 37.3433 27.4523 38.9809 28.4549C40.4426 27.4675 42.1971 27.0138 44.152 27.0138C46.6039 27.0138 48.7636 27.7261 50.5187 29.2259C51.1912 29.8006 51.272 30.8135 50.6992 31.4882C50.1263 32.1629 49.1168 32.244 48.4442 31.6693C47.3456 30.7304 45.9523 30.2234 44.152 30.2234C42.4514 30.2234 41.229 30.6748 40.3359 31.4483C39.6372 32.0533 38.6166 32.1 37.866 31.5613L37.8378 31.541C36.4855 30.5779 35.1213 30.1349 33.7155 30.1349C32.2842 30.1349 30.831 30.5945 29.3312 31.6126C28.627 32.0906 27.6985 32.0696 27.0165 31.5602L26.967 31.5232C25.7118 30.6014 24.2765 30.1349 22.6033 30.1349Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M42.586 21.042C40.2533 19.0121 37.2459 17.9809 33.4258 18.0546C29.5878 18.1286 26.6072 19.2351 24.3451 21.2654C22.0639 23.3128 20.3625 26.4356 19.3578 30.8083C19.1594 31.672 18.3007 32.2107 17.4398 32.0116C16.579 31.8125 16.042 30.951 16.2404 30.0874C17.3431 25.288 19.2947 21.4918 22.2121 18.8733C25.1487 16.2376 28.9124 14.9314 33.3643 14.8456C37.8342 14.7594 41.6528 15.9813 44.6821 18.6173C47.6852 21.2305 49.762 25.1021 51.0304 30.0479C51.2505 30.9063 50.7354 31.7812 49.8799 32.002C49.0243 32.2229 48.1523 31.7061 47.9322 30.8477C46.7664 26.3019 44.9451 23.0948 42.586 21.042Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="0" y1="0" x2="68" y2="68" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="4" y1="5" x2="63" y2="64" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#9289F4"/>
|
||||
<stop offset="1" stop-color="#5351FB"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0">
|
||||
<path d="M15.7998 14.4399H51.7898V54.5599H15.7998V14.4399Z" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 3.3 KiB |
460
ui/src/components/Blockchain.vue
Normal file
460
ui/src/components/Blockchain.vue
Normal file
@ -0,0 +1,460 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="blockchain-container">
|
||||
<div v-if="blocks.length">
|
||||
<!-- transitions for blocks -->
|
||||
<transition-group
|
||||
name="blockchain"
|
||||
mode="out-in"
|
||||
tag="ul"
|
||||
:duration="5000"
|
||||
>
|
||||
<li
|
||||
href="#"
|
||||
class="flex-column align-items-start px-3 px-lg-4 blockchain-block"
|
||||
v-for="block in blocks"
|
||||
:key="block.height"
|
||||
>
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<div class="d-flex">
|
||||
<div class="blockchain-block-icon">
|
||||
<div class="blockchain-block-icon-cube">
|
||||
<span class="edge top">
|
||||
<span class="inside"></span>
|
||||
</span>
|
||||
<span class="edge right">
|
||||
<span class="inside"></span>
|
||||
</span>
|
||||
<span class="edge bottom">
|
||||
<span class="inside"></span>
|
||||
</span>
|
||||
<span class="edge left">
|
||||
<span class="inside"></span>
|
||||
</span>
|
||||
<span class="edge front">
|
||||
<span class="inside"></span>
|
||||
</span>
|
||||
<span class="edge back">
|
||||
<span class="inside"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="blockchain-block-icon-chainlink"></div>
|
||||
<div class="blockchain-block-icon-bg"></div>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<h6 class="mb-1 font-weight-normal">
|
||||
Block {{ block.height.toLocaleString() }}
|
||||
</h6>
|
||||
<small class="text-muted" v-if="block.numTransactions">
|
||||
{{ block.numTransactions.toLocaleString() }}
|
||||
transaction{{ block.numTransactions !== 1 ? "s" : "" }}
|
||||
</small>
|
||||
<!-- <small class="text-muted" v-if="block.size">
|
||||
<span>• {{ Math.round(block.size / 1000) }} KB</span>
|
||||
</small>-->
|
||||
</div>
|
||||
</div>
|
||||
<small
|
||||
class="text-muted align-self-center text-right blockchain-block-timestamp"
|
||||
v-if="block.time"
|
||||
:title="blockReadableTime(block.time)"
|
||||
>{{ blockTime(block.time) }}</small
|
||||
>
|
||||
</div>
|
||||
</li>
|
||||
</transition-group>
|
||||
</div>
|
||||
<div v-else>
|
||||
<ul>
|
||||
<li
|
||||
href="#"
|
||||
class="flex-column align-items-start px-3 px-lg-4 blockchain-block"
|
||||
v-for="(fake, index) in [1, 2, 3, 4]"
|
||||
:key="index"
|
||||
>
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<div class="d-flex">
|
||||
<div
|
||||
class="blockchain-block-icon blockchain-block-icon-loading"
|
||||
>
|
||||
<svg
|
||||
width="28"
|
||||
height="30"
|
||||
viewBox="0 0 28 30"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M23.3472 8.84298C24.6806 8.07317 24.6806 6.14865 23.3472 5.37886L15.0304 0.577324C14.4116 0.220073 13.6493 0.220077 13.0305 0.577333L4.71387 5.37887C3.38053 6.14866 3.38052 8.07316 4.71384 8.84297L13.0304 13.6447C13.6493 14.0019 14.4117 14.0019 15.0305 13.6447L23.3472 8.84298Z"
|
||||
fill="#D3D5DC"
|
||||
/>
|
||||
<path
|
||||
d="M14.9243 26.5359C14.9243 28.0755 16.591 29.0378 17.9243 28.268L26.2443 23.4644C26.8631 23.1072 27.2443 22.4469 27.2443 21.7324V12.1218C27.2443 10.5822 25.5777 9.61992 24.2444 10.3897L15.9243 15.1931C15.3055 15.5504 14.9243 16.2106 14.9243 16.9252V26.5359Z"
|
||||
fill="#D3D5DC"
|
||||
/>
|
||||
<path
|
||||
d="M3.8164 10.3897C2.48306 9.61995 0.816406 10.5822 0.816406 12.1218V21.7324C0.816406 22.4469 1.1976 23.1072 1.81639 23.4644L10.1362 28.268C11.4695 29.0378 13.1362 28.0755 13.1362 26.5359V16.9252C13.1362 16.2106 12.755 15.5504 12.1362 15.1931L3.8164 10.3897Z"
|
||||
fill="#D3D5DC"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div class="blockchain-block-icon-chainlink"></div>
|
||||
<div class="blockchain-block-icon-bg"></div>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<span
|
||||
class="d-block loading-placeholder mb-1"
|
||||
style="width: 140px;"
|
||||
></span>
|
||||
<span
|
||||
class="d-block loading-placeholder loading-placeholder-sm"
|
||||
style="width: 80px"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="loading-placeholder loading-placeholder-sm align-self-center text-right"
|
||||
style="width: 40px"
|
||||
></span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from "moment";
|
||||
import { mapState } from "vuex";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
polling: null,
|
||||
pollInProgress: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
syncPercent: state => state.bitcoin.percent,
|
||||
blocks: state => state.bitcoin.blocks
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
async fetchBlocks() {
|
||||
//prevent multiple polls if previous poll already in progress
|
||||
if (this.pollInProgress) {
|
||||
return;
|
||||
}
|
||||
this.pollInProgress = true;
|
||||
await this.$store.dispatch("bitcoin/getBlocks");
|
||||
this.pollInProgress = false;
|
||||
},
|
||||
poller(syncPercent) {
|
||||
window.clearInterval(this.polling);
|
||||
//if syncing, fetch blocks every second
|
||||
if (Number(syncPercent) !== 100) {
|
||||
this.polling = window.setInterval(this.fetchBlocks, 1000);
|
||||
} else {
|
||||
//else, slow down and fetch blocks every minute
|
||||
this.polling = window.setInterval(this.fetchBlocks, 60 * 1000);
|
||||
}
|
||||
},
|
||||
blockTime(timestamp) {
|
||||
const minedAt = timestamp * 1000;
|
||||
//sometimes the block can have a timestamp with a few seconds in the future compared to browser's time
|
||||
if (moment(minedAt).isBefore(moment())) {
|
||||
return moment(minedAt).fromNow();
|
||||
} else {
|
||||
return "just now";
|
||||
}
|
||||
},
|
||||
blockReadableTime(timestamp) {
|
||||
return moment(timestamp * 1000).format("MMMM D, h:mm:ss a");
|
||||
}
|
||||
},
|
||||
created() {
|
||||
//immediately fetch blocks on first load
|
||||
this.fetchBlocks();
|
||||
|
||||
//then start polling
|
||||
this.poller(this.syncPercent);
|
||||
},
|
||||
watch: {
|
||||
syncPercent(newPercent) {
|
||||
// reset polling time depending upon sync %
|
||||
this.poller(newPercent);
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.clearInterval(this.polling);
|
||||
},
|
||||
props: {
|
||||
numBlocks: {
|
||||
type: Number,
|
||||
default: 5
|
||||
}
|
||||
},
|
||||
|
||||
components: {}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.blockchain-container {
|
||||
position: relative;
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 1rem 0;
|
||||
margin: 0;
|
||||
// max-height: 18rem;
|
||||
height: 25rem;
|
||||
overflow: hidden;
|
||||
// overflow-y: scroll;
|
||||
// -webkit-overflow-scrolling: touch; //momentum scroll on iOS
|
||||
}
|
||||
//bottom fade
|
||||
&:before {
|
||||
//nice faded white so the discarded blocks don't abruptly cut off
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 2rem;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
rgba(255, 255, 255, 0),
|
||||
rgba(255, 255, 255, 1)
|
||||
);
|
||||
}
|
||||
|
||||
//top fade
|
||||
&:after {
|
||||
//nice faded white so the discarded blocks don't abruptly cut off
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 2rem;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
background-image: linear-gradient(
|
||||
to top,
|
||||
rgba(255, 255, 255, 0),
|
||||
rgba(255, 255, 255, 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
.blockchain-block {
|
||||
padding: 0 0 2rem 0;
|
||||
}
|
||||
.blockchain-block-icon {
|
||||
margin-right: 1rem;
|
||||
position: relative;
|
||||
height: 4rem;
|
||||
width: 4rem;
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.blockchain-block-icon-chainlink {
|
||||
position: absolute;
|
||||
height: 4rem;
|
||||
width: 2px;
|
||||
background: #eeeeff;
|
||||
top: 50%;
|
||||
left: calc(50% - 1px);
|
||||
}
|
||||
|
||||
.blockchain-block-icon-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #eeeeff;
|
||||
z-index: 0;
|
||||
border-radius: 100%;
|
||||
}
|
||||
&.blockchain-block-icon-loading {
|
||||
.blockchain-block-icon-bg {
|
||||
background: #ffffff;
|
||||
box-shadow: inset 0px 4px 14px rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
.blockchain-block-icon-chainlink {
|
||||
background: #ededf0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.blockchain-block {
|
||||
transition: all 0.6s ease-in-out;
|
||||
}
|
||||
.blockchain-block-icon {
|
||||
.blockchain-block-icon-bg {
|
||||
transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.blockchain-block-icon-cube {
|
||||
$cube-size: 22px;
|
||||
|
||||
transform-style: preserve-3d;
|
||||
position: absolute;
|
||||
width: $cube-size;
|
||||
height: $cube-size;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 1;
|
||||
margin-left: -($cube-size * 0.5);
|
||||
margin-top: -($cube-size * 0.5);
|
||||
|
||||
transform: rotateX(-40deg) rotateY(45deg) rotateZ(0deg);
|
||||
|
||||
.edge {
|
||||
width: $cube-size;
|
||||
height: $cube-size;
|
||||
line-height: $cube-size;
|
||||
text-align: center;
|
||||
box-shadow: inset 0px 0px 0px 1px #eeeeff;
|
||||
background: #eeeeff;
|
||||
display: block;
|
||||
position: absolute;
|
||||
.inside {
|
||||
position: absolute;
|
||||
top: $cube-size * 0.1;
|
||||
left: $cube-size * 0.1;
|
||||
width: $cube-size * 0.8;
|
||||
height: $cube-size * 0.8;
|
||||
background: #5351fb;
|
||||
border-radius: $cube-size * 0.2;
|
||||
}
|
||||
&.top {
|
||||
transform: rotate3d(1, 0, 0, 90deg);
|
||||
margin-top: -($cube-size * 0.5);
|
||||
}
|
||||
&.right {
|
||||
transform: rotate3d(0, 1, 0, 90deg);
|
||||
margin-left: $cube-size * 0.5;
|
||||
}
|
||||
&.bottom {
|
||||
transform: rotate3d(1, 0, 0, -90deg);
|
||||
margin-top: $cube-size * 0.5;
|
||||
}
|
||||
&.left {
|
||||
transform: rotate3d(0, 1, 0, -90deg);
|
||||
margin-left: -($cube-size * 0.5);
|
||||
}
|
||||
&.front {
|
||||
transform: translateZ($cube-size * 0.5);
|
||||
}
|
||||
&.back {
|
||||
transform: translateZ(-($cube-size * 0.5)) rotate3d(1, 0, 0, 180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.blockchain-block-timestamp {
|
||||
position: relative;
|
||||
&:before,
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
min-width: 80px;
|
||||
opacity: 0;
|
||||
}
|
||||
&:before {
|
||||
content: "● Validated";
|
||||
color: #00cd98;
|
||||
}
|
||||
&:after {
|
||||
content: "Validating...";
|
||||
color: #b1b5b9;
|
||||
}
|
||||
}
|
||||
|
||||
//animations
|
||||
|
||||
.blockchain-enter {
|
||||
opacity: 0;
|
||||
transform: translateY(2rem);
|
||||
.blockchain-block-icon {
|
||||
.blockchain-block-icon-bg {
|
||||
transform: scale(0);
|
||||
background: #5351fb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.blockchain-enter-active {
|
||||
.blockchain-block-icon-cube {
|
||||
animation: spin-cube 5s forwards ease;
|
||||
}
|
||||
.blockchain-block-timestamp {
|
||||
&:after {
|
||||
opacity: 1;
|
||||
animation: slide-up 0.4s forwards ease;
|
||||
animation-delay: 3s;
|
||||
}
|
||||
&:before {
|
||||
opacity: 1;
|
||||
animation: slide-up 0.4s forwards ease;
|
||||
animation-delay: 4.6s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.blockchain-enter-to {
|
||||
opacity: 1;
|
||||
.blockchain-block-icon {
|
||||
.blockchain-block-icon-bg {
|
||||
transform: scale(1);
|
||||
background: #eeeeff;
|
||||
}
|
||||
}
|
||||
}
|
||||
.blockchain-leave {
|
||||
opacity: 1;
|
||||
}
|
||||
.blockchain-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(2rem);
|
||||
}
|
||||
|
||||
.blockchain-leave-active {
|
||||
// position: absolute;
|
||||
}
|
||||
|
||||
@keyframes spin-cube {
|
||||
0% {
|
||||
transform: rotateX(-40deg) rotateY(45deg) rotateZ(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotateX(-40deg) rotateY(945deg) rotateZ(540deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-up {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-10px) rotateX(30deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
114
ui/src/components/CardWidget.vue
Normal file
114
ui/src/components/CardWidget.vue
Normal file
@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<b-card
|
||||
header-tag="header"
|
||||
footer-tag="footer"
|
||||
no-body
|
||||
class="mb-4 card-custom"
|
||||
>
|
||||
<div class="card-custom-loading-bar" v-if="loading"></div>
|
||||
<!-- <template v-slot:header></template> -->
|
||||
<div>
|
||||
<div class="card-custom-header py-4 px-3 px-lg-4" v-if="header">
|
||||
<div class="d-flex w-100 justify-content-between align-items-center">
|
||||
<h6 class="mb-0 font-weight-normal text-muted">{{ header }}</h6>
|
||||
<status
|
||||
v-if="!!status"
|
||||
:variant="status.variant"
|
||||
:blink="!!status.blink"
|
||||
>{{ status.text }}</status
|
||||
>
|
||||
<!-- Only render this div if either there's a menu or a -->
|
||||
<!-- header on the right, else it causes spacing issues -->
|
||||
<div
|
||||
v-if="
|
||||
(!!$slots['header-right'] && !!$slots['header-right'][0]) ||
|
||||
(!!$slots['menu'] && !!$slots['menu'][0])
|
||||
"
|
||||
>
|
||||
<slot name="header-right"></slot>
|
||||
<b-dropdown
|
||||
variant="link"
|
||||
toggle-class="text-decoration-none p-0"
|
||||
no-caret
|
||||
right
|
||||
class="ml-2"
|
||||
>
|
||||
<template v-slot:button-content>
|
||||
<svg
|
||||
width="18"
|
||||
height="4"
|
||||
viewBox="0 0 18 4"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2 4C3.10457 4 4 3.10457 4 2C4 0.89543 3.10457 0 2 0C0.89543 0 0 0.89543 0 2C0 3.10457 0.89543 4 2 4Z"
|
||||
fill="#6c757d"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M9 4C10.1046 4 11 3.10457 11 2C11 0.89543 10.1046 0 9 0C7.89543 0 7 0.89543 7 2C7 3.10457 7.89543 4 9 4Z"
|
||||
fill="#6c757d"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M16 4C17.1046 4 18 3.10457 18 2C18 0.89543 17.1046 0 16 0C14.8954 0 14 0.89543 14 2C14 3.10457 14.8954 4 16 4Z"
|
||||
fill="#6c757d"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<slot name="menu"></slot>
|
||||
</b-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-custom-body">
|
||||
<div class="card-app-info px-3 px-lg-4" v-if="title || subTitle">
|
||||
<div class="d-flex w-100 justify-content-between mb-4">
|
||||
<div>
|
||||
<div>
|
||||
<h3 v-if="title" class="mb-1">{{ title }}</h3>
|
||||
<h3 class="mb-1" v-else>
|
||||
<slot name="title"></slot>
|
||||
</h3>
|
||||
<p class="text-muted mb-0" v-if="subTitle">{{ subTitle }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<img :alt="header" :src="require(`@/assets/${icon}`)" v-if="icon" />
|
||||
</div>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <template v-slot:footer></template> -->
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Status from "@/components/Utility/Status";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
props: {
|
||||
header: String,
|
||||
status: Object, // {text, variant, blink}
|
||||
title: String,
|
||||
subTitle: String,
|
||||
icon: String,
|
||||
loading: Boolean
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
components: {
|
||||
Status
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user