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');
let rpc = url.parse(config.bitcoind.rpc);
rpc.timeout = 15000;
module.exports = jayson.client.http(rpc);
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 = {};
@ -114,11 +116,11 @@ export class User {
}
let self = this;
return new Promise(function(resolve, reject) {
self._lightning.newAddress({ type: 0 }, async function(err, response) {
return new Promise(function (resolve, reject) {
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,17 +402,22 @@ export class User {
}
try {
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,
amount: tx.amount,
confirmations: tx.confirmations,
address: tx.address,
time: tx.time,
});
if (config.bitcoind) {
let txs = await this._bitcoindrpc.request('listtransactions', ['*', 100500, 0, true]);
// now, compacting response a bit
for (const tx of txs.result) {
ret.result.push({
category: tx.category,
amount: tx.amount,
confirmations: tx.confirmations,
address: tx.address,
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
@ -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,17 +20,19 @@ let lightning = require('../lightning');
let identity_pubkey = false;
// ###################### SMOKE TESTS ########################
bitcoinclient.request('getblockchaininfo', false, function (err, info) {
if (info && info.result && info.result.blocks) {
if (info.result.chain === 'mainnet' && info.result.blocks < 550000) {
console.error('bitcoind is not caught up');
process.exit(1);
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 < MIN_BTC_BLOCK && !config.forceStart) {
console.error('bitcoind is not caught up');
process.exit(1);
}
} else {
console.error('bitcoind failure:', err, info);
process.exit(2);
}
} else {
console.error('bitcoind failure:', 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();
@ -469,7 +475,7 @@ router.get('/checkrouteinvoice', async function (req, res) {
});
});
router.get('/queryroutes/:source/:dest/:amt', async function(req, res) {
router.get('/queryroutes/:source/:dest/:amt', async function (req, res) {
logger.log('/queryroutes', [req.id]);
let request = {
@ -477,7 +483,7 @@ router.get('/queryroutes/:source/:dest/:amt', async function(req, res) {
amt: req.params.amt,
source_pub_key: req.params.source,
};
lightning.queryRoutes(request, function(err, response) {
lightning.queryRoutes(request, function (err, response) {
console.log(JSON.stringify(response, null, 2));
res.send(response);
});

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",

View File

@ -13,45 +13,45 @@ const important_channels = {
name: 'coingate.com',
uri: '0242a4ae0c5bef18048fbecf995094b74bfb0f7391418d71ed394784373f41e4f3@3.124.63.44:9735',
},
// '0254ff808f53b2f8c45e74b70430f336c6c76ba2f4af289f48d6086ae6e60462d3': {
// name: 'bitrefill thor',
// uri: '0254ff808f53b2f8c45e74b70430f336c6c76ba2f4af289f48d6086ae6e60462d3@52.30.63.2:9735',
// wumbo: 1,
// },
// '0254ff808f53b2f8c45e74b70430f336c6c76ba2f4af289f48d6086ae6e60462d3': {
// name: 'bitrefill thor',
// uri: '0254ff808f53b2f8c45e74b70430f336c6c76ba2f4af289f48d6086ae6e60462d3@52.30.63.2:9735',
// wumbo: 1,
// },
'030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f': {
name: 'bitrefill 2',
uri: '030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f@52.50.244.44:9735',
wumbo: 1,
},
// '025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5': {
// name: 'paywithmoon.com',
// uri: '025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5@52.86.210.65:9735',
// },
// '0279c22ed7a068d10dc1a38ae66d2d6461e269226c60258c021b1ddcdfe4b00bc4': {
// name: 'ln1.satoshilabs.com',
// uri: '0279c22ed7a068d10dc1a38ae66d2d6461e269226c60258c021b1ddcdfe4b00bc4@157.230.28.160:9735',
// },
// '02004c625d622245606a1ea2c1c69cfb4516b703b47945a3647713c05fe4aaeb1c': {
// name: 'LivingRoomOfSatoshi',
// uri: '02004c625d622245606a1ea2c1c69cfb4516b703b47945a3647713c05fe4aaeb1c@172.81.178.151:9735',
// },
// '025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5': {
// name: 'paywithmoon.com',
// uri: '025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5@52.86.210.65:9735',
// },
// '0279c22ed7a068d10dc1a38ae66d2d6461e269226c60258c021b1ddcdfe4b00bc4': {
// name: 'ln1.satoshilabs.com',
// uri: '0279c22ed7a068d10dc1a38ae66d2d6461e269226c60258c021b1ddcdfe4b00bc4@157.230.28.160:9735',
// },
// '02004c625d622245606a1ea2c1c69cfb4516b703b47945a3647713c05fe4aaeb1c': {
// name: 'LivingRoomOfSatoshi',
// uri: '02004c625d622245606a1ea2c1c69cfb4516b703b47945a3647713c05fe4aaeb1c@172.81.178.151:9735',
// },
'02816caed43171d3c9854e3b0ab2cf0c42be086ff1bd4005acc2a5f7db70d83774': {
name: 'ln.pizza aka fold',
uri: '02816caed43171d3c9854e3b0ab2cf0c42be086ff1bd4005acc2a5f7db70d83774@35.238.153.25:9735',
wumbo: 1,
},
// '0331f80652fb840239df8dc99205792bba2e559a05469915804c08420230e23c7c': {
// name: 'LightningPowerUsers.com',
// uri: '0331f80652fb840239df8dc99205792bba2e559a05469915804c08420230e23c7c@34.200.181.109:9735',
// },
// '033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025': {
// name: 'bfx-lnd0',
// uri: '033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025@34.65.85.39:9735',
// },
// '037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590': {
// name: 'fixedfloat.com',
// uri: '037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590@185.5.53.91:9735',
// },
// '0331f80652fb840239df8dc99205792bba2e559a05469915804c08420230e23c7c': {
// name: 'LightningPowerUsers.com',
// uri: '0331f80652fb840239df8dc99205792bba2e559a05469915804c08420230e23c7c@34.200.181.109:9735',
// },
// '033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025': {
// name: 'bfx-lnd0',
// uri: '033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025@34.65.85.39:9735',
// },
// '037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590': {
// name: 'fixedfloat.com',
// uri: '037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590@185.5.53.91:9735',
// },
'03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda': {
name: 'tippin.me',
uri: '03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda@157.245.68.47:9735',