Umbrel support (#141)

* Don't require BTC core connection
* Docker: Don't clone, but use local repo
* Add GitHub workflows
* Allow to overwrite hostname
* chore: update dependencies
* Get tor URL from env
This commit is contained in:
Aaron Dewes 2021-02-24 16:17:22 +01:00 committed by GitHub
parent 76b289b652
commit f0493d595f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 8369 additions and 1001 deletions

View File

@ -3,7 +3,7 @@
"plugins": [
"prettier"
],
"extends": ["prettier"],
"extends": ["plugin:prettier/recommended"],
"rules": {
"prettier/prettier": [
"warn",

54
.github/workflows/push.yml vendored Normal file
View File

@ -0,0 +1,54 @@
name: Build on push
on:
push:
branches:
- master
env:
DOCKER_CLI_EXPERIMENTAL: enabled
jobs:
build:
name: Build image
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
steps:
- name: Checkout project
uses: actions/checkout@v2
- name: Set env variables
run: echo "BRANCH=$(echo ${GITHUB_REF#refs/heads/} | sed 's/\//-/g')" >> $GITHUB_ENV
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
id: qemu
- 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: Run Docker buildx
run: |
docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache" \
--platform ${{matrix.platform}} \
--tag ${{ secrets.DOCKER_CONTAINER_USERNAME }}/lndhub:$BRANCH \
--output "type=registry" ./

62
.github/workflows/tag.yml vendored Normal file
View File

@ -0,0 +1,62 @@
name: Build on push
on:
push:
tags:
- v[0-9]+.[0-9]+.[0-9]+
- v[0-9]+.[0-9]+.[0-9]+-*
env:
DOCKER_CLI_EXPERIMENTAL: enabled
jobs:
build:
name: Build image
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
steps:
- name: Checkout project
uses: actions/checkout@v2
- name: Set env variables
run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
id: qemu
- 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: Run Docker buildx
run: |
docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache" \
--platform ${{matrix.platform}} \
--tag ${{ secrets.DOCKER_CONTAINER_USERNAME }}/lndhub:$TAG \
--output "type=registry" ./
- 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 ${{matrix.platform}} \
--tag ${{ secrets.DOCKER_CONTAINER_USERNAME }}/lndhub:latest \
--output "type=registry" ./

View File

@ -6,19 +6,26 @@ RUN adduser --disabled-password \
--gecos "" \
"lndhub"
FROM node:buster-slim AS builder
FROM node:12-buster-slim AS builder
# These packages are required for building LNDHub
RUN apt-get update && apt-get -y install git python3
# TODO: Switch to official images once my PR is merged
RUN git clone https://github.com/AaronDewes/LndHub.git -b update-dependencies /lndhub
RUN apt-get update && apt-get -y install python3
WORKDIR /lndhub
# Copy 'package-lock.json' and 'package.json'
COPY package.json package-lock.json ./
# Install dependencies
RUN npm i
FROM node:buster-slim
# Copy project files and folders to the current working directory
COPY . .
# Delete git data as it's not needed inside the container
RUN rm -rf .git
FROM node:12-buster-slim
# Create a specific user so LNDHub doesn't run as root
COPY --from=perms /etc/group /etc/passwd /etc/shadow /etc/
@ -26,12 +33,8 @@ COPY --from=perms /etc/group /etc/passwd /etc/shadow /etc/
# Copy LNDHub with installed modules from builder
COPY --from=builder /lndhub /lndhub
# Delete git data as it's not needed inside the container
RUN rm -rf .git
# Create logs folder and ensure permissions are set correctly
RUN mkdir /lndhub/logs && chown -R lndhub:lndhub /lndhub
USER lndhub
ENV PORT=3000

View File

@ -2,6 +2,10 @@
const config = require('./config');
let jayson = require('jayson/promise');
let url = require('url');
if (config.bitcoind) {
let rpc = url.parse(config.bitcoind.rpc);
rpc.timeout = 15000;
module.exports = jayson.client.http(rpc);
} else {
module.exports = {};
}

79
btc-decoder.js Normal file
View File

@ -0,0 +1,79 @@
const bitcoin = require('bitcoinjs-lib');
const classify = require('bitcoinjs-lib/src/classify');
const decodeFormat = (tx) => ({
txid: tx.getId(),
version: tx.version,
locktime: tx.locktime,
});
const decodeInput = function (tx) {
const result = [];
tx.ins.forEach(function (input, n) {
result.push({
txid: input.hash.reverse().toString('hex'),
n: input.index,
script: bitcoin.script.toASM(input.script),
sequence: input.sequence,
});
});
return result;
};
const decodeOutput = function (tx, network) {
const format = function (out, n, network) {
const vout = {
satoshi: out.value,
value: (1e-8 * out.value).toFixed(8),
n: n,
scriptPubKey: {
asm: bitcoin.script.toASM(out.script),
hex: out.script.toString('hex'),
type: classify.output(out.script),
addresses: [],
},
};
switch (vout.scriptPubKey.type) {
case 'pubkeyhash':
case 'scripthash':
vout.scriptPubKey.addresses.push(bitcoin.address.fromOutputScript(out.script, network));
break;
case 'witnesspubkeyhash':
case 'witnessscripthash':
const data = bitcoin.script.decompile(out.script)[1];
vout.scriptPubKey.addresses.push(bitcoin.address.toBech32(data, 0, network.bech32));
break;
}
return vout;
};
const result = [];
tx.outs.forEach(function (out, n) {
result.push(format(out, n, network));
});
return result;
};
class TxDecoder {
constructor(rawTx, network = bitcoin.networks.bitcoin) {
this.tx = bitcoin.Transaction.fromHex(rawTx);
this.format = decodeFormat(this.tx);
this.inputs = decodeInput(this.tx);
this.outputs = decodeOutput(this.tx, network);
}
decode() {
const result = {};
const self = this;
Object.keys(self.format).forEach(function (key) {
result[key] = self.format[key];
});
result.outputs = self.outputs;
result.inputs = self.inputs;
return result;
}
}
module.exports.decodeRawHex = (rawTx, network = bitcoin.networks.bitcoin) => {
return new TxDecoder(rawTx, network).decode();
};

View File

@ -3,6 +3,8 @@ import { Lock } from './Lock';
var crypto = require('crypto');
var lightningPayReq = require('bolt11');
import { BigNumber } from 'bignumber.js';
import { decodeRawHex } from '../btc-decoder';
const config = require('../config');
// static cache:
let _invoice_ispaid_cache = {};
@ -118,7 +120,7 @@ export class User {
self._lightning.newAddress({ type: 0 }, async function (err, response) {
if (err) return reject('LND failure when trying to generate new address');
await self.addAddress(response.address);
self._bitcoindrpc.request('importaddress', [response.address, response.address, false]);
if (config.bitcoind) self._bitcoindrpc.request('importaddress', [response.address, response.address, false]);
resolve();
});
});
@ -126,7 +128,7 @@ export class User {
async watchAddress(address) {
if (!address) return;
return this._bitcoindrpc.request('importaddress', [address, address, false]);
if (config.bitcoind) return this._bitcoindrpc.request('importaddress', [address, address, false]);
}
/**
@ -304,7 +306,10 @@ export class User {
_invoice_ispaid_cache[invoice.payment_hash] = paymentHashPaidAmountSat;
}
invoice.amt = (paymentHashPaidAmountSat && parseInt(paymentHashPaidAmountSat) > decoded.satoshis) ? parseInt(paymentHashPaidAmountSat) : decoded.satoshis;
invoice.amt =
paymentHashPaidAmountSat && parseInt(paymentHashPaidAmountSat) > decoded.satoshis
? parseInt(paymentHashPaidAmountSat)
: decoded.satoshis;
invoice.expire_time = 3600 * 24;
// ^^^default; will keep for now. if we want to un-hardcode it - it should be among tags (`expire_time`)
invoice.timestamp = decoded.timestamp;
@ -326,12 +331,7 @@ export class User {
* @returns {Promise<Array>}
*/
async getTxs() {
let addr = await this.getAddress();
if (!addr) {
await this.generateAddress();
addr = await this.getAddress();
}
if (!addr) throw new Error('cannot get transactions: no onchain address assigned to user');
const addr = await this.getOrGenerateAddress();
let txs = await this._listtransactions();
txs = txs.result;
let result = [];
@ -402,9 +402,10 @@ export class User {
}
try {
let ret = { result: [] };
if (config.bitcoind) {
let txs = await this._bitcoindrpc.request('listtransactions', ['*', 100500, 0, true]);
// now, compacting response a bit
let ret = { result: [] };
for (const tx of txs.result) {
ret.result.push({
category: tx.category,
@ -414,6 +415,10 @@ export class User {
time: tx.time,
});
}
} else {
let txs = await this._getChainTransactions();
ret.result.push(...txs);
}
_listtransactions_cache = JSON.stringify(ret);
_listtransactions_cache_expiry_ts = +new Date() + 5 * 60 * 1000; // 5 min
this._redis.set('listtransactions', _listtransactions_cache);
@ -426,18 +431,42 @@ export class User {
}
}
async _getChainTransactions() {
return new Promise((resolve, reject) => {
this._lightning.getTransactions({}, (err, data) => {
if (err) return reject(err);
const { transactions } = data;
const outTxns = [];
// on lightning incoming transactions have no labels
// for now filter out known labels to reduce transactions
transactions
.filter((tx) => tx.label !== 'external' && !tx.label.includes('openchannel'))
.map((tx) => {
const decodedTx = decodeRawHex(tx.raw_tx_hex);
decodedTx.outputs.forEach((vout) =>
outTxns.push({
// mark all as received, since external is filtered out
category: 'receive',
confirmations: tx.num_confirmations,
amount: Number(vout.value),
address: vout.scriptPubKey.addresses[0],
time: tx.time_stamp,
}),
);
});
resolve(outTxns);
});
});
}
/**
* Returning onchain txs for user's address that are less than 3 confs
*
* @returns {Promise<Array>}
*/
async getPendingTxs() {
let addr = await this.getAddress();
if (!addr) {
await this.generateAddress();
addr = await this.getAddress();
}
if (!addr) throw new Error('cannot get transactions: no onchain address assigned to user');
const addr = await this.getOrGenerateAddress();
let txs = await this._listtransactions();
txs = txs.result;
let result = [];
@ -556,6 +585,16 @@ export class User {
return result;
}
async getOrGenerateAddress() {
let addr = await this.getAddress();
if (!addr) {
await this.generateAddress();
addr = await this.getAddress();
}
if (!addr) throw new Error('cannot get transactions: no onchain address assigned to user');
return addr;
}
_hash(string) {
return crypto.createHash('sha256').update(string).digest().toString('hex');
}

View File

@ -4,6 +4,7 @@ const config = require('../config');
let express = require('express');
let router = express.Router();
let logger = require('../utils/logger');
const MIN_BTC_BLOCK = 670000;
console.log('using config', JSON.stringify(config));
var Redis = require('ioredis');
@ -19,9 +20,10 @@ let lightning = require('../lightning');
let identity_pubkey = false;
// ###################### SMOKE TESTS ########################
if (config.bitcoind) {
bitcoinclient.request('getblockchaininfo', false, function (err, info) {
if (info && info.result && info.result.blocks) {
if (info.result.chain === 'mainnet' && info.result.blocks < 550000) {
if (info.result.chain === 'mainnet' && info.result.blocks < MIN_BTC_BLOCK && !config.forceStart) {
console.error('bitcoind is not caught up');
process.exit(1);
}
@ -30,6 +32,7 @@ bitcoinclient.request('getblockchaininfo', false, function (err, info) {
process.exit(2);
}
});
}
lightning.getInfo({}, function (err, info) {
if (err) {
@ -39,7 +42,7 @@ lightning.getInfo({}, function (err, info) {
}
if (info) {
console.info(info);
if (!info.synced_to_chain) {
if (!info.synced_to_chain && !config.forceStart) {
console.error('lnd not synced');
// process.exit(4);
}
@ -70,7 +73,10 @@ const subscribeInvoicesCallCallback = async function (response) {
return;
}
let invoice = new Invo(redis, bitcoinclient, lightning);
await invoice._setIsPaymentHashPaidInDatabase(LightningInvoiceSettledNotification.hash, LightningInvoiceSettledNotification.amt_paid_sat || 1);
await invoice._setIsPaymentHashPaidInDatabase(
LightningInvoiceSettledNotification.hash,
LightningInvoiceSettledNotification.amt_paid_sat || 1,
);
const user = new User(redis, bitcoinclient, lightning);
user._userid = await user.getUseridByPaymentHash(LightningInvoiceSettledNotification.hash);
await user.clearBalanceCache();

View File

@ -1,10 +1,10 @@
let express = require('express');
let router = express.Router();
let fs = require('fs');
let mustache = require('mustache');
let lightning = require('../lightning');
let logger = require('../utils/logger');
var qr = require('qr-image');
const express = require('express');
const router = express.Router();
const fs = require('fs');
const mustache = require('mustache');
const lightning = require('../lightning');
const logger = require('../utils/logger');
const qr = require('qr-image');
let lightningGetInfo = {};
let lightningListChannels = {};
@ -92,7 +92,11 @@ router.get('/', function (req, res) {
});
router.get('/qr', function (req, res) {
const url = "bluewallet:setlndhuburl?url=" + encodeURIComponent(req.protocol + '://' + req.headers.host);
let host = req.headers.host;
if (process.env.TOR_URL) {
host = process.env.TOR_URL;
}
const url = 'bluewallet:setlndhuburl?url=' + encodeURIComponent(req.protocol + '://' + host);
var code = qr.image(url, { type: 'png' });
res.setHeader('Content-type', 'image/png');
code.pipe(res);

View File

@ -8,7 +8,7 @@ const loaderOptions = {
longs: String,
enums: String,
defaults: true,
oneofs: true
oneofs: true,
};
const packageDefinition = protoLoader.loadSync('rpc.proto', loaderOptions);
var lnrpc = grpc.loadPackageDefinition(packageDefinition).lnrpc;

8904
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{
"name": "LndHub",
"name": "lndhub",
"version": "1.2.2",
"description": "",
"main": "index.js",
@ -12,24 +12,25 @@
"author": "Igor Korsakov <overtorment@gmail.com>",
"license": "MIT",
"dependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/eslint-parser": "^7.12.1",
"@babel/node": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/register": "^7.12.10",
"@grpc/grpc-js": "^1.2.2",
"@babel/cli": "^7.13.0",
"@babel/core": "^7.13.1",
"@babel/eslint-parser": "^7.13.4",
"@babel/node": "^7.13.0",
"@babel/preset-env": "^7.13.5",
"@babel/register": "^7.13.0",
"@grpc/grpc-js": "^1.2.8",
"@grpc/proto-loader": "^0.5.6",
"bignumber.js": "^9.0.1",
"bitcoinjs-lib": "^5.2.0",
"bolt11": "^1.2.7",
"core-js": "^3.8.1",
"eslint": "^7.19.0",
"eslint-config-prettier": "^7.1.0",
"eslint-plugin-prettier": "^3.3.0",
"core-js": "^3.9.0",
"eslint": "^7.20.0",
"eslint-config-prettier": "^8.0.0",
"eslint-plugin-prettier": "^3.3.1",
"express": "^4.17.1",
"express-rate-limit": "^5.2.3",
"express-rate-limit": "^5.2.6",
"frisbee": "^3.1.4",
"ioredis": "^4.19.4",
"ioredis": "^4.22.0",
"jayson": "^3.4.4",
"morgan": "^1.10.0",
"mustache": "^4.1.0",