Compare commits
366 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b81cc6287 | |||
| 947c7f7056 | |||
| 9563cb9714 | |||
|
|
aca47a23e9 | ||
|
|
53361d2e3a | ||
|
|
ac4461c7f3 | ||
|
|
b7d106b499 | ||
| ac2061c6ed | |||
|
|
14478becae | ||
|
|
292107342e | ||
|
|
661248702d | ||
|
|
d5fb16b074 | ||
|
|
a184872b02 | ||
|
|
6047383747 | ||
|
|
2f2c105177 | ||
|
|
64adcfc279 | ||
|
|
3a9cefa64b | ||
|
|
e149f695cc | ||
|
|
c4a41fff48 | ||
|
|
688798024c | ||
|
|
66a54570c5 | ||
|
|
e42ef3a85e | ||
|
|
600556f84c | ||
|
|
a01a8c6e14 | ||
|
|
e9c6f3abde | ||
|
|
7f9463bbbc | ||
|
|
1b1fe8f08b | ||
|
|
28dd3a847a | ||
|
|
a28eead0ab | ||
|
|
87afa20019 | ||
|
|
067f6d6667 | ||
|
|
957d6c5951 | ||
|
|
d3df6aab05 | ||
|
|
4e8ce5e46f | ||
|
|
1fd24b640e | ||
|
|
c47211f984 | ||
|
|
5223edc2b7 | ||
|
|
92e626508c | ||
|
|
3e2c31f429 | ||
|
|
376d61f81d | ||
|
|
1bb781270c | ||
|
|
c0716bc6e9 | ||
|
|
358f858ad8 | ||
|
|
7a6e8a2940 | ||
|
|
96e82ad20d | ||
|
|
406afef7f7 | ||
|
|
3ebe1938ab | ||
|
|
ee2398f247 | ||
|
|
bc84429b61 | ||
|
|
ec3039d91d | ||
|
|
1483449529 | ||
|
|
e0f037ea95 | ||
|
|
b4f880b35d | ||
|
|
35fe816d0a | ||
|
|
4a4daccecd | ||
|
|
66b9d3ab92 | ||
|
|
87fc5d6ff7 | ||
|
|
e6b1f950f3 | ||
|
|
eab49fde2f | ||
|
|
aa11daf608 | ||
|
|
4e7731ff54 | ||
|
|
1c8232d1ec | ||
|
|
fd6caf859c | ||
|
|
df6412a525 | ||
|
|
c46c1385c6 | ||
|
|
dd5d96aa56 | ||
|
|
d6d563039e | ||
|
|
f5eb5d0c78 | ||
|
|
9d17ceb23f | ||
|
|
d0406afd31 | ||
|
|
6411e9bd28 | ||
|
|
db218ef440 | ||
|
|
735b65dc0f | ||
|
|
c73d762028 | ||
|
|
4f3dff6022 | ||
|
|
a792432700 | ||
|
|
590bccd4c0 | ||
|
|
d90881771c | ||
|
|
d9d75a0b29 | ||
|
|
34ffd59c1b | ||
|
|
7dce6b848e | ||
|
|
0d337bdc7b | ||
|
|
f0bf066cfd | ||
|
|
f0ad86f1b9 | ||
|
|
12d1e9560f | ||
|
|
1eaf6c5a47 | ||
|
|
dfcaac2cfa | ||
|
|
f11c02d2f2 | ||
|
|
a4eedde3fb | ||
|
|
597615209b | ||
|
|
5fc7b074f9 | ||
|
|
df9b9a71d9 | ||
|
|
ede2359e8a | ||
|
|
b9a55f01d7 | ||
|
|
1c4eb6d83a | ||
|
|
d4c2771f4a | ||
|
|
f64a8c8102 | ||
|
|
170bf0d120 | ||
|
|
049ae5de5c | ||
|
|
877d7762ff | ||
|
|
e0a0eed038 | ||
|
|
26692d6c92 | ||
|
|
68fd866894 | ||
|
|
d7216e1506 | ||
|
|
30c8bb2d94 | ||
|
|
17e9bd30c8 | ||
|
|
d47f03501d | ||
|
|
3d1abda05b | ||
|
|
de64f1c55a | ||
|
|
bcf87de2b4 | ||
|
|
ad1b1d3da4 | ||
|
|
a6f354600c | ||
|
|
647632c914 | ||
|
|
d24de77b59 | ||
|
|
f0493d595f | ||
|
|
76b289b652 | ||
|
|
73d65bb7b3 | ||
|
|
076bb485d9 | ||
|
|
9c66b33477 | ||
|
|
eae9da8044 | ||
|
|
a72b722d1d | ||
|
|
5457d377f3 | ||
|
|
36ae5c5f4e | ||
|
|
989b195ba5 | ||
|
|
7df309771e | ||
|
|
c177a046f8 | ||
|
|
41567ac160 | ||
|
|
e3b4071bdb | ||
|
|
325015d95b | ||
|
|
2fd40eb4f6 | ||
|
|
d94afd9860 | ||
|
|
c937ef94aa | ||
|
|
2e574df0fb | ||
|
|
3838746a13 | ||
|
|
59ca61657e | ||
|
|
6d0e58a422 | ||
|
|
8e49fe975f | ||
|
|
cf40d77b01 | ||
|
|
b8cd8d05b0 | ||
|
|
61353a71e2 | ||
|
|
4138082a02 | ||
|
|
6e4dec556e | ||
|
|
a6940cb3c8 | ||
|
|
3e61a40480 | ||
|
|
716c8e53a4 | ||
|
|
2cb5e95112 | ||
|
|
99c419e4a6 | ||
|
|
5bce8181e3 | ||
|
|
e890513e7c | ||
|
|
7163416dfb | ||
|
|
592ff93c93 | ||
|
|
e38f323e07 | ||
|
|
ce58175bc0 | ||
|
|
9faf8bd944 | ||
|
|
b693c6f1e1 | ||
|
|
7b71a7ee9a | ||
|
|
04a4e042b6 | ||
|
|
3ddda25ea2 | ||
|
|
e201ce765b | ||
|
|
ebe2681d3c | ||
|
|
19e4ae977c | ||
|
|
f4b642f3b2 | ||
|
|
999f0534e0 | ||
|
|
ff23e41be7 | ||
|
|
7d5b38c52e | ||
|
|
8f162ec522 | ||
|
|
c440cd1e08 | ||
|
|
988ac805d0 | ||
|
|
d71bedbcd1 | ||
|
|
002fd6ffd0 | ||
|
|
5596912faf | ||
|
|
091e0f6b3e | ||
|
|
a079399759 | ||
|
|
00f7923568 | ||
|
|
29c1ba913b | ||
|
|
d1ad083316 | ||
|
|
3f74f3abf3 | ||
|
|
f528c03d00 | ||
|
|
f30363ff39 | ||
|
|
17bdf37f9b | ||
|
|
ac14a50137 | ||
|
|
69ce4d032a | ||
|
|
9533e43d19 | ||
|
|
9108daf5e9 | ||
|
|
ec8b28b13d | ||
|
|
00561677da | ||
|
|
864a00ab1f | ||
|
|
c69ad09a70 | ||
|
|
b9c5241a55 | ||
|
|
a9ee760a40 | ||
|
|
046380fdb6 | ||
|
|
eec5213624 | ||
|
|
8192d892bb | ||
|
|
389947ecd5 | ||
|
|
0f55b52a33 | ||
|
|
d361578301 | ||
|
|
ec9fad0738 | ||
|
|
5431bcbd15 | ||
|
|
558c84b7e2 | ||
|
|
9df3ce249b | ||
|
|
963348102d | ||
|
|
2f5c487f8a | ||
|
|
3e45ef30dd | ||
|
|
63a99c7147 | ||
|
|
b4a0a7d3d8 | ||
|
|
30422d9ac7 | ||
|
|
83f6b08b82 | ||
|
|
fc0d2a4c86 | ||
|
|
b125d68fff | ||
|
|
e390a06aae | ||
|
|
c8ef1e59e2 | ||
|
|
3ad92da8a6 | ||
|
|
61956f2a46 | ||
|
|
93899facd6 | ||
|
|
6723094ed6 | ||
|
|
ca1f5427cb | ||
|
|
d4e8d9b476 | ||
|
|
3a2ee779b6 | ||
|
|
ea1035e414 | ||
|
|
7afb56398a | ||
|
|
a4cd81a106 | ||
|
|
7a94225d87 | ||
|
|
9be413a549 | ||
|
|
305559174f | ||
|
|
bfdd319070 | ||
|
|
a6b08363a7 | ||
|
|
9f45c31618 | ||
|
|
d7e91f51ec | ||
|
|
b58e7dac88 | ||
|
|
7b5c6bc7e0 | ||
|
|
f9b2cca0bd | ||
|
|
376a3402bd | ||
|
|
38a3dea8c2 | ||
|
|
cdcd1fdd82 | ||
|
|
f4105292a3 | ||
|
|
6074dc6c2c | ||
|
|
76421f2850 | ||
|
|
f01c533899 | ||
|
|
6b67839414 | ||
|
|
dfe55a4f9f | ||
|
|
14d42a7aed | ||
|
|
a4b24ec00a | ||
|
|
0a0bf5caa3 | ||
|
|
2f1c89bb8f | ||
|
|
243902faad | ||
|
|
03ddddbf6a | ||
|
|
8e06bef3b9 | ||
|
|
b55e0f0327 | ||
|
|
ec9a71f4e9 | ||
|
|
9d8466b595 | ||
|
|
0f6a4d8cba | ||
|
|
b56064a7ba | ||
|
|
6507de9770 | ||
|
|
1e17b9925b | ||
|
|
41736cccd8 | ||
|
|
dd409cc04f | ||
|
|
daeae3c72d | ||
|
|
3af9cda96d | ||
|
|
8f28fd2865 | ||
|
|
013cadc5c8 | ||
|
|
ba38ed330d | ||
|
|
bac9480f31 | ||
|
|
5895573112 | ||
|
|
d595120a05 | ||
|
|
60ea4b2368 | ||
|
|
8aa496938e | ||
|
|
070a7d8232 | ||
|
|
cc4503ecbd | ||
|
|
cf5441c08a | ||
|
|
2c25a2090e | ||
|
|
35d8ff5599 | ||
|
|
4a871a7d17 | ||
|
|
6a6611e9b6 | ||
|
|
16c6d167da | ||
|
|
70262b1059 | ||
|
|
724bc3d656 | ||
|
|
862cda69fa | ||
|
|
75bfac565c | ||
|
|
0f423817c3 | ||
|
|
995374ff48 | ||
|
|
0fed216b32 | ||
|
|
067f2aeae4 | ||
|
|
f0d9fee177 | ||
|
|
a74f1e9ff2 | ||
|
|
43ceee11ce | ||
|
|
7c71c5e775 | ||
|
|
92f1370dba | ||
|
|
c1ac67a853 | ||
|
|
7517b02d1e | ||
|
|
454dc50693 | ||
|
|
6cf75686da | ||
|
|
7eea6c650f | ||
|
|
1ad26e21b8 | ||
|
|
f7ed99173f | ||
|
|
402d07a97f | ||
|
|
6fa5791d89 | ||
|
|
31b718e115 | ||
|
|
1da578e8a4 | ||
|
|
3317e620dd | ||
|
|
cb722e1608 | ||
|
|
f624ced5ce | ||
|
|
27ebe36b4b | ||
|
|
65a9541673 | ||
|
|
77fb7c0daa | ||
|
|
23e835c888 | ||
|
|
91f552ca7e | ||
|
|
8f2d98774b | ||
|
|
afb07aa9ba | ||
|
|
3cbe38ed28 | ||
|
|
17cd8adf41 | ||
|
|
1a740d6b06 | ||
|
|
ef9cc6d388 | ||
|
|
a7cfbca1e7 | ||
|
|
daa25d1b75 | ||
|
|
fe6cf0e860 | ||
|
|
e9bf03b58b | ||
|
|
efd31067ba | ||
|
|
db9ccb9dbe | ||
|
|
2f5f300a90 | ||
|
|
216de588a0 | ||
|
|
6d2abdfcce | ||
|
|
dd288d413f | ||
|
|
d04b155e11 | ||
|
|
e59ea298d1 | ||
|
|
d6bb2bbe25 | ||
|
|
da37b5315d | ||
|
|
1df24d0e80 | ||
|
|
0f75933bf0 | ||
|
|
4a9c4ef996 | ||
|
|
dec313aa4c | ||
|
|
b85097f7cc | ||
|
|
c1e13e9c8c | ||
|
|
e854626a45 | ||
|
|
f2386f0acf | ||
|
|
ff2d98dc10 | ||
|
|
9c47746eba | ||
|
|
a08f02572f | ||
|
|
f2616044eb | ||
|
|
4317123ffe | ||
|
|
dd7430503d | ||
|
|
d8bd2aacdf | ||
|
|
b7073abb0e | ||
|
|
98ba940342 | ||
|
|
6851769be6 | ||
|
|
d3100a1390 | ||
|
|
6a57559b16 | ||
|
|
4f345b20a8 | ||
|
|
a42d331e38 | ||
|
|
130a8dc773 | ||
|
|
b3c0873e4c | ||
|
|
6a3de700f4 | ||
|
|
51b0f89fd1 | ||
|
|
55ac146f1e | ||
|
|
2904afaf26 | ||
|
|
2cd9a19d67 | ||
|
|
765eeed9c9 | ||
|
|
bc5841d07d | ||
|
|
5b8ab18a59 | ||
|
|
7e1d4a3110 | ||
|
|
5100252ea5 | ||
|
|
b8bc4dd0cd | ||
|
|
a7294cbbbd | ||
|
|
950900b136 | ||
|
|
4ad56aaebe | ||
|
|
f4b35e4895 | ||
|
|
9e70f123a6 |
11
.babelrc
@ -1,3 +1,12 @@
|
||||
{
|
||||
"presets": ["es2015"]
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"node": true
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
14
.eslintrc
@ -1,16 +1,16 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"parser": "@babel/eslint-parser",
|
||||
"plugins": [
|
||||
"prettier"
|
||||
],
|
||||
"extends": ["prettier"],
|
||||
"extends": ["plugin:prettier/recommended"],
|
||||
"rules": {
|
||||
'prettier/prettier': [
|
||||
'warn',
|
||||
"prettier/prettier": [
|
||||
"warn",
|
||||
{
|
||||
singleQuote: true,
|
||||
printWidth: 140,
|
||||
trailingComma: 'all'
|
||||
"singleQuote": true,
|
||||
"printWidth": 140,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
49
.github/workflows/push.yml
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
name: Build on push
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
DOCKER_CLI_EXPERIMENTAL: enabled
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build image
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
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 linux/arm64,linux/amd64 \
|
||||
--tag ${{ secrets.DOCKER_CONTAINER_USERNAME }}/lndhub:$BRANCH \
|
||||
--output "type=registry" ./
|
||||
57
.github/workflows/tag.yml
vendored
Normal file
@ -0,0 +1,57 @@
|
||||
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
|
||||
|
||||
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 linux/arm64,linux/amd64 \
|
||||
--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 linux/arm64,linux/amd64 \
|
||||
--tag ${{ secrets.DOCKER_CONTAINER_USERNAME }}/lndhub:latest \
|
||||
--output "type=registry" ./
|
||||
43
Dockerfile
Normal file
@ -0,0 +1,43 @@
|
||||
FROM alpine:latest AS perms
|
||||
|
||||
# This is a bit weird, but required to make sure the LND data can be accessed.
|
||||
RUN adduser --disabled-password \
|
||||
--home "/lndhub" \
|
||||
--gecos "" \
|
||||
"lndhub"
|
||||
|
||||
FROM node:16-bullseye-slim AS builder
|
||||
|
||||
# These packages are required for building 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
|
||||
|
||||
# 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:16-bullseye-slim
|
||||
|
||||
# Create a specific user so LNDHub doesn't run as root
|
||||
COPY --from=perms /etc/group /etc/passwd /etc/shadow /etc/
|
||||
|
||||
# Copy LNDHub with installed modules from builder
|
||||
COPY --from=builder /lndhub /lndhub
|
||||
|
||||
# Create logs folder and ensure permissions are set correctly
|
||||
RUN mkdir /lndhub/logs && chown -R lndhub:lndhub /lndhub
|
||||
USER lndhub
|
||||
|
||||
ENV PORT=3000
|
||||
EXPOSE 3000
|
||||
|
||||
CMD cp $LND_CERT_FILE /lndhub/ && cp $LND_ADMIN_MACAROON_FILE /lndhub/ && cd /lndhub && npm start
|
||||
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 BlueWallet
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
44
README.md
@ -1,21 +1,33 @@
|
||||
LndHub
|
||||
======
|
||||
|
||||
Wrapper for Lightning Network Daemon. It provides separate accounts with minimum trust for end users
|
||||
Wrapper for Lightning Network Daemon (lnd). It provides separate accounts with minimum trust for end users.
|
||||
|
||||
INSTALLATION
|
||||
------------
|
||||
|
||||
You can use those guides or follow instructions below:
|
||||
|
||||
* https://github.com/dangeross/guides/blob/master/raspibolt/raspibolt_6B_lndhub.md
|
||||
* https://medium.com/@jpthor/running-lndhub-on-mac-osx-5be6671b2e0c
|
||||
|
||||
```
|
||||
git clone git@github.com:BlueWallet/LndHub.git
|
||||
cd LndHub
|
||||
npm i
|
||||
```
|
||||
|
||||
Install `bitcoind`, `lnd` and `redis`.
|
||||
Install `bitcoind`, `lnd`, and `redis`. Edit LndHub's `config.js` to set it up correctly.
|
||||
Copy the files `admin.macaroon` (for Bitcoin mainnet, usually stored in `~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon`)
|
||||
and `tls.cert` (usually stored in `~/.lnd/tls.cert`) into the root folder of LndHub.
|
||||
|
||||
Edit `config.js` and set it up correctly.
|
||||
Copy `admin.macaroon` and `tls.cert` in root folder of LndHub.
|
||||
LndHub expects LND's wallet to be unlocked, if not — it will attempt to unlock it with the password stored in `config.lnd.password`.
|
||||
Don't forget to configure disk-persistence for `redis` (e.g., you may want to set `appendonly` to `yes` in `redis.conf` (see
|
||||
http://redis.io/topics/persistence for more information).
|
||||
|
||||
If you have no `bitcoind` instance, for example if you use neutrino, or you have no bitcoind wallet,
|
||||
for example if you use LND for wallet managment, you can remove the bitcoind settings from `config.js`.
|
||||
Please note that this feature is limited to Bitcoin, so you can't use it if you use any other cryptocurrency with LND (e.g., Litecoin).
|
||||
|
||||
### Deploy to Heroku
|
||||
|
||||
@ -24,7 +36,29 @@ Add config vars :
|
||||
* `MACAROON`: hex-encoded `admin.macaroon`
|
||||
* `TLSCERT`: hex-encoded `tls.cert`
|
||||
|
||||
### Run in docker
|
||||
|
||||
LndHub is available on Docker Hub as [`bluewalletorganization/lndhub`](https://hub.docker.com/r/bluewalletorganization/lndhub).
|
||||
Please note that this requires a separate instance of redis and LND and optionally, bitcoind.
|
||||
You can also view Umbrel's implementation using docker-compose [here](https://github.com/getumbrel/umbrel/blob/280c87f0f323666b1b0552aeb24f60df94d1e43c/apps/lndhub/docker-compose.yml).
|
||||
|
||||
### Reference client implementation
|
||||
|
||||
Can be used in ReactNative or Nodejs environment
|
||||
|
||||
* https://github.com/BlueWallet/BlueWallet/blob/master/class/wallets/lightning-custodian-wallet.js
|
||||
|
||||
|
||||
|
||||
### Tests
|
||||
|
||||
Acceptance tests are in https://github.com/BlueWallet/BlueWallet/blob/master/LightningCustodianWallet.test.js
|
||||
Acceptance tests are in https://github.com/BlueWallet/BlueWallet/blob/master/tests/integration/lightning-custodian-wallet.test.js
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## Responsible disclosure
|
||||
|
||||
Found critical bugs/vulnerabilities? Please email them to bluewallet@bluewallet.io
|
||||
Thanks!
|
||||
|
||||
10
bitcoin.js
@ -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 = 5000;
|
||||
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
@ -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();
|
||||
};
|
||||
113
class/Invo.js
Normal file
@ -0,0 +1,113 @@
|
||||
var crypto = require('crypto');
|
||||
var lightningPayReq = require('bolt11');
|
||||
|
||||
export class Invo {
|
||||
constructor(redis, bitcoindrpc, lightning) {
|
||||
this._redis = redis;
|
||||
this._bitcoindrpc = bitcoindrpc;
|
||||
this._lightning = lightning;
|
||||
this._decoded = false;
|
||||
this._bolt11 = false;
|
||||
this._isPaid = null;
|
||||
}
|
||||
|
||||
setInvoice(bolt11) {
|
||||
this._bolt11 = bolt11;
|
||||
}
|
||||
|
||||
async getIsMarkedAsPaidInDatabase() {
|
||||
if (!this._bolt11) throw new Error('bolt11 is not provided');
|
||||
const decoded = lightningPayReq.decode(this._bolt11);
|
||||
let paymentHash = false;
|
||||
for (const tag of decoded.tags) {
|
||||
if (tag.tagName === 'payment_hash') {
|
||||
paymentHash = tag.data;
|
||||
}
|
||||
}
|
||||
if (!paymentHash) throw new Error('Could not find payment hash in invoice tags');
|
||||
return await this._getIsPaymentHashMarkedPaidInDatabase(paymentHash);
|
||||
}
|
||||
|
||||
async markAsPaidInDatabase() {
|
||||
if (!this._bolt11) throw new Error('bolt11 is not provided');
|
||||
const decoded = lightningPayReq.decode(this._bolt11);
|
||||
let paymentHash = false;
|
||||
for (const tag of decoded.tags) {
|
||||
if (tag.tagName === 'payment_hash') {
|
||||
paymentHash = tag.data;
|
||||
}
|
||||
}
|
||||
if (!paymentHash) throw new Error('Could not find payment hash in invoice tags');
|
||||
return await this._setIsPaymentHashPaidInDatabase(paymentHash, decoded.satoshis);
|
||||
}
|
||||
|
||||
async markAsUnpaidInDatabase() {
|
||||
if (!this._bolt11) throw new Error('bolt11 is not provided');
|
||||
const decoded = lightningPayReq.decode(this._bolt11);
|
||||
let paymentHash = false;
|
||||
for (const tag of decoded.tags) {
|
||||
if (tag.tagName === 'payment_hash') {
|
||||
paymentHash = tag.data;
|
||||
}
|
||||
}
|
||||
if (!paymentHash) throw new Error('Could not find payment hash in invoice tags');
|
||||
return await this._setIsPaymentHashPaidInDatabase(paymentHash, false);
|
||||
}
|
||||
|
||||
async _setIsPaymentHashPaidInDatabase(paymentHash, settleAmountSat) {
|
||||
if (settleAmountSat) {
|
||||
return await this._redis.set('ispaid_' + paymentHash, settleAmountSat);
|
||||
} else {
|
||||
return await this._redis.del('ispaid_' + paymentHash);
|
||||
}
|
||||
}
|
||||
|
||||
async _getIsPaymentHashMarkedPaidInDatabase(paymentHash) {
|
||||
return await this._redis.get('ispaid_' + paymentHash);
|
||||
}
|
||||
|
||||
async getPreimage() {
|
||||
if (!this._bolt11) throw new Error('bolt11 is not provided');
|
||||
const decoded = lightningPayReq.decode(this._bolt11);
|
||||
let paymentHash = false;
|
||||
for (const tag of decoded.tags) {
|
||||
if (tag.tagName === 'payment_hash') {
|
||||
paymentHash = tag.data;
|
||||
}
|
||||
}
|
||||
if (!paymentHash) throw new Error('Could not find payment hash in invoice tags');
|
||||
return await this._redis.get('preimage_for_' + paymentHash);
|
||||
}
|
||||
|
||||
async savePreimage(preimageHex) {
|
||||
const paymentHashHex = require('crypto').createHash('sha256').update(Buffer.from(preimageHex, 'hex')).digest('hex');
|
||||
const key = 'preimage_for_' + paymentHashHex;
|
||||
await this._redis.set(key, preimageHex);
|
||||
await this._redis.expire(key, 3600 * 24 * 30); // 1 month
|
||||
}
|
||||
|
||||
makePreimageHex() {
|
||||
let buffer = crypto.randomBytes(32);
|
||||
return buffer.toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries LND ofr all user invoices
|
||||
*
|
||||
* @return {Promise<array>}
|
||||
*/
|
||||
async listInvoices() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._lightning.listInvoices(
|
||||
{
|
||||
num_max_invoices: 99000111,
|
||||
reversed: true,
|
||||
},
|
||||
function (err, response) {
|
||||
if (err) return reject(err);
|
||||
resolve(response);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
35
class/Lock.js
Normal file
@ -0,0 +1,35 @@
|
||||
export class Lock {
|
||||
/**
|
||||
*
|
||||
* @param {Redis} redis
|
||||
* @param {String} lock_key
|
||||
*/
|
||||
constructor(redis, lock_key) {
|
||||
this._redis = redis;
|
||||
this._lock_key = lock_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to obtain lock in single-threaded Redis.
|
||||
* Returns TRUE if success.
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async obtainLock() {
|
||||
const timestamp = +new Date();
|
||||
let setResult = await this._redis.setnx(this._lock_key, timestamp);
|
||||
if (!setResult) {
|
||||
// it already held a value - failed locking
|
||||
return false;
|
||||
}
|
||||
|
||||
// success - got lock
|
||||
await this._redis.expire(this._lock_key, 5 * 60);
|
||||
// lock expires in 5 mins just for any case
|
||||
return true;
|
||||
}
|
||||
|
||||
async releaseLock() {
|
||||
await this._redis.del(this._lock_key);
|
||||
}
|
||||
}
|
||||
155
class/Paym.js
Normal file
@ -0,0 +1,155 @@
|
||||
var crypto = require('crypto');
|
||||
var lightningPayReq = require('bolt11');
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
|
||||
export class Paym {
|
||||
constructor(redis, bitcoindrpc, lightning) {
|
||||
this._redis = redis;
|
||||
this._bitcoindrpc = bitcoindrpc;
|
||||
this._lightning = lightning;
|
||||
this._decoded = false;
|
||||
this._bolt11 = false;
|
||||
this._isPaid = null;
|
||||
}
|
||||
|
||||
setInvoice(bolt11) {
|
||||
this._bolt11 = bolt11;
|
||||
}
|
||||
|
||||
async decodePayReqViaRpc(invoice) {
|
||||
let that = this;
|
||||
return new Promise(function (resolve, reject) {
|
||||
that._lightning.decodePayReq({ pay_req: invoice }, function (err, info) {
|
||||
if (err) return reject(err);
|
||||
that._decoded = info;
|
||||
return resolve(info);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async queryRoutes() {
|
||||
if (!this._bolt11) throw new Error('bolt11 is not provided');
|
||||
if (!this._decoded) await this.decodePayReqViaRpc(this._bolt11);
|
||||
|
||||
var request = {
|
||||
pub_key: this._decoded.destination,
|
||||
amt: this._decoded.num_satoshis,
|
||||
final_cltv_delta: 144,
|
||||
fee_limit: { fixed: Math.floor(this._decoded.num_satoshis * forwardFee) + 1 },
|
||||
};
|
||||
let that = this;
|
||||
return new Promise(function (resolve, reject) {
|
||||
that._lightning.queryRoutes(request, function (err, response) {
|
||||
if (err) return reject(err);
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async sendToRouteSync(routes) {
|
||||
if (!this._bolt11) throw new Error('bolt11 is not provided');
|
||||
if (!this._decoded) await this.decodePayReqViaRpc(this._bolt11);
|
||||
|
||||
let request = {
|
||||
payment_hash_string: this._decoded.payment_hash,
|
||||
route: routes[0],
|
||||
};
|
||||
|
||||
console.log('sendToRouteSync:', { request });
|
||||
|
||||
let that = this;
|
||||
return new Promise(function (resolve, reject) {
|
||||
that._lightning.sendToRouteSync(request, function (err, response) {
|
||||
if (err) reject(err);
|
||||
resolve(that.processSendPaymentResponse(response));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
processSendPaymentResponse(payment) {
|
||||
if (payment && payment.payment_route && payment.payment_route.total_amt_msat) {
|
||||
// paid just now
|
||||
this._isPaid = true;
|
||||
payment.payment_route.total_fees = +payment.payment_route.total_fees + Math.floor(+payment.payment_route.total_amt * internalFee);
|
||||
if (this._bolt11) payment.pay_req = this._bolt11;
|
||||
if (this._decoded) payment.decoded = this._decoded;
|
||||
}
|
||||
|
||||
if (payment.payment_error && payment.payment_error.indexOf('already paid') !== -1) {
|
||||
// already paid
|
||||
this._isPaid = true;
|
||||
if (this._decoded) {
|
||||
payment.decoded = this._decoded;
|
||||
if (this._bolt11) payment.pay_req = this._bolt11;
|
||||
// trying to guess the fee
|
||||
payment.payment_route = payment.payment_route || {};
|
||||
payment.payment_route.total_fees = Math.floor(this._decoded.num_satoshis * forwardFee); // we dont know the exact fee, so we use max (same as fee_limit)
|
||||
payment.payment_route.total_amt = this._decoded.num_satoshis;
|
||||
}
|
||||
}
|
||||
|
||||
if (payment.payment_error && payment.payment_error.indexOf('unable to') !== -1) {
|
||||
// failed to pay
|
||||
this._isPaid = false;
|
||||
}
|
||||
|
||||
if (payment.payment_error && payment.payment_error.indexOf('FinalExpiryTooSoon') !== -1) {
|
||||
this._isPaid = false;
|
||||
}
|
||||
|
||||
if (payment.payment_error && payment.payment_error.indexOf('UnknownPaymentHash') !== -1) {
|
||||
this._isPaid = false;
|
||||
}
|
||||
|
||||
if (payment.payment_error && payment.payment_error.indexOf('IncorrectOrUnknownPaymentDetails') !== -1) {
|
||||
this._isPaid = false;
|
||||
}
|
||||
|
||||
if (payment.payment_error && payment.payment_error.indexOf('payment is in transition') !== -1) {
|
||||
this._isPaid = null; // null is default, but lets set it anyway
|
||||
}
|
||||
|
||||
return payment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns NULL if unknown, true if its paid, false if its unpaid
|
||||
* (judging by error in sendPayment response)
|
||||
*
|
||||
* @returns {boolean|null}
|
||||
*/
|
||||
getIsPaid() {
|
||||
return this._isPaid;
|
||||
}
|
||||
|
||||
async attemptPayToRoute() {
|
||||
let routes = await this.queryRoutes();
|
||||
return await this.sendToRouteSync(routes.routes);
|
||||
}
|
||||
|
||||
async listPayments() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._lightning.listPayments({}, function (err, response) {
|
||||
if (err) return reject(err);
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async isExpired() {
|
||||
if (!this._bolt11) throw new Error('bolt11 is not provided');
|
||||
const decoded = await this.decodePayReqViaRpc(this._bolt11);
|
||||
return +decoded.timestamp + +decoded.expiry < +new Date() / 1000;
|
||||
}
|
||||
|
||||
decodePayReqLocally(payReq) {
|
||||
this._decoded_locally = lightningPayReq.decode(payReq);
|
||||
}
|
||||
|
||||
async getPaymentHash() {
|
||||
if (!this._bolt11) throw new Error('bolt11 is not provided');
|
||||
if (!this._decoded) await this.decodePayReqViaRpc(this._bolt11);
|
||||
|
||||
return this._decoded['payment_hash'];
|
||||
}
|
||||
}
|
||||
391
class/User.js
@ -1,6 +1,15 @@
|
||||
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 = {};
|
||||
let _listtransactions_cache = false;
|
||||
let _listtransactions_cache_expiry_ts = 0;
|
||||
|
||||
export class User {
|
||||
/**
|
||||
@ -35,6 +44,7 @@ export class User {
|
||||
}
|
||||
|
||||
async loadByAuthorization(authorization) {
|
||||
if (!authorization) return false;
|
||||
let access_token = authorization.replace('Bearer ', '');
|
||||
let userid = await this._redis.get('userid_for_' + access_token);
|
||||
|
||||
@ -99,23 +109,94 @@ export class User {
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
async generateAddress() {
|
||||
let lock = new Lock(this._redis, 'generating_address_' + this._userid);
|
||||
if (!(await lock.obtainLock())) {
|
||||
// someone's already generating address
|
||||
return;
|
||||
}
|
||||
|
||||
let self = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
self._lightning.newAddress({ type: 0 }, async function(err, response) {
|
||||
if (err) return reject('LND failure');
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getBalance() {
|
||||
return (await this._redis.get('balance_for_' + this._userid)) * 1;
|
||||
async watchAddress(address) {
|
||||
if (!address) return;
|
||||
if (config.bitcoind) return this._bitcoindrpc.request('importaddress', [address, address, false]);
|
||||
}
|
||||
|
||||
/**
|
||||
* LndHub no longer relies on redis balance as source of truth, this is
|
||||
* more a cache now. See `this.getCalculatedBalance()` to get correct balance.
|
||||
*
|
||||
* @returns {Promise<number>} Balance available to spend
|
||||
*/
|
||||
async getBalance() {
|
||||
let balance = (await this._redis.get('balance_for_' + this._userid)) * 1;
|
||||
if (!balance) {
|
||||
balance = await this.getCalculatedBalance();
|
||||
await this.saveBalance(balance);
|
||||
}
|
||||
return balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accounts for all possible transactions in user's account and
|
||||
* sums their amounts.
|
||||
*
|
||||
* @returns {Promise<number>} Balance available to spend
|
||||
*/
|
||||
async getCalculatedBalance() {
|
||||
let calculatedBalance = 0;
|
||||
let userinvoices = await this.getUserInvoices();
|
||||
|
||||
for (let invo of userinvoices) {
|
||||
if (invo && invo.ispaid) {
|
||||
calculatedBalance += +invo.amt;
|
||||
}
|
||||
}
|
||||
|
||||
let txs = await this.getTxs();
|
||||
for (let tx of txs) {
|
||||
if (tx.type === 'bitcoind_tx') {
|
||||
// topup
|
||||
calculatedBalance += new BigNumber(tx.amount).multipliedBy(100000000).toNumber();
|
||||
} else {
|
||||
calculatedBalance -= +tx.value;
|
||||
}
|
||||
}
|
||||
|
||||
let lockedPayments = await this.getLockedPayments();
|
||||
for (let paym of lockedPayments) {
|
||||
// locked payments are processed in scripts/process-locked-payments.js
|
||||
calculatedBalance -= +paym.amount + /* feelimit */ Math.floor(paym.amount * forwardFee);
|
||||
}
|
||||
|
||||
return calculatedBalance;
|
||||
}
|
||||
|
||||
/**
|
||||
* LndHub no longer relies on redis balance as source of truth, this is
|
||||
* more a cache now. See `this.getCalculatedBalance()` to get correct balance.
|
||||
*
|
||||
* @param balance
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async saveBalance(balance) {
|
||||
return await this._redis.set('balance_for_' + this._userid, balance);
|
||||
const key = 'balance_for_' + this._userid;
|
||||
await this._redis.set(key, balance);
|
||||
await this._redis.expire(key, 1800);
|
||||
}
|
||||
|
||||
async clearBalanceCache() {
|
||||
const key = 'balance_for_' + this._userid;
|
||||
return this._redis.del(key);
|
||||
}
|
||||
|
||||
async savePaidLndInvoice(doc) {
|
||||
@ -144,15 +225,17 @@ export class User {
|
||||
|
||||
/**
|
||||
* Doent belong here, FIXME
|
||||
* @see Invo._setIsPaymentHashPaidInDatabase
|
||||
* @see Invo.markAsPaidInDatabase
|
||||
*/
|
||||
async setPaymentHashPaid(payment_hash) {
|
||||
return await this._redis.set('ispaid_' + payment_hash, 1);
|
||||
async setPaymentHashPaid(payment_hash, settleAmountSat) {
|
||||
return await this._redis.set('ispaid_' + payment_hash, settleAmountSat);
|
||||
}
|
||||
|
||||
async lookupInvoice(payment_hash) {
|
||||
let that = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
that._lightning.lookupInvoice({ r_hash_str: payment_hash }, function(err, response) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
that._lightning.lookupInvoice({ r_hash_str: payment_hash }, function (err, response) {
|
||||
if (err) resolve({});
|
||||
resolve(response);
|
||||
});
|
||||
@ -161,13 +244,29 @@ export class User {
|
||||
|
||||
/**
|
||||
* Doent belong here, FIXME
|
||||
* @see Invo._getIsPaymentHashMarkedPaidInDatabase
|
||||
* @see Invo.getIsMarkedAsPaidInDatabase
|
||||
*/
|
||||
async getPaymentHashPaid(payment_hash) {
|
||||
return await this._redis.get('ispaid_' + payment_hash);
|
||||
}
|
||||
|
||||
async getUserInvoices() {
|
||||
async syncInvoicePaid(payment_hash) {
|
||||
const invoice = await this.lookupInvoice(payment_hash);
|
||||
const ispaid = invoice.settled; // TODO: start using `state` instead as its future proof, and this one might get deprecated
|
||||
if (ispaid) {
|
||||
// so invoice was paid after all
|
||||
await this.setPaymentHashPaid(payment_hash, invoice.amt_paid_msat ? Math.floor(invoice.amt_paid_msat / 1000) : invoice.amt_paid_sat);
|
||||
await this.clearBalanceCache();
|
||||
}
|
||||
return ispaid;
|
||||
}
|
||||
|
||||
async getUserInvoices(limit) {
|
||||
let range = await this._redis.lrange('userinvoices_for_' + this._userid, 0, -1);
|
||||
if (limit && !isNaN(parseInt(limit))) {
|
||||
range = range.slice(parseInt(limit) * -1);
|
||||
}
|
||||
let result = [];
|
||||
for (let invoice of range) {
|
||||
invoice = JSON.parse(invoice);
|
||||
@ -175,20 +274,46 @@ export class User {
|
||||
invoice.description = '';
|
||||
for (let tag of decoded.tags) {
|
||||
if (tag.tagName === 'description') {
|
||||
invoice.description += decodeURIComponent(tag.data);
|
||||
try {
|
||||
invoice.description += decodeURIComponent(tag.data);
|
||||
} catch (_) {
|
||||
invoice.description += tag.data;
|
||||
}
|
||||
}
|
||||
if (tag.tagName === 'payment_hash') {
|
||||
invoice.payment_hash = tag.data;
|
||||
}
|
||||
}
|
||||
invoice.ispaid = !!(await this.getPaymentHashPaid(invoice.payment_hash));
|
||||
if (!invoice.ispaid) {
|
||||
// attempting to lookup invoice
|
||||
let lookup_info = await this.lookupInvoice(invoice.payment_hash);
|
||||
invoice.ispaid = lookup_info.settled;
|
||||
|
||||
let paymentHashPaidAmountSat = 0;
|
||||
if (_invoice_ispaid_cache[invoice.payment_hash]) {
|
||||
// static cache hit
|
||||
invoice.ispaid = true;
|
||||
paymentHashPaidAmountSat = _invoice_ispaid_cache[invoice.payment_hash];
|
||||
} else {
|
||||
// static cache miss, asking redis cache
|
||||
paymentHashPaidAmountSat = await this.getPaymentHashPaid(invoice.payment_hash);
|
||||
if (paymentHashPaidAmountSat) invoice.ispaid = true;
|
||||
}
|
||||
|
||||
invoice.amt = decoded.satoshis;
|
||||
if (!invoice.ispaid) {
|
||||
if (decoded && decoded.timestamp > +new Date() / 1000 - 3600 * 24 * 5) {
|
||||
// if invoice is not too old we query lnd to find out if its paid
|
||||
invoice.ispaid = await this.syncInvoicePaid(invoice.payment_hash);
|
||||
paymentHashPaidAmountSat = await this.getPaymentHashPaid(invoice.payment_hash); // since we have just saved it
|
||||
}
|
||||
} else {
|
||||
_invoice_ispaid_cache[invoice.payment_hash] = paymentHashPaidAmountSat;
|
||||
}
|
||||
|
||||
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;
|
||||
invoice.type = 'user_invoice';
|
||||
result.push(invoice);
|
||||
}
|
||||
|
||||
@ -201,17 +326,17 @@ export class User {
|
||||
|
||||
/**
|
||||
* User's onchain txs that are >= 3 confs
|
||||
* Queries bitcoind RPC.
|
||||
*
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
async getTxs() {
|
||||
let addr = await this.getAddress();
|
||||
if (!addr) throw new Error('cannot get transactions: no onchain address assigned to user');
|
||||
let txs = await this._bitcoindrpc.request('listtransactions', [addr, 100500, 0, true]);
|
||||
const addr = await this.getOrGenerateAddress();
|
||||
let txs = await this._listtransactions();
|
||||
txs = txs.result;
|
||||
let result = [];
|
||||
for (let tx of txs) {
|
||||
if (tx.confirmations >= 3) {
|
||||
if (tx.confirmations >= 3 && tx.address === addr && tx.category === 'receive') {
|
||||
tx.type = 'bitcoind_tx';
|
||||
result.push(tx);
|
||||
}
|
||||
@ -221,29 +346,132 @@ export class User {
|
||||
for (let invoice of range) {
|
||||
invoice = JSON.parse(invoice);
|
||||
invoice.type = 'paid_invoice';
|
||||
invoice.fee = +invoice.payment_route.total_fees;
|
||||
invoice.value = +invoice.payment_route.total_fees + +invoice.payment_route.total_amt;
|
||||
invoice.timestamp = invoice.decoded.timestamp;
|
||||
invoice.memo = invoice.decoded.description;
|
||||
|
||||
// for internal invoices it might not have properties `payment_route` and `decoded`...
|
||||
if (invoice.payment_route) {
|
||||
invoice.fee = +invoice.payment_route.total_fees;
|
||||
invoice.value = +invoice.payment_route.total_fees + +invoice.payment_route.total_amt;
|
||||
if (invoice.payment_route.total_amt_msat && invoice.payment_route.total_amt_msat / 1000 !== +invoice.payment_route.total_amt) {
|
||||
// okay, we have to account for MSAT
|
||||
invoice.value =
|
||||
+invoice.payment_route.total_fees +
|
||||
Math.max(parseInt(invoice.payment_route.total_amt_msat / 1000), +invoice.payment_route.total_amt) +
|
||||
1; // extra sat to cover for msats, as external layer (clients) dont have that resolution
|
||||
}
|
||||
} else {
|
||||
invoice.fee = 0;
|
||||
}
|
||||
if (invoice.decoded) {
|
||||
invoice.timestamp = invoice.decoded.timestamp;
|
||||
invoice.memo = invoice.memo || invoice.decoded.description;
|
||||
}
|
||||
if (invoice.payment_preimage) {
|
||||
invoice.payment_preimage = Buffer.from(invoice.payment_preimage, 'hex').toString('hex');
|
||||
}
|
||||
// removing unsued by client fields to reduce size
|
||||
delete invoice.payment_error;
|
||||
delete invoice.payment_route;
|
||||
delete invoice.pay_req;
|
||||
delete invoice.decoded;
|
||||
result.push(invoice);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple caching for this._bitcoindrpc.request('listtransactions', ['*', 100500, 0, true]);
|
||||
* since its too much to fetch from bitcoind every time
|
||||
*
|
||||
* @returns {Promise<*>}
|
||||
* @private
|
||||
*/
|
||||
async _listtransactions() {
|
||||
let response = _listtransactions_cache;
|
||||
if (response) {
|
||||
if (+new Date() > _listtransactions_cache_expiry_ts) {
|
||||
// invalidate cache
|
||||
response = _listtransactions_cache = false;
|
||||
} else {
|
||||
try {
|
||||
return JSON.parse(response);
|
||||
} catch (_) {
|
||||
// nop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let ret = { result: [] };
|
||||
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.blocktime || 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);
|
||||
return ret;
|
||||
} catch (error) {
|
||||
console.warn('listtransactions error:', error);
|
||||
let _listtransactions_cache = await this._redis.get('listtransactions');
|
||||
if (!_listtransactions_cache) return { result: [] };
|
||||
return JSON.parse(_listtransactions_cache);
|
||||
}
|
||||
}
|
||||
|
||||
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) throw new Error('cannot get transactions: no onchain address assigned to user');
|
||||
let txs = await this._bitcoindrpc.request('listtransactions', [addr, 100500, 0, true]);
|
||||
const addr = await this.getOrGenerateAddress();
|
||||
let txs = await this._listtransactions();
|
||||
txs = txs.result;
|
||||
let result = [];
|
||||
for (let tx of txs) {
|
||||
if (tx.confirmations < 3) {
|
||||
if (tx.confirmations < 3 && tx.address === addr && tx.category === 'receive') {
|
||||
result.push(tx);
|
||||
}
|
||||
}
|
||||
@ -276,8 +504,9 @@ export class User {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async accountForPosibleTxids() {
|
||||
let imported_txids = await this._redis.lrange('imported_txids_for_' + this._userid, 0, -1);
|
||||
// return; // TODO: remove
|
||||
let onchain_txs = await this.getTxs();
|
||||
let imported_txids = await this._redis.lrange('imported_txids_for_' + this._userid, 0, -1);
|
||||
for (let tx of onchain_txs) {
|
||||
if (tx.type !== 'bitcoind_tx') continue;
|
||||
let already_imported = false;
|
||||
@ -286,19 +515,103 @@ export class User {
|
||||
}
|
||||
|
||||
if (!already_imported && tx.category === 'receive') {
|
||||
let userBalance = await this.getBalance();
|
||||
userBalance += new BigNumber(tx.amount).multipliedBy(100000000).toNumber();
|
||||
// first, locking...
|
||||
let lock = new Lock(this._redis, 'importing_' + tx.txid);
|
||||
if (!(await lock.obtainLock())) {
|
||||
// someone's already importing this tx
|
||||
return;
|
||||
}
|
||||
|
||||
let userBalance = await this.getCalculatedBalance();
|
||||
// userBalance += new BigNumber(tx.amount).multipliedBy(100000000).toNumber();
|
||||
// no need to add since it was accounted for in `this.getCalculatedBalance()`
|
||||
await this.saveBalance(userBalance);
|
||||
await this._redis.rpush('imported_txids_for_' + this._userid, tx.txid);
|
||||
await lock.releaseLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds invoice to a list of user's locked payments.
|
||||
* Used to calculate balance till the lock is lifted (payment is in
|
||||
* determined state - succeded or failed).
|
||||
*
|
||||
* @param {String} pay_req
|
||||
* @param {Object} decodedInvoice
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async lockFunds(pay_req, decodedInvoice) {
|
||||
let doc = {
|
||||
pay_req,
|
||||
amount: +decodedInvoice.num_satoshis,
|
||||
timestamp: Math.floor(+new Date() / 1000),
|
||||
};
|
||||
|
||||
return this._redis.rpush('locked_payments_for_' + this._userid, JSON.stringify(doc));
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips specific payreq from the list of locked payments
|
||||
* @param pay_req
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async unlockFunds(pay_req) {
|
||||
let payments = await this.getLockedPayments();
|
||||
let saveBack = [];
|
||||
for (let paym of payments) {
|
||||
if (paym.pay_req !== pay_req) {
|
||||
saveBack.push(paym);
|
||||
}
|
||||
}
|
||||
|
||||
await this._redis.del('locked_payments_for_' + this._userid);
|
||||
for (let doc of saveBack) {
|
||||
await this._redis.rpush('locked_payments_for_' + this._userid, JSON.stringify(doc));
|
||||
}
|
||||
}
|
||||
|
||||
async getLockedPayments() {
|
||||
let payments = await this._redis.lrange('locked_payments_for_' + this._userid, 0, -1);
|
||||
let result = [];
|
||||
for (let paym of payments) {
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(paym);
|
||||
result.push(json);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
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');
|
||||
return crypto.createHash('sha256').update(string).digest().toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffles array in place. ES6 version
|
||||
* @param {Array} a items An array containing the items.
|
||||
*/
|
||||
static _shuffle(a) {
|
||||
for (let i = a.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[a[i], a[j]] = [a[j], a[i]];
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
static async _sleep(s) {
|
||||
return new Promise((r) => setTimeout(r, s * 1000));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1 +1,4 @@
|
||||
export * from './User';
|
||||
export * from './Lock';
|
||||
export * from './Paym';
|
||||
export * from './Invo';
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
let config = {
|
||||
enableUpdateDescribeGraph: false,
|
||||
postRateLimit: 100,
|
||||
rateLimit: 200,
|
||||
forwardReserveFee: 0.01, // default 0.01
|
||||
intraHubFee: 0.003, // default 0.003
|
||||
bitcoind: {
|
||||
rpc: 'http://login:password@1.1.1.1:8332',
|
||||
rpc: 'http://login:password@1.1.1.1:8332/wallet/wallet.dat',
|
||||
},
|
||||
redis: {
|
||||
port: 12914,
|
||||
@ -11,6 +16,7 @@ let config = {
|
||||
},
|
||||
lnd: {
|
||||
url: '1.1.1.1:10009',
|
||||
password: '',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -1,70 +1,163 @@
|
||||
import { User } from '../class/User';
|
||||
import { User, Lock, Paym, Invo } from '../class/';
|
||||
import Frisbee from 'frisbee';
|
||||
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');
|
||||
var redis = new Redis(config.redis);
|
||||
redis.monitor(function(err, monitor) {
|
||||
monitor.on('monitor', function(time, args, source, database) {
|
||||
console.log('REDIS', JSON.stringify(args));
|
||||
redis.monitor(function (err, monitor) {
|
||||
monitor.on('monitor', function (time, args, source, database) {
|
||||
// console.log('REDIS', JSON.stringify(args));
|
||||
});
|
||||
});
|
||||
|
||||
/****** START SET FEES FROM CONFIG AT STARTUP ******/
|
||||
/** GLOBALS */
|
||||
global.forwardFee = config.forwardReserveFee || 0.01;
|
||||
global.internalFee = config.intraHubFee || 0.003;
|
||||
/****** END SET FEES FROM CONFIG AT STARTUP ******/
|
||||
|
||||
let bitcoinclient = require('../bitcoin');
|
||||
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.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);
|
||||
}
|
||||
console.log('bitcoind getblockchaininfo:', info);
|
||||
} else {
|
||||
console.error('bitcoind failure:', err, info);
|
||||
process.exit(2);
|
||||
}
|
||||
} else {
|
||||
console.error('bitcoind failure');
|
||||
process.exit(2);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
lightning.getInfo({}, function(err, info) {
|
||||
lightning.getInfo({}, function (err, info) {
|
||||
if (err) {
|
||||
console.error('lnd failure');
|
||||
console.dir(err);
|
||||
process.exit(3);
|
||||
}
|
||||
if (info) {
|
||||
if (!info.synced_to_chain) {
|
||||
console.info('lnd getinfo:', info);
|
||||
if (!info.synced_to_chain && !config.forceStart) {
|
||||
console.error('lnd not synced');
|
||||
process.exit(4);
|
||||
// process.exit(4);
|
||||
}
|
||||
identity_pubkey = info.identity_pubkey;
|
||||
}
|
||||
});
|
||||
|
||||
redis.info(function(err, info) {
|
||||
redis.info(function (err, info) {
|
||||
if (err || !info) {
|
||||
console.error('redis failure');
|
||||
process.exit(5);
|
||||
}
|
||||
});
|
||||
|
||||
const subscribeInvoicesCallCallback = async function (response) {
|
||||
if (response.state === 'SETTLED') {
|
||||
const LightningInvoiceSettledNotification = {
|
||||
memo: response.memo,
|
||||
preimage: response.r_preimage.toString('hex'),
|
||||
hash: response.r_hash.toString('hex'),
|
||||
amt_paid_sat: response.amt_paid_msat ? Math.floor(response.amt_paid_msat / 1000) : response.amt_paid_sat,
|
||||
};
|
||||
// obtaining a lock, to make sure we push to groundcontrol only once
|
||||
// since this web server can have several instances running, and each will get the same callback from LND
|
||||
// and dont release the lock - it will autoexpire in a while
|
||||
let lock = new Lock(redis, 'groundcontrol_hash_' + LightningInvoiceSettledNotification.hash);
|
||||
if (!(await lock.obtainLock())) {
|
||||
return;
|
||||
}
|
||||
let invoice = new Invo(redis, bitcoinclient, lightning);
|
||||
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();
|
||||
console.log('payment', LightningInvoiceSettledNotification.hash, 'was paid, posting to GroundControl...');
|
||||
const baseURI = process.env.GROUNDCONTROL;
|
||||
if (!baseURI) return;
|
||||
const _api = new Frisbee({ baseURI: baseURI });
|
||||
const apiResponse = await _api.post(
|
||||
'/lightningInvoiceGotSettled',
|
||||
Object.assign(
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: LightningInvoiceSettledNotification,
|
||||
},
|
||||
),
|
||||
);
|
||||
console.log('GroundControl:', apiResponse.originalResponse.status);
|
||||
}
|
||||
};
|
||||
let subscribeInvoicesCall = lightning.subscribeInvoices({});
|
||||
subscribeInvoicesCall.on('data', subscribeInvoicesCallCallback);
|
||||
subscribeInvoicesCall.on('status', function (status) {
|
||||
// The current status of the stream.
|
||||
});
|
||||
subscribeInvoicesCall.on('end', function () {
|
||||
// The server has closed the stream.
|
||||
});
|
||||
|
||||
let lightningDescribeGraph = {};
|
||||
function updateDescribeGraph() {
|
||||
console.log('updateDescribeGraph()');
|
||||
lightning.describeGraph({ include_unannounced: true }, function (err, response) {
|
||||
if (!err) lightningDescribeGraph = response;
|
||||
console.log('updated graph');
|
||||
});
|
||||
}
|
||||
if (config.enableUpdateDescribeGraph) {
|
||||
updateDescribeGraph();
|
||||
setInterval(updateDescribeGraph, 120000);
|
||||
}
|
||||
|
||||
// ######################## ROUTES ########################
|
||||
|
||||
router.post('/create', async function(req, res) {
|
||||
if (!(req.body.partnerid && req.body.partnerid === 'bluewallet' && req.body.accounttype)) return errorBadArguments(res);
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const postLimiter = rateLimit({
|
||||
windowMs: 30 * 60 * 1000,
|
||||
max: config.postRateLimit || 100,
|
||||
});
|
||||
|
||||
let u = new User(redis);
|
||||
router.post('/create', postLimiter, async function (req, res) {
|
||||
logger.log('/create', [req.id]);
|
||||
// Valid if the partnerid isn't there or is a string (same with accounttype)
|
||||
if (! (
|
||||
(!req.body.partnerid || (typeof req.body.partnerid === 'string' || req.body.partnerid instanceof String))
|
||||
&& (!req.body.accounttype || (typeof req.body.accounttype === 'string' || req.body.accounttype instanceof String))
|
||||
) ) return errorBadArguments(res);
|
||||
|
||||
if (config.sunset) return errorSunset(res);
|
||||
|
||||
let u = new User(redis, bitcoinclient, lightning);
|
||||
await u.create();
|
||||
await u.saveMetadata({ partnerid: req.body.partnerid, accounttype: req.body.accounttype, created_at: new Date().toISOString() });
|
||||
res.send({ login: u.getLogin(), password: u.getPassword() });
|
||||
});
|
||||
|
||||
router.post('/auth', async function(req, res) {
|
||||
router.post('/auth', postLimiter, async function (req, res) {
|
||||
logger.log('/auth', [req.id]);
|
||||
if (!((req.body.login && req.body.password) || req.body.refresh_token)) return errorBadArguments(res);
|
||||
|
||||
let u = new User(redis);
|
||||
let u = new User(redis, bitcoinclient, lightning);
|
||||
|
||||
if (req.body.refresh_token) {
|
||||
// need to refresh token
|
||||
@ -81,85 +174,181 @@ router.post('/auth', async function(req, res) {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/addinvoice', async function(req, res) {
|
||||
let u = new User(redis);
|
||||
router.post('/addinvoice', postLimiter, async function (req, res) {
|
||||
logger.log('/addinvoice', [req.id]);
|
||||
let u = new User(redis, bitcoinclient, lightning);
|
||||
if (!(await u.loadByAuthorization(req.headers.authorization))) {
|
||||
return errorBadAuth(res);
|
||||
}
|
||||
logger.log('/addinvoice', [req.id, 'userid: ' + u.getUserId()]);
|
||||
|
||||
if (!req.body.amt) return errorBadArguments(res);
|
||||
if (!req.body.amt || /*stupid NaN*/ !(req.body.amt > 0)) return errorBadArguments(res);
|
||||
|
||||
lightning.addInvoice({ memo: req.body.memo, value: req.body.amt }, async function(err, info) {
|
||||
if (err) return errorLnd(res);
|
||||
if (config.sunset) return errorSunsetAddInvoice(res);
|
||||
|
||||
info.pay_req = info.payment_request; // client backwards compatibility
|
||||
await u.saveUserInvoice(info);
|
||||
const invoice = new Invo(redis, bitcoinclient, lightning);
|
||||
const r_preimage = invoice.makePreimageHex();
|
||||
const invoice_args = { memo: req.body.memo, value: req.body.amt, expiry: 3600 * 24, r_preimage: Buffer.from(r_preimage, 'hex').toString('base64') };
|
||||
if (req.body.description_hash) {
|
||||
invoice_args.description_hash = Buffer.from(req.body.description_hash, 'hex').toString('base64')
|
||||
}
|
||||
lightning.addInvoice(
|
||||
invoice_args,
|
||||
async function (err, info) {
|
||||
if (err) return errorLnd(res);
|
||||
|
||||
res.send(info);
|
||||
});
|
||||
info.memo = req.body.memo;
|
||||
info.pay_req = info.payment_request; // client backwards compatibility
|
||||
await u.saveUserInvoice(info);
|
||||
await invoice.savePreimage(r_preimage);
|
||||
|
||||
res.send(info);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
router.post('/payinvoice', async function(req, res) {
|
||||
let u = new User(redis);
|
||||
router.post('/payinvoice', async function (req, res) {
|
||||
let u = new User(redis, bitcoinclient, lightning);
|
||||
if (!(await u.loadByAuthorization(req.headers.authorization))) {
|
||||
return errorBadAuth(res);
|
||||
}
|
||||
|
||||
logger.log('/payinvoice', [req.id, 'userid: ' + u.getUserId(), 'invoice: ' + req.body.invoice]);
|
||||
|
||||
if (!req.body.invoice) return errorBadArguments(res);
|
||||
let freeAmount = false;
|
||||
if (req.body.amount) {
|
||||
freeAmount = parseInt(req.body.amount);
|
||||
if (freeAmount <= 0) return errorBadArguments(res);
|
||||
}
|
||||
|
||||
let userBalance = await u.getBalance();
|
||||
// obtaining a lock
|
||||
let lock = new Lock(redis, 'invoice_paying_for_' + u.getUserId());
|
||||
if (!(await lock.obtainLock())) {
|
||||
return errorGeneralServerError(res);
|
||||
}
|
||||
|
||||
lightning.decodePayReq({ pay_req: req.body.invoice }, async function(err, info) {
|
||||
if (err) return errorNotAValidInvoice(res);
|
||||
let userBalance;
|
||||
try {
|
||||
userBalance = await u.getCalculatedBalance();
|
||||
} catch (Error) {
|
||||
logger.log('', [req.id, 'error running getCalculatedBalance():', Error.message]);
|
||||
lock.releaseLock();
|
||||
return errorTryAgainLater(res);
|
||||
}
|
||||
|
||||
if (userBalance >= info.num_satoshis) {
|
||||
// got enough balance
|
||||
lightning.decodePayReq({ pay_req: req.body.invoice }, async function (err, info) {
|
||||
if (err) {
|
||||
await lock.releaseLock();
|
||||
return errorNotAValidInvoice(res);
|
||||
}
|
||||
|
||||
if (+info.num_satoshis === 0) {
|
||||
// 'tip' invoices
|
||||
info.num_satoshis = freeAmount;
|
||||
}
|
||||
|
||||
logger.log('/payinvoice', [req.id, 'userBalance: ' + userBalance, 'num_satoshis: ' + info.num_satoshis]);
|
||||
|
||||
if (userBalance >= +info.num_satoshis + Math.floor(info.num_satoshis * forwardFee)) {
|
||||
// got enough balance, including 1% of payment amount - reserve for fees
|
||||
|
||||
if (identity_pubkey === info.destination) {
|
||||
// this is internal invoice
|
||||
// now, receiver add balance
|
||||
let userid_payee = await u.getUseridByPaymentHash(info.payment_hash);
|
||||
if (!userid_payee) return errorGeneralServerError(res);
|
||||
if (!userid_payee) {
|
||||
await lock.releaseLock();
|
||||
return errorGeneralServerError(res);
|
||||
}
|
||||
|
||||
let UserPayee = new User(redis);
|
||||
if (await u.getPaymentHashPaid(info.payment_hash)) {
|
||||
// this internal invoice was paid, no sense paying it again
|
||||
await lock.releaseLock();
|
||||
return errorLnd(res);
|
||||
}
|
||||
|
||||
let UserPayee = new User(redis, bitcoinclient, lightning);
|
||||
UserPayee._userid = userid_payee; // hacky, fixme
|
||||
let payee_balance = await UserPayee.getBalance();
|
||||
payee_balance += info.num_satoshis * 1;
|
||||
await UserPayee.saveBalance(payee_balance);
|
||||
await UserPayee.clearBalanceCache();
|
||||
|
||||
// sender spent his balance:
|
||||
userBalance -= info.num_satoshis * 1;
|
||||
await u.saveBalance(userBalance);
|
||||
await u.clearBalanceCache();
|
||||
await u.savePaidLndInvoice({
|
||||
timestamp: parseInt(+new Date() / 1000),
|
||||
type: 'paid_invoice',
|
||||
value: +info.num_satoshis + Math.floor(info.num_satoshis * internalFee),
|
||||
fee: Math.floor(info.num_satoshis * internalFee),
|
||||
memo: decodeURIComponent(info.description),
|
||||
pay_req: req.body.invoice,
|
||||
});
|
||||
|
||||
await u.setPaymentHashPaid(info.payment_hash);
|
||||
const invoice = new Invo(redis, bitcoinclient, lightning);
|
||||
invoice.setInvoice(req.body.invoice);
|
||||
await invoice.markAsPaidInDatabase();
|
||||
|
||||
// now, faking LND callback about invoice paid:
|
||||
const preimage = await invoice.getPreimage();
|
||||
if (preimage) {
|
||||
subscribeInvoicesCallCallback({
|
||||
state: 'SETTLED',
|
||||
memo: info.description,
|
||||
r_preimage: Buffer.from(preimage, 'hex'),
|
||||
r_hash: Buffer.from(info.payment_hash, 'hex'),
|
||||
amt_paid_sat: +info.num_satoshis,
|
||||
});
|
||||
}
|
||||
await lock.releaseLock();
|
||||
return res.send(info);
|
||||
}
|
||||
|
||||
// else - regular lightning network payment:
|
||||
|
||||
var call = lightning.sendPayment();
|
||||
call.on('data', function(payment) {
|
||||
call.on('data', async function (payment) {
|
||||
// payment callback
|
||||
await u.unlockFunds(req.body.invoice);
|
||||
if (payment && payment.payment_route && payment.payment_route.total_amt_msat) {
|
||||
userBalance -= +payment.payment_route.total_fees + +payment.payment_route.total_amt;
|
||||
u.saveBalance(userBalance);
|
||||
let PaymentShallow = new Paym(false, false, false);
|
||||
payment = PaymentShallow.processSendPaymentResponse(payment);
|
||||
payment.pay_req = req.body.invoice;
|
||||
payment.decoded = info;
|
||||
u.savePaidLndInvoice(payment);
|
||||
await u.savePaidLndInvoice(payment);
|
||||
await u.clearBalanceCache();
|
||||
lock.releaseLock();
|
||||
res.send(payment);
|
||||
} else {
|
||||
// payment failed
|
||||
return errorLnd(res);
|
||||
lock.releaseLock();
|
||||
return errorPaymentFailed(res);
|
||||
}
|
||||
});
|
||||
let inv = { payment_request: req.body.invoice };
|
||||
call.write(inv);
|
||||
if (!info.num_satoshis) {
|
||||
// tip invoice, but someone forgot to specify amount
|
||||
await lock.releaseLock();
|
||||
return errorBadArguments(res);
|
||||
}
|
||||
let inv = {
|
||||
payment_request: req.body.invoice,
|
||||
amt: info.num_satoshis, // amt is used only for 'tip' invoices
|
||||
fee_limit: { fixed: Math.floor(info.num_satoshis * forwardFee) + 1 },
|
||||
};
|
||||
try {
|
||||
await u.lockFunds(req.body.invoice, info);
|
||||
call.write(inv);
|
||||
} catch (Err) {
|
||||
await lock.releaseLock();
|
||||
return errorPaymentFailed(res);
|
||||
}
|
||||
} else {
|
||||
await lock.releaseLock();
|
||||
return errorNotEnougBalance(res);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/getbtc', async function(req, res) {
|
||||
router.get('/getbtc', async function (req, res) {
|
||||
logger.log('/getbtc', [req.id]);
|
||||
let u = new User(redis, bitcoinclient, lightning);
|
||||
await u.loadByAuthorization(req.headers.authorization);
|
||||
|
||||
@ -167,66 +356,121 @@ router.get('/getbtc', async function(req, res) {
|
||||
return errorBadAuth(res);
|
||||
}
|
||||
|
||||
if (config.sunset) return errorSunsetAddInvoice(res);
|
||||
|
||||
let address = await u.getAddress();
|
||||
if (!address) {
|
||||
await u.generateAddress();
|
||||
address = await u.getAddress();
|
||||
}
|
||||
u.watchAddress(address);
|
||||
|
||||
res.send([{ address }]);
|
||||
});
|
||||
|
||||
router.get('/balance', async function(req, res) {
|
||||
router.get('/checkpayment/:payment_hash', async function (req, res) {
|
||||
logger.log('/checkpayment', [req.id]);
|
||||
let u = new User(redis, bitcoinclient, lightning);
|
||||
await u.loadByAuthorization(req.headers.authorization);
|
||||
|
||||
if (!u.getUserId()) {
|
||||
return errorBadAuth(res);
|
||||
}
|
||||
|
||||
let paid = true;
|
||||
if (!(await u.getPaymentHashPaid(req.params.payment_hash))) {
|
||||
// Not found on cache
|
||||
paid = await u.syncInvoicePaid(req.params.payment_hash);
|
||||
}
|
||||
res.send({ paid: paid });
|
||||
});
|
||||
|
||||
router.get('/balance', postLimiter, async function (req, res) {
|
||||
let u = new User(redis, bitcoinclient, lightning);
|
||||
try {
|
||||
logger.log('/balance', [req.id]);
|
||||
if (!(await u.loadByAuthorization(req.headers.authorization))) {
|
||||
return errorBadAuth(res);
|
||||
}
|
||||
logger.log('/balance', [req.id, 'userid: ' + u.getUserId()]);
|
||||
|
||||
if (!(await u.getAddress())) await u.generateAddress(); // onchain address needed further
|
||||
await u.accountForPosibleTxids();
|
||||
let balance = await u.getBalance();
|
||||
if (balance < 0) balance = 0;
|
||||
res.send({ BTC: { AvailableBalance: balance } });
|
||||
} catch (Error) {
|
||||
logger.log('', [req.id, 'error getting balance:', Error, 'userid:', u.getUserId()]);
|
||||
return errorGeneralServerError(res);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/getinfo', postLimiter, async function (req, res) {
|
||||
logger.log('/getinfo', [req.id]);
|
||||
let u = new User(redis, bitcoinclient, lightning);
|
||||
if (!(await u.loadByAuthorization(req.headers.authorization))) {
|
||||
return errorBadAuth(res);
|
||||
}
|
||||
|
||||
if (!(await u.getAddress())) await u.generateAddress(); // onchain address needed further
|
||||
await u.accountForPosibleTxids();
|
||||
let balance = await u.getBalance();
|
||||
res.send({ BTC: { AvailableBalance: balance } });
|
||||
});
|
||||
|
||||
router.get('/getinfo', async function(req, res) {
|
||||
let u = new User(redis);
|
||||
if (!(await u.loadByAuthorization(req.headers.authorization))) {
|
||||
return errorBadAuth(res);
|
||||
}
|
||||
|
||||
lightning.getInfo({}, function(err, info) {
|
||||
lightning.getInfo({}, function (err, info) {
|
||||
if (err) return errorLnd(res);
|
||||
res.send(info);
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/gettxs', async function(req, res) {
|
||||
router.get('/gettxs', async function (req, res) {
|
||||
logger.log('/gettxs', [req.id]);
|
||||
let u = new User(redis, bitcoinclient, lightning);
|
||||
if (!(await u.loadByAuthorization(req.headers.authorization))) {
|
||||
return errorBadAuth(res);
|
||||
}
|
||||
logger.log('/gettxs', [req.id, 'userid: ' + u.getUserId()]);
|
||||
|
||||
if (!(await u.getAddress())) await u.generateAddress(); // onchain addr needed further
|
||||
await u.accountForPosibleTxids();
|
||||
let txs = await u.getTxs();
|
||||
res.send(txs);
|
||||
try {
|
||||
await u.accountForPosibleTxids();
|
||||
let txs = await u.getTxs();
|
||||
let lockedPayments = await u.getLockedPayments();
|
||||
for (let locked of lockedPayments) {
|
||||
txs.push({
|
||||
type: 'paid_invoice',
|
||||
fee: Math.floor(locked.amount * forwardFee) /* feelimit */,
|
||||
value: locked.amount + Math.floor(locked.amount * forwardFee) /* feelimit */,
|
||||
timestamp: locked.timestamp,
|
||||
memo: 'Payment in transition',
|
||||
});
|
||||
}
|
||||
res.send(txs);
|
||||
} catch (Err) {
|
||||
logger.log('', [req.id, 'error gettxs:', Err.message, 'userid:', u.getUserId()]);
|
||||
res.send([]);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/getuserinvoices', async function(req, res) {
|
||||
router.get('/getuserinvoices', postLimiter, async function (req, res) {
|
||||
logger.log('/getuserinvoices', [req.id]);
|
||||
let u = new User(redis, bitcoinclient, lightning);
|
||||
if (!(await u.loadByAuthorization(req.headers.authorization))) {
|
||||
return errorBadAuth(res);
|
||||
}
|
||||
logger.log('/getuserinvoices', [req.id, 'userid: ' + u.getUserId()]);
|
||||
|
||||
let invoices = await u.getUserInvoices();
|
||||
res.send(invoices);
|
||||
try {
|
||||
let invoices = await u.getUserInvoices(req.query.limit);
|
||||
res.send(invoices);
|
||||
} catch (Err) {
|
||||
logger.log('', [req.id, 'error getting user invoices:', Err.message, 'userid:', u.getUserId()]);
|
||||
res.send([]);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/getpending', async function(req, res) {
|
||||
router.get('/getpending', async function (req, res) {
|
||||
logger.log('/getpending', [req.id]);
|
||||
let u = new User(redis, bitcoinclient, lightning);
|
||||
if (!(await u.loadByAuthorization(req.headers.authorization))) {
|
||||
return errorBadAuth(res);
|
||||
}
|
||||
logger.log('/getpending', [req.id, 'userid: ' + u.getUserId()]);
|
||||
|
||||
if (!(await u.getAddress())) await u.generateAddress(); // onchain address needed further
|
||||
await u.accountForPosibleTxids();
|
||||
@ -234,22 +478,24 @@ router.get('/getpending', async function(req, res) {
|
||||
res.send(txs);
|
||||
});
|
||||
|
||||
router.get('/decodeinvoice', async function(req, res) {
|
||||
let u = new User(redis, bitcoinclient);
|
||||
router.get('/decodeinvoice', async function (req, res) {
|
||||
logger.log('/decodeinvoice', [req.id]);
|
||||
let u = new User(redis, bitcoinclient, lightning);
|
||||
if (!(await u.loadByAuthorization(req.headers.authorization))) {
|
||||
return errorBadAuth(res);
|
||||
}
|
||||
|
||||
if (!req.query.invoice) return errorGeneralServerError(res);
|
||||
|
||||
lightning.decodePayReq({ pay_req: req.query.invoice }, function(err, info) {
|
||||
lightning.decodePayReq({ pay_req: req.query.invoice }, function (err, info) {
|
||||
if (err) return errorNotAValidInvoice(res);
|
||||
res.send(info);
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/checkrouteinvoice', async function(req, res) {
|
||||
let u = new User(redis, bitcoinclient);
|
||||
router.get('/checkrouteinvoice', async function (req, res) {
|
||||
logger.log('/checkrouteinvoice', [req.id]);
|
||||
let u = new User(redis, bitcoinclient, lightning);
|
||||
if (!(await u.loadByAuthorization(req.headers.authorization))) {
|
||||
return errorBadAuth(res);
|
||||
}
|
||||
@ -258,12 +504,40 @@ router.get('/checkrouteinvoice', async function(req, res) {
|
||||
|
||||
// at the momment does nothing.
|
||||
// TODO: decode and query actual route to destination
|
||||
lightning.decodePayReq({ pay_req: req.query.invoice }, function(err, info) {
|
||||
lightning.decodePayReq({ pay_req: req.query.invoice }, function (err, info) {
|
||||
if (err) return errorNotAValidInvoice(res);
|
||||
res.send(info);
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/queryroutes/:source/:dest/:amt', async function (req, res) {
|
||||
logger.log('/queryroutes', [req.id]);
|
||||
|
||||
let request = {
|
||||
pub_key: req.params.dest,
|
||||
use_mission_control: true,
|
||||
amt: req.params.amt,
|
||||
source_pub_key: req.params.source,
|
||||
};
|
||||
lightning.queryRoutes(request, function (err, response) {
|
||||
console.log(JSON.stringify(response, null, 2));
|
||||
res.send(response);
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/getchaninfo/:chanid', async function (req, res) {
|
||||
logger.log('/getchaninfo', [req.id]);
|
||||
|
||||
if (lightningDescribeGraph && lightningDescribeGraph.edges) {
|
||||
for (const edge of lightningDescribeGraph.edges) {
|
||||
if (edge.channel_id == req.params.chanid) {
|
||||
return res.send(JSON.stringify(edge, null, 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
res.send('');
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
// ################# HELPERS ###########################
|
||||
@ -280,7 +554,7 @@ function errorNotEnougBalance(res) {
|
||||
return res.send({
|
||||
error: true,
|
||||
code: 2,
|
||||
message: 'not enough balance',
|
||||
message: 'not enough balance. Make sure you have at least 1% reserved for potential fees',
|
||||
});
|
||||
}
|
||||
|
||||
@ -304,7 +578,7 @@ function errorGeneralServerError(res) {
|
||||
return res.send({
|
||||
error: true,
|
||||
code: 6,
|
||||
message: 'Server fault',
|
||||
message: 'Something went wrong. Please try again later',
|
||||
});
|
||||
}
|
||||
|
||||
@ -315,3 +589,35 @@ function errorBadArguments(res) {
|
||||
message: 'Bad arguments',
|
||||
});
|
||||
}
|
||||
|
||||
function errorTryAgainLater(res) {
|
||||
return res.send({
|
||||
error: true,
|
||||
code: 9,
|
||||
message: 'Your previous payment is in transit. Try again in 5 minutes',
|
||||
});
|
||||
}
|
||||
|
||||
function errorPaymentFailed(res) {
|
||||
return res.send({
|
||||
error: true,
|
||||
code: 10,
|
||||
message: 'Payment failed. Does the receiver have enough inbound capacity?',
|
||||
});
|
||||
}
|
||||
|
||||
function errorSunset(res) {
|
||||
return res.send({
|
||||
error: true,
|
||||
code: 11,
|
||||
message: 'This LNDHub instance is not accepting any more users',
|
||||
});
|
||||
}
|
||||
|
||||
function errorSunsetAddInvoice(res) {
|
||||
return res.send({
|
||||
error: true,
|
||||
code: 11,
|
||||
message: 'This LNDHub instance is scheduled to shut down. Withdraw any remaining funds',
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,28 +1,118 @@
|
||||
let express = require('express');
|
||||
let router = express.Router();
|
||||
let fs = require('fs');
|
||||
let mustache = require('mustache');
|
||||
let lightning = require('../lightning');
|
||||
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');
|
||||
|
||||
router.get('/', function(req, res) {
|
||||
let html = fs.readFileSync('./templates/index.html').toString('utf8');
|
||||
lightning.getInfo({}, function(err, info) {
|
||||
if (err) {
|
||||
console.error('lnd failure');
|
||||
process.exit(3);
|
||||
}
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
return res.status(200).send(mustache.render(html, info));
|
||||
});
|
||||
});
|
||||
let lightningGetInfo = {};
|
||||
let lightningListChannels = {};
|
||||
function updateLightning() {
|
||||
console.log('updateLightning()');
|
||||
try {
|
||||
lightning.getInfo({}, function (err, info) {
|
||||
if (err) {
|
||||
console.error('lnd failure:', err);
|
||||
process.exit(4);
|
||||
return;
|
||||
}
|
||||
lightningGetInfo = info;
|
||||
});
|
||||
|
||||
router.get('/about', function(req, res) {
|
||||
let html = fs.readFileSync('./templates/about.html').toString('utf8');
|
||||
lightning.listChannels({}, function (err, response) {
|
||||
if (err) {
|
||||
console.error('lnd failure:', err);
|
||||
process.exit(4);
|
||||
return;
|
||||
}
|
||||
console.log('updated');
|
||||
lightningListChannels = response;
|
||||
let channels = [];
|
||||
let max_chan_capacity = -1;
|
||||
for (const channel of lightningListChannels.channels) {
|
||||
max_chan_capacity = Math.max(max_chan_capacity, channel.capacity);
|
||||
}
|
||||
for (let channel of lightningListChannels.channels) {
|
||||
let magic = max_chan_capacity / 100;
|
||||
channel.local = channel.local_balance * 1;
|
||||
channel.total = channel.capacity * 1;
|
||||
channel.size = Math.round(channel.capacity / magic); // total size of the bar on page. 100% means it takes maximum width
|
||||
channel.capacity_btc = channel.capacity / 100000000;
|
||||
channel.name = pubkey2name[channel.remote_pubkey];
|
||||
if (channel.name) {
|
||||
channels.unshift(channel);
|
||||
} else {
|
||||
channels.push(channel);
|
||||
}
|
||||
}
|
||||
lightningListChannels.channels = channels;
|
||||
});
|
||||
} catch (Err) {
|
||||
console.log(Err);
|
||||
}
|
||||
}
|
||||
updateLightning();
|
||||
setInterval(updateLightning, 60000);
|
||||
|
||||
const pubkey2name = {
|
||||
'03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56': 'yalls.org',
|
||||
'0232e20e7b68b9b673fb25f48322b151a93186bffe4550045040673797ceca43cf': 'zigzag.io',
|
||||
'02df5ffe895c778e10f7742a6c5b8a0cefbe9465df58b92fadeb883752c8107c8f': 'blockstream store',
|
||||
'030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f': 'bitrefill.com',
|
||||
'03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f': 'ACINQ',
|
||||
'03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e': 'OpenNode',
|
||||
'028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4': 'OpenNode 2',
|
||||
'0242a4ae0c5bef18048fbecf995094b74bfb0f7391418d71ed394784373f41e4f3': 'coingate.com',
|
||||
'0279c22ed7a068d10dc1a38ae66d2d6461e269226c60258c021b1ddcdfe4b00bc4': 'ln1.satoshilabs.com',
|
||||
'02c91d6aa51aa940608b497b6beebcb1aec05be3c47704b682b3889424679ca490': 'lnd-21.LNBIG.com',
|
||||
'024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca': 'satoshis.place',
|
||||
'03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda': 'tippin.me',
|
||||
'022c699df736064b51a33017abfc4d577d133f7124ac117d3d9f9633b6297a3b6a': 'globee.com',
|
||||
'0237fefbe8626bf888de0cad8c73630e32746a22a2c4faa91c1d9877a3826e1174': '1.ln.aantonop.com',
|
||||
'026c7d28784791a4b31a64eb34d9ab01552055b795919165e6ae886de637632efb': 'LivingRoomOfSatoshi',
|
||||
'02816caed43171d3c9854e3b0ab2cf0c42be086ff1bd4005acc2a5f7db70d83774': 'ln.pizza',
|
||||
'0254ff808f53b2f8c45e74b70430f336c6c76ba2f4af289f48d6086ae6e60462d3': 'bitrefill thor',
|
||||
'03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac': 'bitrefill 3',
|
||||
'02a0bc43557fae6af7be8e3a29fdebda819e439bea9c0f8eb8ed6a0201f3471ca9': 'LightningPeachHub',
|
||||
'02d4531a2f2e6e5a9033d37d548cff4834a3898e74c3abe1985b493c42ebbd707d': 'coinfinity.co',
|
||||
'02d23fa6794d8fd056c757f3c8f4877782138dafffedc831fc570cab572620dc61': 'paywithmoon.com',
|
||||
'025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5': 'paywithmoon.com',
|
||||
'02004c625d622245606a1ea2c1c69cfb4516b703b47945a3647713c05fe4aaeb1c': 'walletofsatoshi',
|
||||
'0331f80652fb840239df8dc99205792bba2e559a05469915804c08420230e23c7c': 'LightningPowerUsers.com',
|
||||
'033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025': 'bfx-lnd0',
|
||||
'03021c5f5f57322740e4ee6936452add19dc7ea7ccf90635f95119ab82a62ae268': 'lnd1.bluewallet.io',
|
||||
'037cc5f9f1da20ac0d60e83989729a204a33cc2d8e80438969fadf35c1c5f1233b': 'lnd2.bluewallet.io',
|
||||
'036b53093df5a932deac828cca6d663472dbc88322b05eec1d42b26ab9b16caa1c': 'okcoin',
|
||||
'038f8f113c580048d847d6949371726653e02b928196bad310e3eda39ff61723f6': 'magnetron',
|
||||
'03829249ef39746fd534a196510232df08b83db0967804ec71bf4120930864ff97': 'blokada.org',
|
||||
'02ce691b2e321954644514db708ba2a72769a6f9142ac63e65dd87964e9cf2add9': 'Satoshis.Games',
|
||||
};
|
||||
|
||||
router.get('/', function (req, res) {
|
||||
logger.log('/', [req.id]);
|
||||
if (!lightningGetInfo) {
|
||||
console.error('lnd failure');
|
||||
process.exit(3);
|
||||
}
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
return res.status(200).send(mustache.render(html, {}));
|
||||
let html = fs.readFileSync('./templates/index.html').toString('utf8');
|
||||
return res.status(200).send(mustache.render(html, Object.assign({}, lightningGetInfo, lightningListChannels)));
|
||||
});
|
||||
|
||||
router.use(function(req, res) {
|
||||
router.get('/qr', function (req, res) {
|
||||
let host = req.headers.host;
|
||||
if (process.env.TOR_URL) {
|
||||
host = process.env.TOR_URL;
|
||||
}
|
||||
const customPath = req.url.replace('/qr', '');
|
||||
const url = 'bluewallet:setlndhuburl?url=' + encodeURIComponent(req.protocol + '://' + host + customPath);
|
||||
var code = qr.image(url, { type: 'png' });
|
||||
res.setHeader('Content-type', 'image/png');
|
||||
code.pipe(res);
|
||||
});
|
||||
|
||||
router.use(function (req, res) {
|
||||
res.status(404).send('404');
|
||||
});
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
# User story
|
||||
- *As a user, I want to have ability to topup my balance with Bitcoin and send payments within Lightning network.*
|
||||
- *As a product owner, I want to have transparent usage statistic and run-time information on payment channels and environment.*
|
||||
- *As a product owner, I want to have transparent usage statistics and run-time information on payment channels and environment.*
|
||||
|
||||
# Basics
|
||||
|
||||
1. LndHub API is standalone software and needs LND client syncronized and running. LndHub API is not a Lightning wallet
|
||||
in terms of funds storage, it operates whole amout of available funds on channels. User's balances and transactions
|
||||
1. LndHub API is standalone software and needs LND client synchronized and running. LndHub API is not a Lightning wallet
|
||||
in terms of funds storage, it operates whole amount of available funds on channels. User's balances and transactions
|
||||
stored in internal database.
|
||||
|
||||
2. LndHub API is accessible for everyone, but only `/create` can be called without authorization token.
|
||||
@ -16,13 +16,13 @@ for Lightning payments.
|
||||
|
||||
4. gRPC RPC framework is used for communication with LND. See https://github.com/lightningnetwork/lnd/tree/master/lnrpc
|
||||
|
||||
5. Outh2 library, MongoDB and Golang backend is used for API implementation. Every request from user is sighned and
|
||||
5. Outh2 library, MongoDB and Golang backend is used for API implementation. Every request from user is signed and
|
||||
associated with corresponding user id.
|
||||
|
||||
6. Double entry system is used for internal accounting https://en.wikipedia.org/wiki/Double-entry_bookkeeping_system
|
||||
6.1. Internal accounting requirements https://github.com/matveyco/lnd-wallet-api-spec/edit/master/Accounting-requirements.md
|
||||
|
||||
7. All amounts are satoshis (int), althrough millisatoshis are used in LND internally (rounding is up to server implementation).
|
||||
7. All amounts are satoshis (int), although millisatoshis are used in LND internally (rounding is up to server implementation).
|
||||
|
||||
8. Every account has its separate Lightning, BTC addresses and unique session. If user runs few accounts from one device or wallet, corresponding amount of sessions should be opened.
|
||||
|
||||
@ -33,10 +33,10 @@ associated with corresponding user id.
|
||||
| Call | Method | Handler | Params | Return | Description |
|
||||
| ------------- | ------------- | ------------- | ------------- | ------------- | ------------- |
|
||||
| Create Account | POST | /create | {none} | JSON Auth Data | Create new user account and get credentials |
|
||||
| Authorize | POST | /auth | auth params (login/password of refresh_token) | JSON token data | Authorize user with Oauth. When user use refresh_token to auth, then this refresh_token not available for access once again. Use new refresh_token |
|
||||
| Authorize | POST | /auth | auth params (login/password or refresh_token) | JSON token data | Authorize user with Oauth. When user use refresh_token to auth, then this refresh_token not available for access once again. Use new refresh_token |
|
||||
| Get token | POST | /oauth2/token | user id, secret, grant_type and scope | token data | Get token data from user id, secret, grant_type and scope |
|
||||
| Get BTC Addr | GET | /getbtc | {none} | Text address | Get user's BTC address to top-up his account |
|
||||
| New BTC Addr | POST | /newbtc | {none} | Text address | Create new BTC address for user. Old addresses should remain valid, so if user accidentialy sends money to old address transaction will be assigned to his account |
|
||||
| New BTC Addr | POST | /newbtc | {none} | Text address | Create new BTC address for user. Old addresses should remain valid, so if user accidentaly sends money to old address transaction will be assigned to his account |
|
||||
| Get Pending Balance | GET | /getpending | {none} | JSON | Get information about BTC pending transactions which have less than 3 confirmations |
|
||||
| Decode Invoice | GET | /decodeinvoice | Invoice string | JSON | Decode invoice from invoice string. If invoice is represented as QR-code, fronted device should decode it first |
|
||||
| Check Route | GET | /checkroute | Payment destination | Success | Check if payment destination is available and invoice could be paid |
|
||||
@ -45,7 +45,7 @@ associated with corresponding user id.
|
||||
| Get transactions | GET | /gettxs | Offset, limit | JSON array | Get transactions for a wallet. With load offset at limit |
|
||||
| Get transaction | GET | /gettx | Tx id | JSON | Get tx info by its ID |
|
||||
| Get balance| GET | /balance | {none} | int64 | Available unspent internal balance (in Satoshis)
|
||||
| Get info | GET | /getinfo | {none} | JSON | Tech info. Fee on transactions for current user (0 for a start), availble actual funds on channel, maximum tx size, service status etc.
|
||||
| Get info | GET | /getinfo | {none} | JSON | Tech info. Fee on transactions for current user (0 for a start), available actual funds on channel, maximum tx size, service status etc.
|
||||
| Get info | POST | /addinvoice | JSON | JSON | Create invoice.
|
||||
| Get info | GET | /getuserinvoices | {none} | JSON | List of invoices created by user.
|
||||
|
||||
@ -70,9 +70,9 @@ Response is always JSON.
|
||||
`error:true` should be always present.
|
||||
|
||||
{
|
||||
"error" : true, // boolean
|
||||
"code" : 1, // int
|
||||
"message": "..." // string
|
||||
"error" : true, // boolean
|
||||
"code" : 1, // int
|
||||
"message": "..." // string
|
||||
}
|
||||
|
||||
Error code | Error message
|
||||
@ -95,15 +95,15 @@ Create new user account and get credentials. Not whitelisted partners should ret
|
||||
Request:
|
||||
|
||||
{
|
||||
"partnerid" : "bluewallet" // string, not mandatory parameter
|
||||
"accounttype" : "..." // string, not mandatory, default is common, also can be test or core
|
||||
"partnerid" : "bluewallet" // string, not mandatory parameter
|
||||
"accounttype" : "..." // string, not mandatory, default is common, also can be test or core
|
||||
}
|
||||
|
||||
Response:
|
||||
|
||||
{
|
||||
"login":"...", // srting
|
||||
"password":"...", // srting
|
||||
"login":"...", // string
|
||||
"password":"...", // string
|
||||
}
|
||||
|
||||
## POST /auth?type=auth
|
||||
@ -113,16 +113,16 @@ Authorize user with Oauth user and login
|
||||
Request:
|
||||
|
||||
{
|
||||
"login": "...", //string
|
||||
"password": "..." //string
|
||||
"login": "...", // string
|
||||
"password": "..." // string
|
||||
}
|
||||
|
||||
Response:
|
||||
|
||||
{
|
||||
"access_token": "...", //string
|
||||
"token_type": "...", //string
|
||||
"refresh_token": "...", //string
|
||||
"access_token": "...", // string
|
||||
"token_type": "...", // string
|
||||
"refresh_token": "...", // string
|
||||
"expiry": "0001-01-01T00:00:00Z" // datetime
|
||||
}
|
||||
|
||||
@ -135,16 +135,16 @@ Authorize user with Oauth user and login
|
||||
Request:
|
||||
|
||||
{
|
||||
"refresh_token": "...", //string
|
||||
"refresh_token": "...", // string
|
||||
}
|
||||
|
||||
Response:
|
||||
|
||||
{
|
||||
"access_token": "...", //string
|
||||
"token_type": "...", //string
|
||||
"refresh_token": "...", //string
|
||||
"expiry": "0001-01-01T00:00:00Z" // datetime
|
||||
"access_token": "...", // string
|
||||
"token_type": "...", // string
|
||||
"refresh_token": "...", // string
|
||||
"expiry": "0001-01-01T00:00:00Z" // datetime
|
||||
}
|
||||
|
||||
## POST /oauth2/token
|
||||
@ -154,17 +154,17 @@ Authorize user with Oauth user and login
|
||||
Request:
|
||||
|
||||
{
|
||||
"grant_type": "client_credentials", //string
|
||||
"client_id": "...", //string
|
||||
"grant_type": "client_credentials", // string
|
||||
"client_id": "...", // string
|
||||
"client_secret": "..." // string
|
||||
}
|
||||
|
||||
Response:
|
||||
|
||||
{
|
||||
"access_token": "...", //string
|
||||
"token_type": "...", //string
|
||||
"refresh_token": "...", //string
|
||||
"access_token": "...", // string
|
||||
"token_type": "...", // string
|
||||
"refresh_token": "...", // string
|
||||
"expiry": "0001-01-01T00:00:00Z" // datetime
|
||||
}
|
||||
|
||||
@ -185,7 +185,7 @@ Response:
|
||||
|
||||
## POST /newbtc
|
||||
|
||||
Create new BTC address for user. Old addresses should remain valid, so if user accidentialy sends
|
||||
Create new BTC address for user. Old addresses should remain valid, so if user accidentaly sends
|
||||
money to old address transaction will be assigned to his account
|
||||
|
||||
Request:
|
||||
@ -229,34 +229,34 @@ Request:
|
||||
Response:
|
||||
|
||||
{
|
||||
"destination": "...", //string, lnd node address
|
||||
"payment_hash": "...", //string
|
||||
"num_satoshis": "78497", //string, satoshis
|
||||
"timestamp": "1534430501", //string, unixtime
|
||||
"expiry": "3600", //string, seconds
|
||||
"description": "...", //string
|
||||
"description_hash": "", //string
|
||||
"fallback_addr": "...", //string, fallback on-chain address
|
||||
"cltv_expiry": "...", //string, delta to use for the time-lock of the CLTV extended to the final hop
|
||||
"destination": "...", // string, lnd node address
|
||||
"payment_hash": "...", // string
|
||||
"num_satoshis": "78497", // string, satoshis
|
||||
"timestamp": "1534430501", // string, unixtime
|
||||
"expiry": "3600", // string, seconds
|
||||
"description": "...", // string
|
||||
"description_hash": "", // string
|
||||
"fallback_addr": "...", // string, fallback on-chain address
|
||||
"cltv_expiry": "...", // string, delta to use for the time-lock of the CLTV extended to the final hop
|
||||
"route_hints": [
|
||||
{
|
||||
"hop_hints" : [
|
||||
{
|
||||
"node_id": "..", //string, the public key of the node at the start of the
|
||||
"node_id": "..", // string, the public key of the node at the start of the
|
||||
// channel.
|
||||
|
||||
"chan_id": ..., //int, the unique identifier of the channel.
|
||||
"chan_id": ..., // int, the unique identifier of the channel.
|
||||
|
||||
"fee_base_msat": ..., //int, The base fee of the channel denominated in
|
||||
"fee_base_msat": ..., // int, The base fee of the channel denominated in
|
||||
// millisatoshis.
|
||||
|
||||
"fee_proportional_millionths": ...,
|
||||
//int, the fee rate of the channel
|
||||
// int, the fee rate of the channel
|
||||
// for sending one satoshi across it denominated
|
||||
// in millionths of a satoshi
|
||||
|
||||
"cltv_expiry_delta": ...
|
||||
//int, the fee rate of the channel for sending one satoshi
|
||||
// int, the fee rate of the channel for sending one satoshi
|
||||
// across it denominated in millionths of a satoshi
|
||||
}, ...
|
||||
]
|
||||
@ -288,7 +288,7 @@ Request:
|
||||
|
||||
{
|
||||
"destination" : "..." // string, destination lnd node address
|
||||
"amt": "..." // string,
|
||||
"amt": "..." // string,
|
||||
}
|
||||
|
||||
Response:
|
||||
@ -311,26 +311,26 @@ Request:
|
||||
Response:
|
||||
|
||||
{
|
||||
"payment_error": "..." //string
|
||||
"payment_preimage": "..." //string
|
||||
"payment_route": {
|
||||
"total_time_lock": ... , //int
|
||||
"total_fees": ... , //int
|
||||
"total_amt": ... , //int
|
||||
"total_fees_msat": ... , //int
|
||||
"total_amt_msat": ... , //int
|
||||
"hops": [
|
||||
{
|
||||
"chan_id": ... , //int
|
||||
"chan_capacity": ... , //int
|
||||
"amt_to_forward": ... , //int
|
||||
"fee": ... , //int
|
||||
"expiry": ... , //int
|
||||
"amt_to_forward_msat": ... , //int
|
||||
"fee_msat": ... , //int
|
||||
},
|
||||
]
|
||||
}
|
||||
"payment_error": "..." // string
|
||||
"payment_preimage": "..." // string
|
||||
"payment_route": {
|
||||
"total_time_lock": ... , // int
|
||||
"total_fees": ... , // int
|
||||
"total_amt": ... , // int
|
||||
"total_fees_msat": ... , // int
|
||||
"total_amt_msat": ... , // int
|
||||
"hops": [
|
||||
{
|
||||
"chan_id": ... , // int
|
||||
"chan_capacity": ... , // int
|
||||
"amt_to_forward": ... , // int
|
||||
"fee": ... , // int
|
||||
"expiry": ... , // int
|
||||
"amt_to_forward_msat": ... , // int
|
||||
"fee_msat": ... , // int
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
## POST /sendcoins
|
||||
@ -351,19 +351,19 @@ Response:
|
||||
|
||||
## GET /gettxs
|
||||
|
||||
Get successfull lightning and btc transactions user made. Order newest to oldest.
|
||||
Get successful lightning and btc transactions user made. Order newest to oldest.
|
||||
|
||||
Request:
|
||||
|
||||
{
|
||||
"limit" : 10, // INT
|
||||
"offset": 0, // INT
|
||||
"limit" : 10, // INT
|
||||
"offset": 0, // INT
|
||||
}
|
||||
|
||||
Response:
|
||||
|
||||
{
|
||||
[ // array of Transaction object (see below)
|
||||
[ // array of Transaction object (see below)
|
||||
{
|
||||
...
|
||||
}
|
||||
@ -372,7 +372,7 @@ Response:
|
||||
|
||||
## GET /gettx
|
||||
|
||||
Get info on successfull lighning transaction user made. TXID is an internal LndHub identifier,
|
||||
Get info on successful lightning transaction user made. TXID is an internal LndHub identifier,
|
||||
no relation to onchain bitcoin txid.
|
||||
|
||||
Request:
|
||||
@ -398,10 +398,10 @@ Request:
|
||||
Response:
|
||||
|
||||
{
|
||||
"BTC": { //string, currency
|
||||
"TotalBalance": 109388, //int, satoshis
|
||||
"BTC": { // string, currency
|
||||
"TotalBalance": 109388, // int, satoshis
|
||||
"AvailableBalance": 109388, // int, satoshis
|
||||
"UncomfirmedBalance": 0 //int, satoshis
|
||||
"UncomfirmedBalance": 0 // int, satoshis
|
||||
}, ...
|
||||
//now available only btc balance
|
||||
|
||||
@ -422,50 +422,52 @@ Response:
|
||||
"fee": 0, // int, in cents of percent, i.e. 100 for 1%, 50 for 0.5%, 1 for 0.01%
|
||||
|
||||
|
||||
"identity_pubkey": "...", //string, lnd node identity pubkey
|
||||
"alias": "...", //string, lnd node alias
|
||||
"num_pending_channels": 0, //int
|
||||
"num_active_channels": 3, //int
|
||||
"num_peers": 6, //int
|
||||
"block_height": 542389, //int
|
||||
"block_hash": "...", //string
|
||||
"synced_to_chain": true, //bool
|
||||
"testnet": false,
|
||||
"chains": [
|
||||
"bitcoin" //string, available chans to operate by lnd
|
||||
],
|
||||
"uris": [
|
||||
"...", //string, uris of lnd node
|
||||
],
|
||||
"best_header_timestamp": "...", //string, unixtime
|
||||
"version": "..." // string, lnd version
|
||||
"identity_pubkey": "...", // string, lnd node identity pubkey
|
||||
"alias": "...", // string, lnd node alias
|
||||
"num_pending_channels": 0, // int
|
||||
"num_active_channels": 3, // int
|
||||
"num_peers": 6, // int
|
||||
"block_height": 542389, // int
|
||||
"block_hash": "...", // string
|
||||
"synced_to_chain": true, // bool
|
||||
"testnet": false,
|
||||
"chains": [
|
||||
"bitcoin" // string, available chans to operate by lnd
|
||||
],
|
||||
"uris": [
|
||||
"...", // string, uris of lnd node
|
||||
],
|
||||
"best_header_timestamp": "...", // string, unixtime
|
||||
"version": "..." // string, lnd version
|
||||
}
|
||||
|
||||
## GET /getaddinvoice
|
||||
## GET /getinvoice
|
||||
|
||||
Returns fees user pays for payments, status of the system, etc.
|
||||
|
||||
Request:
|
||||
|
||||
{
|
||||
"amt": "...", //string
|
||||
"memo":"...", //string
|
||||
"receipt":"...", //string, not mandatory parameter
|
||||
"preimage": "...", //string, not mandatory parameter
|
||||
"fallbackAddr": "...", //string, not mandatory parameter
|
||||
"expiry": "...", //string, not mandatory parameter
|
||||
"private": "..." //string, not mandatory parameter
|
||||
"amt": "...", // string
|
||||
"memo":"...", // string
|
||||
"receipt":"...", // string, not mandatory parameter
|
||||
"preimage": "...", // string, not mandatory parameter
|
||||
"fallbackAddr": "...", // string, not mandatory parameter
|
||||
"expiry": "...", // string, not mandatory parameter
|
||||
"private": "..." // string, not mandatory parameter
|
||||
}
|
||||
|
||||
Response:
|
||||
|
||||
{
|
||||
"r_hash": "...", //string,
|
||||
"pay_req": "...", //string, a bare-bones invoice for a payment within the Lightning Network
|
||||
"add_index": ... //int, The “add” index of this invoice. Each newly created invoice will
|
||||
"r_hash": "...", // string,
|
||||
"pay_req": "...", // string, a bare-bones invoice for a payment within the Lightning Network
|
||||
"add_index": ... // int, The “add” index of this invoice. Each newly created invoice will
|
||||
// increment this index making it monotonically increasing.
|
||||
// Callers to the SubscribeInvoices call can use this to instantly
|
||||
// get notified of all added invoices with an add_index greater than this one.
|
||||
}
|
||||
|
||||
## GET /getuserinvoices
|
||||
|
||||
Returns fees user pays for payments, status of the system, etc.
|
||||
@ -475,62 +477,42 @@ Request:
|
||||
none
|
||||
|
||||
Response:
|
||||
{
|
||||
"r_hash": "...", //string
|
||||
"payment_request": "...", //string
|
||||
"add_index": "...", //string
|
||||
"description": "...", //string
|
||||
"amt": ... , //int
|
||||
"ispaid": ... //bool
|
||||
}
|
||||
|
||||
{
|
||||
"r_hash": "...", // string
|
||||
"payment_request": "...", // string
|
||||
"add_index": "...", // string
|
||||
"description": "...", // string
|
||||
"amt": ... , // int
|
||||
"ispaid": ... // bool
|
||||
}
|
||||
|
||||
# Data structures
|
||||
|
||||
## Transaction object
|
||||
|
||||
{
|
||||
"type": "...", // string, type of txs. Types:
|
||||
// bitcoind_internal_tx - moves to user btc address or account
|
||||
// bitcoind_tx - received by address or account
|
||||
// paid_invoice - user paid someone's invoice
|
||||
// sent_coins - user sent coins by lnd to someone's btc account
|
||||
// received_invoice_payments - user received payments by invoice
|
||||
"txid": "...", // string, internal tx id. not related to onchain transaction id
|
||||
"amt": 666, // satoshi, int
|
||||
"fee": 11, // satoshi, int
|
||||
"timestamp": 1234567, // int, unixtime
|
||||
"from": "...", // string
|
||||
"to": "...", // string
|
||||
"description": "...", // string, user-defined text
|
||||
"invoice": "...", // string, original bolt11-format invoice
|
||||
// bitcoind_internal_tx - moves to user btc address or account
|
||||
// bitcoind_tx - received by address or account
|
||||
// paid_invoice - user paid someone's invoice
|
||||
// sent_coins - user sent coins by lnd to someone's btc account
|
||||
// received_invoice_payments - user received payments by invoice
|
||||
"txid": "...", // string, internal tx id. not related to onchain transaction id
|
||||
"amt": 666, // satoshi, int
|
||||
"fee": 11, // satoshi, int
|
||||
"timestamp": 1234567, // int, unixtime
|
||||
"from": "...", // string
|
||||
"to": "...", // string
|
||||
"description": "...", // string, user-defined text
|
||||
"invoice": "...", // string, original bolt11-format invoice
|
||||
}
|
||||
|
||||
# Explaining oauth2 mechanism
|
||||
## Oauth2 processes
|
||||
Oauth2 process consists of such stages as:
|
||||
- Client (someone, who use api), make request to Authorization service with credentials (POST /auth?type=auth)
|
||||
- Authorization service checks credentials and searchs for appropriate user id and secret (stored on Authoriztion service and Token service) and sends user id and secret to Token service (for example POST /getinfo/oauth2/token)
|
||||
- Token service checks user id and secret and sends token data with refresh token to Authorization sevice which sends it to Client
|
||||
- Authorization service checks credentials and searches for appropriate user id and secret (stored on Authorization service and Token service) and sends user id and secret to Token service (for example POST /getinfo/oauth2/token)
|
||||
- Token service checks user id and secret and sends token data with refresh token to Authorization service which sends it to Client
|
||||
- Client uses token to access protected resources (GET ?access_token=XXXXXXXXXXXXXX)
|
||||
- When token expires or needs to refresh token for security issues Client sends refresh_token to Token service (POST /auth?type=refresh_token), which sends new token data with refresh_token and disables to access old
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```
|
||||
Добавил POST метод /addinvoice для того, чтобы пользователь мог создать свой инвойс
|
||||
в боди передаются параметы:
|
||||
{
|
||||
"amt": "string" обязательный
|
||||
"memo":"string" не обязательный
|
||||
"receipt":"string" не обязательный
|
||||
"preimage": "string" не обязательный
|
||||
"fallbackAddr": "string" не обязательны
|
||||
"expiry": "string" не обязательны
|
||||
"private": "string" не обязательны
|
||||
}
|
||||
|
||||
информация по инвойсам, которые оплатили пользователям досутпна через метод GET /getuserinvoices , полученные коины учитываются в балансе (settled в unconfirmed balance, остальной в confirmed balance)
|
||||
|
||||
```
|
||||
|
||||
18
doc/recover.md
Normal file
@ -0,0 +1,18 @@
|
||||
|
||||
recover user's wallet
|
||||
=====================
|
||||
|
||||
* find user's id
|
||||
f0db84e6fd5dee530314fbb90cec24839f4620914e7cd0c7
|
||||
* issue new credentials via tests/integration/LightningCustodianWallet.test.js
|
||||
lndhub://3d7c028419356d017199:66666666666666666666
|
||||
(this is user:password)
|
||||
* lookup redis record `user_{login}_{password_hash} = {userid}` :
|
||||
```
|
||||
> keys user_3d7c028419356d017199*
|
||||
1) "user_3d7c028419356d017199_505018e35414147406fcacdae63babbfca9b1abfcb6d091a4cca9a7611183284"
|
||||
```
|
||||
|
||||
* save to this record old user's id:
|
||||
`> set user_3d7c028419356d017199_505018e35414147406fcacdae63babbfca9b1abfcb6d091a4cca9a7611183284 f0db84e6fd5dee530314fbb90cec24839f4620914e7cd0c7`
|
||||
done! issued credentials should point to old user
|
||||
@ -9,6 +9,10 @@ User storage schema
|
||||
* access_token_for_{userid} = {access_token}
|
||||
* userid_for_{refresh_token} = {userid}
|
||||
* refresh_token_for_{userid} = {access_token}
|
||||
* importing_{txid} = 1 `atomic lock when processing topup tx`
|
||||
* invoice_paying_for_{userid} = 1 `lock for when payinvoice is in progress`
|
||||
* generating_address_{userid} = 1 `lock for address generation`
|
||||
* preimage_for_{payment_hash_hex} = {preimage_hex} `ttl 1 month`
|
||||
|
||||
|
||||
|
||||
@ -18,10 +22,19 @@ User storage schema
|
||||
* bitcoin_address_for_{userid} = {address}
|
||||
* balance_for_{userid} = {int}
|
||||
* txs_for_{userid} = [] `serialized paid lnd invoices in a list`
|
||||
* locked_payments_for_{userid} = [] `serialized attempts to pay invoice. used in calculating user's balance`
|
||||
: {pay_req:..., amount:666, timestamp:666}
|
||||
* imported_txids_for_{userid} = [] `list of txids processed for this user`
|
||||
* metadata_for_{userid}= {serialized json}
|
||||
* userinvoices_for_{userid} = []
|
||||
* payment_hash_{payment_hash} = {userid}
|
||||
* ispaid_{payment_hash} = 1
|
||||
* ispaid_{payment_hash} = {settleAmountSat}
|
||||
|
||||
|
||||
####cleanup test user
|
||||
|
||||
* del locked_payments_for_666
|
||||
* del txs_for_666
|
||||
* del invoice_paying_for_666
|
||||
* del userinvoices_for_666
|
||||
* del balance_for_666
|
||||
33
index.js
@ -1,17 +1,34 @@
|
||||
process.on('uncaughtException', function (err) {
|
||||
console.error(err);
|
||||
console.log('Node NOT Exiting...');
|
||||
});
|
||||
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||
let express = require('express');
|
||||
const helmet = require('helmet');
|
||||
let morgan = require('morgan');
|
||||
let uuid = require('node-uuid');
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
let logger = require('./utils/logger');
|
||||
const config = require('./config');
|
||||
|
||||
morgan.token('id', function getId(req) {
|
||||
return req.id;
|
||||
});
|
||||
|
||||
let app = express();
|
||||
app.enable('trust proxy');
|
||||
app.use(helmet.hsts());
|
||||
app.use(helmet.hidePoweredBy());
|
||||
|
||||
app.use(function(req, res, next) {
|
||||
req.id = uuid.v4();
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: config.rateLimit || 200,
|
||||
});
|
||||
app.use(limiter);
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
req.id = uuidv4();
|
||||
next();
|
||||
});
|
||||
|
||||
@ -21,10 +38,7 @@ app.use(
|
||||
),
|
||||
);
|
||||
|
||||
app.set('trust proxy', 'loopback');
|
||||
|
||||
let bodyParser = require('body-parser');
|
||||
let config = require('./config');
|
||||
|
||||
app.use(bodyParser.urlencoded({ extended: false })); // parse application/x-www-form-urlencoded
|
||||
app.use(bodyParser.json(null)); // parse application/json
|
||||
@ -33,7 +47,10 @@ app.use('/static', express.static('static'));
|
||||
app.use(require('./controllers/api'));
|
||||
app.use(require('./controllers/website'));
|
||||
|
||||
let server = app.listen(process.env.PORT || 3000, function() {
|
||||
logger.log('BOOTING UP', 'Listening on port ' + (process.env.PORT || 3000));
|
||||
const bindHost = process.env.HOST || '0.0.0.0';
|
||||
const bindPort = process.env.PORT || 3000;
|
||||
|
||||
let server = app.listen(bindPort, bindHost, function () {
|
||||
logger.log('BOOTING UP', 'Listening on ' + bindHost + ':' + bindPort);
|
||||
});
|
||||
module.exports = server;
|
||||
|
||||
41
lightning.js
@ -1,8 +1,18 @@
|
||||
// setup lnd rpc
|
||||
const config = require('./config');
|
||||
var fs = require('fs');
|
||||
var grpc = require('grpc');
|
||||
var lnrpc = grpc.load('rpc.proto').lnrpc;
|
||||
var grpc = require('@grpc/grpc-js');
|
||||
const protoLoader = require('@grpc/proto-loader');
|
||||
const loaderOptions = {
|
||||
keepCase: true,
|
||||
longs: String,
|
||||
enums: String,
|
||||
defaults: true,
|
||||
oneofs: true,
|
||||
};
|
||||
const packageDefinition = protoLoader.loadSync('rpc.proto', loaderOptions);
|
||||
var lnrpc = grpc.loadPackageDefinition(packageDefinition).lnrpc;
|
||||
|
||||
process.env.GRPC_SSL_CIPHER_SUITES = 'HIGH+ECDSA';
|
||||
var lndCert;
|
||||
if (process.env.TLSCERT) {
|
||||
@ -10,7 +20,7 @@ if (process.env.TLSCERT) {
|
||||
} else {
|
||||
lndCert = fs.readFileSync('tls.cert');
|
||||
}
|
||||
console.log('using tls.cert', lndCert.toString('hex'));
|
||||
process.env.VERBOSE && console.log('using tls.cert', lndCert.toString('hex'));
|
||||
let sslCreds = grpc.credentials.createSsl(lndCert);
|
||||
let macaroon;
|
||||
if (process.env.MACAROON) {
|
||||
@ -18,11 +28,30 @@ if (process.env.MACAROON) {
|
||||
} else {
|
||||
macaroon = fs.readFileSync('admin.macaroon').toString('hex');
|
||||
}
|
||||
console.log('using macaroon', macaroon);
|
||||
let macaroonCreds = grpc.credentials.createFromMetadataGenerator(function(args, callback) {
|
||||
process.env.VERBOSE && console.log('using macaroon', macaroon);
|
||||
let macaroonCreds = grpc.credentials.createFromMetadataGenerator(function (args, callback) {
|
||||
let metadata = new grpc.Metadata();
|
||||
metadata.add('macaroon', macaroon);
|
||||
callback(null, metadata);
|
||||
});
|
||||
let creds = grpc.credentials.combineChannelCredentials(sslCreds, macaroonCreds);
|
||||
module.exports = new lnrpc.Lightning(config.lnd.url, creds);
|
||||
|
||||
// trying to unlock the wallet:
|
||||
if (config.lnd.password) {
|
||||
process.env.VERBOSE && console.log('trying to unlock the wallet');
|
||||
var walletUnlocker = new lnrpc.WalletUnlocker(config.lnd.url, creds);
|
||||
walletUnlocker.unlockWallet(
|
||||
{
|
||||
wallet_password: Buffer.from(config.lnd.password).toString('base64'),
|
||||
},
|
||||
function (err, response) {
|
||||
if (err) {
|
||||
process.env.VERBOSE && console.log('unlockWallet failed, probably because its been aleady unlocked');
|
||||
} else {
|
||||
console.log('unlockWallet:', response);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = new lnrpc.Lightning(config.lnd.url, creds, { 'grpc.max_receive_message_length': 1024 * 1024 * 1024 });
|
||||
|
||||
8578
package-lock.json
generated
56
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "LndHub",
|
||||
"version": "1.0.0",
|
||||
"name": "lndhub",
|
||||
"version": "1.4.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@ -12,29 +12,33 @@
|
||||
"author": "Igor Korsakov <overtorment@gmail.com>",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"babel": "^6.23.0",
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-register": "^6.26.0",
|
||||
"bignumber.js": "^8.0.1",
|
||||
"bitcoinjs-lib": "^4.0.2",
|
||||
"bolt11": "https://github.com/bitcoinjs/bolt11",
|
||||
"eslint": "^5.9.0",
|
||||
"eslint-config-prettier": "^3.3.0",
|
||||
"eslint-plugin-prettier": "^3.0.0",
|
||||
"express": "^4.16.4",
|
||||
"grpc": "^1.17.0-pre1",
|
||||
"ioredis": "^4.2.0",
|
||||
"jayson": "^2.1.0",
|
||||
"morgan": "^1.9.1",
|
||||
"mustache": "^3.0.1",
|
||||
"node-uuid": "^1.4.8",
|
||||
"prettier": "^1.15.3",
|
||||
"request": "^2.88.0",
|
||||
"request-promise": "^4.2.2",
|
||||
"winston": "^3.1.0"
|
||||
"@babel/cli": "^7.14.8",
|
||||
"@babel/core": "^7.15.0",
|
||||
"@babel/eslint-parser": "^7.14.2",
|
||||
"@babel/node": "^7.14.9",
|
||||
"@babel/preset-env": "^7.15.6",
|
||||
"@babel/register": "^7.14.5",
|
||||
"@grpc/grpc-js": "^1.3.7",
|
||||
"@grpc/proto-loader": "^0.6.5",
|
||||
"bignumber.js": "^9.0.1",
|
||||
"bitcoinjs-lib": "^5.2.0",
|
||||
"bolt11": "^1.3.2",
|
||||
"eslint": "^7.24.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"express": "^4.17.1",
|
||||
"express-rate-limit": "^5.4.1",
|
||||
"frisbee": "^3.1.4",
|
||||
"helmet": "^4.6.0",
|
||||
"ioredis": "^4.27.10",
|
||||
"jayson": "^3.6.4",
|
||||
"morgan": "^1.10.0",
|
||||
"mustache": "^4.1.0",
|
||||
"prettier": "^2.3.0",
|
||||
"qr-image": "3.2.0",
|
||||
"request": "^2.88.2",
|
||||
"request-promise": "^4.2.6",
|
||||
"uuid": "^8.3.2",
|
||||
"winston": "^3.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
7
run-process-locked.sh
Executable file
@ -0,0 +1,7 @@
|
||||
while [ 1 ] ;
|
||||
do
|
||||
date
|
||||
./node_modules/.bin/babel-node scripts/process-locked-payments.js 2>/dev/null
|
||||
sleep 3600
|
||||
done
|
||||
|
||||
172
scripts/important-channels.js
Normal file
@ -0,0 +1,172 @@
|
||||
const important_channels = {
|
||||
'03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f': {
|
||||
name: 'ACINQ',
|
||||
uri: '03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f@34.239.230.56:9735',
|
||||
wumbo: 1,
|
||||
},
|
||||
'03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e': {
|
||||
name: 'OpenNode',
|
||||
uri: '03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e@18.221.23.28:9735',
|
||||
wumbo: 1,
|
||||
},
|
||||
'028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4': {
|
||||
name: 'OpenNode 2',
|
||||
uri: '028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4@18.222.70.85:9735',
|
||||
wumbo: 1,
|
||||
},
|
||||
// '0242a4ae0c5bef18048fbecf995094b74bfb0f7391418d71ed394784373f41e4f3': {
|
||||
// name: 'coingate.com',
|
||||
// uri: '0242a4ae0c5bef18048fbecf995094b74bfb0f7391418d71ed394784373f41e4f3@3.124.63.44:9735',
|
||||
// },
|
||||
// '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,
|
||||
// },
|
||||
'03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac': {
|
||||
name: 'bitrefill 3',
|
||||
uri: '03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac@3.237.23.179: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',
|
||||
// },
|
||||
'02816caed43171d3c9854e3b0ab2cf0c42be086ff1bd4005acc2a5f7db70d83774': {
|
||||
name: 'ln.pizza aka fold',
|
||||
uri: '02816caed43171d3c9854e3b0ab2cf0c42be086ff1bd4005acc2a5f7db70d83774@35.238.153.25:9735',
|
||||
wumbo: 1,
|
||||
},
|
||||
'036b53093df5a932deac828cca6d663472dbc88322b05eec1d42b26ab9b16caa1c': {
|
||||
name: 'okcoin',
|
||||
uri: '036b53093df5a932deac828cca6d663472dbc88322b05eec1d42b26ab9b16caa1c@47.243.25.4:26658',
|
||||
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',
|
||||
// },
|
||||
// '03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda': {
|
||||
// name: 'tippin.me',
|
||||
// uri: '03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda@157.245.68.47:9735',
|
||||
// },
|
||||
};
|
||||
|
||||
let lightning = require('../lightning');
|
||||
|
||||
lightning.listChannels({}, function (err, response) {
|
||||
console.log();
|
||||
if (err) {
|
||||
console.error('lnd failure:', err);
|
||||
return;
|
||||
}
|
||||
let lightningListChannels = response;
|
||||
for (let channel of lightningListChannels.channels) {
|
||||
if (channel.capacity <= 1000000) {
|
||||
console.log(
|
||||
'lncli closechannel',
|
||||
channel.channel_point.replace(':', ' '),
|
||||
(!channel.active && '--force') || '',
|
||||
'; sleep 10 #',
|
||||
'low capacity channel',
|
||||
channel.capacity / 100000000,
|
||||
'btc',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv.includes('--reconnect')) {
|
||||
let doneReconnect = {}; // so theres no duplicates
|
||||
console.log('# reconnect important channels that are inactive:\n');
|
||||
for (const important of Object.keys(important_channels)) {
|
||||
for (let channel of lightningListChannels.channels) {
|
||||
if (channel.remote_pubkey === important && !channel.active && !doneReconnect[channel.remote_pubkey]) {
|
||||
doneReconnect[channel.remote_pubkey] = true;
|
||||
console.log(
|
||||
'lncli disconnect',
|
||||
channel.remote_pubkey,
|
||||
'; sleep 5;',
|
||||
'lncli connect',
|
||||
important_channels[channel.remote_pubkey].uri,
|
||||
'#',
|
||||
important_channels[channel.remote_pubkey].name,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv.includes('--reconnect-all')) {
|
||||
let doneReconnect = {}; // so theres no duplicates
|
||||
console.log('# reconnect important channels that are inactive:\n');
|
||||
for (const important of Object.keys(important_channels)) {
|
||||
for (let channel of lightningListChannels.channels) {
|
||||
if (channel.remote_pubkey === important && !doneReconnect[channel.remote_pubkey]) {
|
||||
doneReconnect[channel.remote_pubkey] = true;
|
||||
console.log(
|
||||
'lncli disconnect',
|
||||
channel.remote_pubkey,
|
||||
'; sleep 5;',
|
||||
'lncli connect',
|
||||
important_channels[channel.remote_pubkey].uri,
|
||||
'#',
|
||||
important_channels[channel.remote_pubkey].name,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv.includes('--open')) {
|
||||
console.log('\n# open important channels:\n');
|
||||
for (const important of Object.keys(important_channels)) {
|
||||
let atLeastOneChannelIsSufficientCapacity = false;
|
||||
for (let channel of lightningListChannels.channels) {
|
||||
if (channel.remote_pubkey === important && channel.local_balance >= 4000000 && channel.active) {
|
||||
atLeastOneChannelIsSufficientCapacity = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!atLeastOneChannelIsSufficientCapacity) {
|
||||
console.log(
|
||||
'lncli disconnect',
|
||||
important,
|
||||
'; sleep 3;',
|
||||
'lncli openchannel --node_key',
|
||||
important,
|
||||
'--connect',
|
||||
important_channels[important].uri.split('@')[1],
|
||||
'--local_amt',
|
||||
important_channels[important].wumbo ? '100000000' : '16777215',
|
||||
'--remote_csv_delay 144',
|
||||
'--sat_per_byte 10',
|
||||
'#',
|
||||
important_channels[important].name,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
process.exit();
|
||||
});
|
||||
25
scripts/migrate_addresses_to_other_bitcoind.sh
Normal file
@ -0,0 +1,25 @@
|
||||
# this script should be used if youre retiring one bitcoind in favor of new one
|
||||
# it exports all addresses from the old one and prepares script to import them on a new node
|
||||
#
|
||||
echo export 1...
|
||||
./bitcoin-0.21.0/bin/bitcoin-cli -rpcwallet="" -rpcconnect=1.1.1.1 -rpcuser=user -rpcpassword=oldPassword listreceivedbyaddress 0 true true > addresses.txt
|
||||
echo export 2...
|
||||
./bitcoin-0.21.0/bin/bitcoin-cli -rpcwallet="wallet.dat" -rpcconnect=1.1.1.1 -rpcuser=user -rpcpassword=oldPassword listreceivedbyaddress 0 true true >> addresses.txt
|
||||
|
||||
echo clean...
|
||||
cat addresses.txt | grep address | sort -u | awk '{print $2}' | sed 's/"//g' | sed 's/,//g' > addresses_clean.txt
|
||||
|
||||
echo "got addresses:"
|
||||
wc -l < addresses_clean.txt
|
||||
|
||||
|
||||
echo writing import_on_other_node.sh ...
|
||||
>import_on_other_node.sh
|
||||
chmod +x import_on_other_node.sh
|
||||
|
||||
while read in; do
|
||||
echo "./bitcoin-0.21.0/bin/bitcoin-cli -rpcconnect=2.2.2.2 -rpcuser=user -rpcpassword=newPassword importaddress $in $in false" >> import_on_other_node.sh
|
||||
done < addresses_clean.txt
|
||||
|
||||
echo 'done. dont forget to run ./import_on_other_node.sh and then ./bitcoin-0.21.0/bin/bitcoin-cli -rpcconnect=2.2.2.2 -rpcwallet="wallet.dat" -rpcuser=user -rpcpassword=newPassword rescanblockchain 459491'
|
||||
|
||||
103
scripts/process-locked-payments.js
Normal file
@ -0,0 +1,103 @@
|
||||
/**
|
||||
* This script gets all locked payments from our database and cross-checks them with actual
|
||||
* sentout payments from LND. If locked payment is in there we moe locked payment to array of real payments for the user
|
||||
* (it is effectively spent coins by user), if not - we attempt to pay it again (if it is not too old).
|
||||
*/
|
||||
import { User, Paym } from '../class/';
|
||||
const config = require('../config');
|
||||
|
||||
/****** START SET FEES FROM CONFIG AT STARTUP ******/
|
||||
/** GLOBALS */
|
||||
global.forwardFee = config.forwardReserveFee || 0.01;
|
||||
global.internalFee = config.intraHubFee || 0.003;
|
||||
/****** END SET FEES FROM CONFIG AT STARTUP ******/
|
||||
|
||||
var Redis = require('ioredis');
|
||||
var redis = new Redis(config.redis);
|
||||
|
||||
let bitcoinclient = require('../bitcoin');
|
||||
let lightning = require('../lightning');
|
||||
|
||||
(async () => {
|
||||
let keys = await redis.keys('locked_payments_for_*');
|
||||
keys = User._shuffle(keys);
|
||||
|
||||
console.log('fetching listPayments...');
|
||||
let tempPaym = new Paym(redis, bitcoinclient, lightning);
|
||||
let listPayments = await tempPaym.listPayments();
|
||||
// DEBUG let listPayments = JSON.parse(fs.readFileSync('listpayments.txt').toString('ascii'));
|
||||
console.log('done', 'got', listPayments['payments'].length, 'payments');
|
||||
|
||||
for (let key of keys) {
|
||||
const userid = key.replace('locked_payments_for_', '');
|
||||
console.log('===================================================================================');
|
||||
console.log('userid=', userid);
|
||||
let user = new User(redis, bitcoinclient, lightning);
|
||||
user._userid = userid;
|
||||
let lockedPayments = await user.getLockedPayments();
|
||||
// DEBUG let lockedPayments = [{ pay_req : 'lnbc108130n1pshdaeupp58kw9djt9vcdx26wkdxl07tgncdmxz2w7s9hzul45tf8gfplme94sdqqcqzzgxqrrssrzjqw8c7yfutqqy3kz8662fxutjvef7q2ujsxtt45csu0k688lkzu3ld93gutl3k6wauyqqqqryqqqqthqqpysp5jcmk82hypuud0lhpf66dg3w5ta6aumc4w9g9sxljazglq9wkwstq9qypqsqnw8hwwauvzrala3g4yrkgazk2l2fh582j9ytz7le46gmsgglvmrknx842ej9z4c63en5866l8tpevm8cwul8g94kf2nepppn256unucp43jnsw', amount: 10813, timestamp: 1635186606 }];
|
||||
|
||||
for (let lockedPayment of lockedPayments) {
|
||||
let daysPassed = (+new Date() / 1000 - lockedPayment.timestamp) / 3600 / 24;
|
||||
console.log('processing lockedPayment=', lockedPayment, daysPassed, 'days passed');
|
||||
|
||||
let payment = new Paym(redis, bitcoinclient, lightning);
|
||||
payment.setInvoice(lockedPayment.pay_req);
|
||||
|
||||
// first things first:
|
||||
// trying to lookup this stuck payment in an array of delivered payments
|
||||
let isPaid = false;
|
||||
for (let sentPayment of listPayments['payments']) {
|
||||
if ((await payment.getPaymentHash()) == sentPayment.payment_hash) {
|
||||
console.log('found this payment in listPayments array, so it is paid successfully');
|
||||
let sendResult = payment.processSendPaymentResponse({ payment_error: 'already paid' } /* hacky */); // adds fees
|
||||
console.log('saving paid invoice:', sendResult);
|
||||
await user.savePaidLndInvoice(sendResult);
|
||||
await user.unlockFunds(lockedPayment.pay_req);
|
||||
isPaid = true;
|
||||
console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!', await payment.getPaymentHash(), sentPayment.payment_hash);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// could not find...
|
||||
|
||||
if (daysPassed > 1 / 24 && daysPassed <= 1) {
|
||||
let sendResult;
|
||||
console.log('attempting to pay to route');
|
||||
try {
|
||||
sendResult = await payment.attemptPayToRoute();
|
||||
} catch (_) {
|
||||
console.log(_);
|
||||
console.log('evict lock');
|
||||
await user.unlockFunds(lockedPayment.pay_req);
|
||||
continue;
|
||||
}
|
||||
console.log('sendResult=', sendResult);
|
||||
console.log('payment.getIsPaid() = ', payment.getIsPaid());
|
||||
if (payment.getIsPaid() === true) {
|
||||
console.log('paid successfully');
|
||||
sendResult = payment.processSendPaymentResponse(sendResult); // adds fees
|
||||
console.log('saving paid invoice:', sendResult);
|
||||
await user.savePaidLndInvoice(sendResult);
|
||||
await user.unlockFunds(lockedPayment.pay_req);
|
||||
} else if (payment.getIsPaid() === false) {
|
||||
console.log('not paid, just evict the lock');
|
||||
await user.unlockFunds(lockedPayment.pay_req);
|
||||
} else {
|
||||
console.log('payment is in unknown state');
|
||||
}
|
||||
console.log('sleeping 5 sec...');
|
||||
console.log('-----------------------------------------------------------------------------------');
|
||||
await User._sleep(0);
|
||||
} else if (daysPassed > 1) {
|
||||
// could not find in listpayments array; too late to retry
|
||||
if (!isPaid) {
|
||||
console.log('very old payment, evict the lock');
|
||||
await user.unlockFunds(lockedPayment.pay_req);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('done');
|
||||
process.exit();
|
||||
})();
|
||||
38
scripts/process-unpaid-invoices.js
Normal file
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* This script goes through all user invoices in LND and if it is settled - marks it
|
||||
* so in our database. Does this only for invoices younger than week. *
|
||||
*/
|
||||
import { Invo } from '../class/';
|
||||
const config = require('../config');
|
||||
|
||||
const fs = require('fs');
|
||||
const Redis = require('ioredis');
|
||||
const redis = new Redis(config.redis);
|
||||
|
||||
let bitcoinclient = require('../bitcoin');
|
||||
let lightning = require('../lightning');
|
||||
|
||||
(async () => {
|
||||
console.log('fetching listinvoices...');
|
||||
let tempInv = new Invo(redis, bitcoinclient, lightning);
|
||||
|
||||
let listinvoices = await tempInv.listInvoices();
|
||||
console.log('done', 'got', listinvoices['invoices'].length, 'invoices');
|
||||
fs.writeFileSync('listInvoices.json', '[\n');
|
||||
|
||||
let markedInvoices = 0;
|
||||
for (const invoice of listinvoices['invoices']) {
|
||||
fs.appendFileSync('listInvoices.json', JSON.stringify(invoice, null, 2) + ',\n');
|
||||
if (invoice.state === 'SETTLED' && +invoice.creation_date >= +new Date() / 1000 - 3600 * 24 * 7 * 2) {
|
||||
tempInv.setInvoice(invoice.payment_request);
|
||||
await tempInv.markAsPaidInDatabase();
|
||||
markedInvoices++;
|
||||
process.stdout.write(markedInvoices + '\r');
|
||||
}
|
||||
}
|
||||
|
||||
fs.appendFileSync('listInvoices.json', ']');
|
||||
|
||||
console.log('done, marked', markedInvoices, 'invoices');
|
||||
process.exit();
|
||||
})();
|
||||
59
scripts/show_user.js
Normal file
@ -0,0 +1,59 @@
|
||||
import { User } from '../class/';
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
const config = require('../config');
|
||||
|
||||
var Redis = require('ioredis');
|
||||
var redis = new Redis(config.redis);
|
||||
|
||||
redis.info(function (err, info) {
|
||||
if (err || !info) {
|
||||
console.error('redis failure');
|
||||
process.exit(5);
|
||||
}
|
||||
});
|
||||
|
||||
let bitcoinclient = require('../bitcoin');
|
||||
let lightning = require('../lightning');
|
||||
|
||||
(async () => {
|
||||
let userid = process.argv[2];
|
||||
let U = new User(redis, bitcoinclient, lightning);
|
||||
U._userid = userid;
|
||||
|
||||
let userinvoices = await U.getUserInvoices();
|
||||
let txs;
|
||||
|
||||
let calculatedBalance = 0;
|
||||
|
||||
console.log('\ndb balance\n==============\n', await U.getBalance());
|
||||
|
||||
console.log('\nuserinvoices\n================\n');
|
||||
for (let invo of userinvoices) {
|
||||
if (invo && invo.ispaid) {
|
||||
console.log('+', +invo.amt, new Date(invo.timestamp * 1000).toString());
|
||||
calculatedBalance += +invo.amt;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\ntxs\n===\n');
|
||||
|
||||
txs = await U.getTxs();
|
||||
for (let tx of txs) {
|
||||
if (tx.type === 'bitcoind_tx') {
|
||||
console.log('+', new BigNumber(tx.amount).multipliedBy(100000000).toNumber(), '[on-chain refill]');
|
||||
calculatedBalance += new BigNumber(tx.amount).multipliedBy(100000000).toNumber();
|
||||
} else {
|
||||
console.log('-', +tx.value, new Date(tx.timestamp * 1000).toString(), tx.memo, '; preimage:', tx.payment_preimage || '');
|
||||
calculatedBalance -= +tx.value;
|
||||
}
|
||||
}
|
||||
|
||||
let locked = await U.getLockedPayments();
|
||||
for (let loc of locked) {
|
||||
console.log('-', loc.amount + /* fee limit */ Math.floor(loc.amount * config.forwardReserveFee), new Date(loc.timestamp * 1000).toString(), '[locked]');
|
||||
}
|
||||
|
||||
console.log('\ncalculatedBalance\n================\n', calculatedBalance, await U.getCalculatedBalance());
|
||||
console.log('txs:', txs.length, 'userinvoices:', userinvoices.length);
|
||||
process.exit();
|
||||
})();
|
||||
@ -1,145 +0,0 @@
|
||||
/*
|
||||
* Globals
|
||||
*/
|
||||
|
||||
/* Links */
|
||||
a,
|
||||
a:focus,
|
||||
a:hover {
|
||||
color: #fff;
|
||||
}
|
||||
.lead {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.lead strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Custom default button */
|
||||
.btn-secondary,
|
||||
.btn-secondary:hover,
|
||||
.btn-secondary:focus {
|
||||
color: #333;
|
||||
text-shadow: none; /* Prevent inheritance from `body` */
|
||||
background-color: #fff;
|
||||
border: .05rem solid #fff;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Base structure
|
||||
*/
|
||||
|
||||
html,
|
||||
body {
|
||||
background-color: #1C2529;
|
||||
}
|
||||
.clear {
|
||||
clear: both;
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
color: #fff;
|
||||
}
|
||||
h1 {
|
||||
font-size: 70px;
|
||||
font-weight: bold;
|
||||
}
|
||||
h2 {
|
||||
font-size: 50px;
|
||||
font-weight: bold;
|
||||
padding-top: 80px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.cover-heading {
|
||||
padding-top: 190px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.cover-container {
|
||||
max-width: 960px;
|
||||
}
|
||||
@media (max-width: 991px) {
|
||||
.cover-container {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
.github {
|
||||
margin-top: 80px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Header
|
||||
*/
|
||||
.masthead {
|
||||
padding-top: 58px;
|
||||
}
|
||||
|
||||
.masthead-brand {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 48em) {
|
||||
.masthead-brand {
|
||||
float: left;
|
||||
}
|
||||
.nav-masthead {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Cover
|
||||
*/
|
||||
.cover {
|
||||
padding-bottom: 140px;
|
||||
}
|
||||
.cover .btn-lg {
|
||||
padding: .75rem 1.25rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.explain {
|
||||
min-height: 800px;
|
||||
background: #29353C;
|
||||
}
|
||||
.explain img {
|
||||
margin: 90px auto;
|
||||
}
|
||||
.lndhub-graph {
|
||||
max-width: 100%;
|
||||
}
|
||||
.promote {
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
.promote img {
|
||||
max-width: 232px;
|
||||
}
|
||||
.cta {
|
||||
margin-top: 40px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.cta li:first-child {
|
||||
float: left;
|
||||
}
|
||||
.cta li {
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@media (max-width: 991px) {
|
||||
.cta li {
|
||||
margin: 0 auto 32px 0;
|
||||
text-align: center;
|
||||
width: 50%;
|
||||
float: left;
|
||||
}
|
||||
.cta li img{
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Footer
|
||||
*/
|
||||
.mastfoot {
|
||||
color: rgba(255, 255, 255, .5);
|
||||
}
|
||||
253
static/css/style.css
Normal file
@ -0,0 +1,253 @@
|
||||
html, body {
|
||||
height: 100%;
|
||||
background: #FAFBFE;
|
||||
color: #000;
|
||||
}
|
||||
html {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box
|
||||
}
|
||||
*, :before, :after {
|
||||
box-sizing: inherit
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Helvetica Neue, Menlo, Consolas, "Courier New", monospace;
|
||||
word-wrap: break-word;
|
||||
font-weight: 400;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.sidebar {
|
||||
background: #fff;
|
||||
box-shadow: 0 -1px 4px 0 rgba(0,0,0,.20);
|
||||
}
|
||||
.container32 {
|
||||
padding: 32px;
|
||||
}
|
||||
.container24 {
|
||||
padding: 24px;
|
||||
}
|
||||
.container16 {
|
||||
padding: 16px;
|
||||
}
|
||||
.nosidepadding {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
.boxes {
|
||||
display: flex;
|
||||
margin: 40px 0 56px 0;
|
||||
}
|
||||
.box {
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px 0 rgba(0,0,0,.12);
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
}
|
||||
.boxes .box {
|
||||
width: 25%;
|
||||
margin-right: 32px;
|
||||
}
|
||||
|
||||
.box h3 {
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
.meta {
|
||||
font-size: 13px;
|
||||
color: #9AA0AA;
|
||||
margin: 0 0 4px 0;
|
||||
padding: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
.uri {
|
||||
font-size: 13px;
|
||||
color: #000;
|
||||
font-weight: 500;
|
||||
}
|
||||
.number1 {
|
||||
font-size: 30px;
|
||||
font-weight: 500;
|
||||
margin-top: 32px;
|
||||
display: inline-block;
|
||||
}
|
||||
.right {
|
||||
float: right;
|
||||
}
|
||||
.label {
|
||||
padding: 4px 8px;
|
||||
border-radius: 16px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
}
|
||||
[title~=true], [title~=active] {
|
||||
background: #50E3C2;
|
||||
}
|
||||
[title~=false], [title~=inactive] {
|
||||
background: #8E8E8E;
|
||||
}
|
||||
.label[title~=true]::after {
|
||||
content: "synced";
|
||||
}
|
||||
.label[title~=false]::after {
|
||||
content: "not synced";
|
||||
}
|
||||
.label[title~=active]::after {
|
||||
content: "active";
|
||||
}
|
||||
.label[title~=inactive]::after {
|
||||
content: "inactive";
|
||||
}
|
||||
.synced {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
|
||||
#progressbar {
|
||||
appearance: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-width: 100%;
|
||||
-webkit-appearance: none;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#progressbar[max]::-webkit-progress-value {
|
||||
border-radius: 8px 4px 4px 8px;
|
||||
-webkit-appearance: none;
|
||||
background: linear-gradient(0deg, rgba(47,95,179,1) 0%, rgba(63,120,220,1) 100%);
|
||||
}
|
||||
#progressbar[value]::-webkit-progress-bar {
|
||||
background: linear-gradient(0deg, rgba(104,187,225,1) 0%, rgba(139,215,249,1) 100%);
|
||||
height: 16px;
|
||||
border-radius: 8px;
|
||||
transition: 0.4s linear;
|
||||
transition-property: width, background-color;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
.row {
|
||||
padding: 8px 0;
|
||||
border-radius: 8px;
|
||||
transition: 0.2s ease-in-out;
|
||||
}
|
||||
.row:hover {
|
||||
background: #F4F8FB;
|
||||
}
|
||||
.row .name {
|
||||
padding: 2px 4px 2px 8px;
|
||||
}
|
||||
.row .name h2 {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.row .graph {
|
||||
flex-grow: 1;
|
||||
padding: 0 16px 0 0;
|
||||
align-self: center;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
.row .status {
|
||||
align-self: center;
|
||||
padding: 2px 8px 2px 4px;
|
||||
}
|
||||
.decor {
|
||||
text-decoration : none;
|
||||
}
|
||||
.name h2 {
|
||||
color: #000;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
font-size: 18px;
|
||||
}
|
||||
.amount {
|
||||
color: #9AA0AA;
|
||||
margin: 4px 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
.qr {
|
||||
margin: 0 -24px;
|
||||
width: 268px
|
||||
}
|
||||
footer {
|
||||
color: #9AA0AA;
|
||||
}
|
||||
footer a {
|
||||
font-size: 14px;
|
||||
color: #0070FF;
|
||||
text-decoration: none;
|
||||
}
|
||||
@media (min-width: 1200px){
|
||||
body {
|
||||
padding-right: 300px;
|
||||
}
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 300px;
|
||||
height: 100%;
|
||||
box-shadow: 0 0 4px 0 rgba(0,0,0,.20);
|
||||
}
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 32px;
|
||||
color: #9AA0AA;
|
||||
}
|
||||
.boxes .box:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
}
|
||||
.row .name {
|
||||
flex-grow: 0.2;
|
||||
max-width: 20%;
|
||||
}
|
||||
.row .status {
|
||||
flex-grow: 0.1;
|
||||
max-width: 10%;
|
||||
}
|
||||
}
|
||||
@media (max-width: 1199px){
|
||||
.logo {
|
||||
width: 200px;
|
||||
}
|
||||
.scroll {
|
||||
overflow: scroll;
|
||||
padding: 0px 16px 0px 1px;
|
||||
margin-right: -32px;
|
||||
}
|
||||
.boxes {
|
||||
width: 900px;
|
||||
}
|
||||
.boxes .box {
|
||||
width: 180px;
|
||||
|
||||
}
|
||||
.row {
|
||||
position: relative;
|
||||
}
|
||||
.row .name {
|
||||
flex-grow: 0.8;
|
||||
max-width: 80%;
|
||||
}
|
||||
.row .status {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
@media (max-width: 959px){
|
||||
.logo {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
@ -1,57 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="197px" height="69px" viewBox="0 0 197 69" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.5 (67469) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>app-store-badge</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<path d="M174.564444,53.2702703 L6.30148148,53.2702703 C2.81555556,53.2702703 0,50.4735811 0,47.0110135 L0,6.39243243 C0,2.92986486 2.81555556,0.133175676 6.30148148,0.133175676 L174.564444,0.133175676 C178.05037,0.133175676 181,2.92986486 181,6.39243243 L181,47.0110135 C181,50.4735811 178.05037,53.2702703 174.564444,53.2702703 Z" id="path-1"></path>
|
||||
<filter x="-7.2%" y="-20.7%" width="114.4%" height="148.9%" filterUnits="objectBoundingBox" id="filter-2">
|
||||
<feOffset dx="0" dy="2" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
||||
<feGaussianBlur stdDeviation="4" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.907382246 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
|
||||
</filter>
|
||||
</defs>
|
||||
<g id="Page-2" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Desktop-HD" transform="translate(-725.000000, -2060.000000)" fill-rule="nonzero">
|
||||
<g id="Group-5" transform="translate(253.000000, 1680.000000)">
|
||||
<g id="app-store-badge" transform="translate(480.000000, 386.000000)">
|
||||
<g id="Group">
|
||||
<g id="Shape">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-2)" xlink:href="#path-1"></use>
|
||||
<use fill="#FFFFFF" xlink:href="#path-1"></use>
|
||||
</g>
|
||||
<g transform="translate(16.818728, 7.990541)" fill="#222A2E" id="Shape">
|
||||
<path d="M23.5375687,18.3782432 C23.5375687,14.1166216 27.0234946,11.9858108 27.2916428,11.8526351 C25.2805317,8.92277027 22.0627539,8.52324324 20.9901613,8.52324324 C18.3086798,8.25689189 15.7612724,10.1213514 14.4205317,10.1213514 C13.0797909,10.1213514 10.9346057,8.52324324 8.78942055,8.65641892 C5.97386499,8.65641892 3.29238351,10.3877027 1.8175687,12.9180405 C-1.26613501,18.1118919 1.01312425,25.8360811 3.96275388,30.0977027 C5.4375687,32.2285135 7.18053166,34.4925 9.32571685,34.3593243 C11.470902,34.2261486 12.2753465,33.0275676 14.956828,33.0275676 C17.5042354,33.0275676 18.3086798,34.3593243 20.5879391,34.3593243 C22.8671983,34.3593243 24.3420131,32.2285135 25.816828,30.2308784 C27.5597909,27.8337162 28.2301613,25.5697297 28.2301613,25.4365541 C28.0960872,25.1702027 23.6716428,23.4389189 23.5375687,18.3782432 Z"></path>
|
||||
<path d="M19.2471983,5.72655405 C20.453865,4.26162162 21.2583094,2.26398649 20.9901613,0.266351351 C19.2471983,0.399527027 17.2360872,1.46493243 15.8953465,2.79668919 C14.8227539,3.99527027 13.8842354,6.12608108 14.1523835,7.99054054 C16.1634946,8.25689189 18.0405317,7.19148649 19.2471983,5.72655405 Z"></path>
|
||||
</g>
|
||||
<g transform="translate(56.311111, 23.971622)" fill="#222A2E" id="Shape">
|
||||
<path d="M15.5525926,17.9787162 L12.4688889,17.9787162 L10.86,12.7848649 L5.09481481,12.7848649 L3.48592593,17.9787162 L0.536296296,17.9787162 L6.30148148,0.266351351 L9.78740741,0.266351351 L15.5525926,17.9787162 Z M10.4577778,10.6540541 L8.98296296,5.99290541 C8.84888889,5.4602027 8.58074074,4.3947973 8.04444444,2.66351351 C7.91037037,3.46256757 7.64222222,4.52797297 7.24,5.99290541 L5.76518519,10.6540541 L10.4577778,10.6540541 Z"></path>
|
||||
<path d="M30.4348148,11.4531081 C30.4348148,13.5839189 29.8985185,15.3152027 28.6918519,16.6469595 C27.6192593,17.7123649 26.2785185,18.3782432 24.8037037,18.3782432 C23.0607407,18.3782432 21.8540741,17.7123649 21.1837037,16.5137838 L21.1837037,23.3057432 L18.3681481,23.3057432 L18.3681481,9.58864865 C18.3681481,8.25689189 18.3681481,6.79195946 18.2340741,5.32702703 L20.7814815,5.32702703 L20.9155556,7.32466216 C21.8540741,5.85972973 23.3288889,5.06067568 25.2059259,5.06067568 C26.6807407,5.06067568 28.0214815,5.59337838 28.96,6.79195946 C29.8985185,7.85736486 30.4348148,9.45547297 30.4348148,11.4531081 Z M27.4851852,11.5862838 C27.4851852,10.3877027 27.217037,9.3222973 26.6807407,8.52324324 C26.0103704,7.72418919 25.2059259,7.32466216 24.1333333,7.32466216 C23.462963,7.32466216 22.7925926,7.59101351 22.2562963,7.99054054 C21.72,8.52324324 21.3177778,9.05594595 21.1837037,9.855 C21.0496296,10.254527 21.0496296,10.5208784 21.0496296,10.6540541 L21.0496296,12.7848649 C21.0496296,13.7170946 21.3177778,14.5161486 21.8540741,15.182027 C22.3903704,15.8479054 23.1948148,16.1142568 24.1333333,16.1142568 C25.2059259,16.1142568 26.0103704,15.7147297 26.6807407,14.9156757 C27.217037,13.9834459 27.4851852,12.9180405 27.4851852,11.5862838 Z"></path>
|
||||
<path d="M45.182963,11.4531081 C45.182963,13.5839189 44.6466667,15.3152027 43.44,16.6469595 C42.3674074,17.7123649 41.0266667,18.3782432 39.5518519,18.3782432 C37.8088889,18.3782432 36.6022222,17.7123649 35.9318519,16.5137838 L35.9318519,23.3057432 L33.1162963,23.3057432 L33.1162963,9.58864865 C33.1162963,8.25689189 33.1162963,6.79195946 32.9822222,5.32702703 L35.5296296,5.32702703 L35.6637037,7.32466216 C36.6022222,5.85972973 38.077037,5.06067568 39.9540741,5.06067568 C41.4288889,5.06067568 42.7696296,5.59337838 43.7081481,6.79195946 C44.6466667,7.85736486 45.182963,9.45547297 45.182963,11.4531081 Z M42.2333333,11.5862838 C42.2333333,10.3877027 41.9651852,9.3222973 41.4288889,8.52324324 C40.7585185,7.72418919 39.9540741,7.32466216 38.8814815,7.32466216 C38.2111111,7.32466216 37.5407407,7.59101351 37.0044444,7.99054054 C36.4681481,8.52324324 36.0659259,9.05594595 35.9318519,9.855 C35.7977778,10.254527 35.7977778,10.5208784 35.7977778,10.6540541 L35.7977778,12.7848649 C35.7977778,13.7170946 36.0659259,14.5161486 36.6022222,15.182027 C37.1385185,15.8479054 37.942963,16.1142568 38.8814815,16.1142568 C39.9540741,16.1142568 40.7585185,15.7147297 41.4288889,14.9156757 C41.9651852,13.9834459 42.2333333,12.9180405 42.2333333,11.5862838 Z"></path>
|
||||
<path d="M61.6740741,13.0512162 C61.6740741,14.5161486 61.1377778,15.8479054 60.0651852,16.7801351 C58.8585185,17.8455405 57.2496296,18.3782432 55.2385185,18.3782432 C53.3614815,18.3782432 51.7525926,17.9787162 50.68,17.3128378 L51.3503704,14.9156757 C52.557037,15.7147297 54.0318519,16.1142568 55.5066667,16.1142568 C56.5792593,16.1142568 57.3837037,15.8479054 58.0540741,15.4483784 C58.5903704,14.9156757 58.9925926,14.382973 58.9925926,13.4507432 C58.9925926,12.7848649 58.7244444,12.1189865 58.1881481,11.5862838 C57.6518519,11.0535811 56.8474074,10.6540541 55.7748148,10.254527 C52.6911111,9.05594595 51.0822222,7.45783784 51.0822222,5.19385135 C51.0822222,3.72891892 51.6185185,2.53033784 52.6911111,1.59810811 C53.7637037,0.665878378 55.2385185,0.266351351 57.1155556,0.266351351 C58.7244444,0.266351351 60.0651852,0.532702703 61.1377778,1.06540541 L60.4674074,3.32939189 C59.3948148,2.79668919 58.3222222,2.53033784 57.1155556,2.53033784 C56.042963,2.53033784 55.3725926,2.79668919 54.7022222,3.32939189 C54.1659259,3.72891892 54.0318519,4.26162162 54.0318519,4.9275 C54.0318519,5.59337838 54.3,6.25925676 54.8362963,6.65878378 C55.3725926,7.05831081 56.177037,7.59101351 57.3837037,7.99054054 C58.8585185,8.65641892 60.0651852,9.3222973 60.7355556,10.1213514 C61.4059259,10.7872297 61.6740741,11.8526351 61.6740741,13.0512162 Z"></path>
|
||||
<path d="M71.1933333,7.32466216 L67.9755556,7.32466216 L67.9755556,13.5839189 C67.9755556,15.182027 68.5118519,15.9810811 69.5844444,15.9810811 C70.1207407,15.9810811 70.522963,15.9810811 70.7911111,15.8479054 L70.9251852,17.9787162 C70.3888889,18.2450676 69.5844444,18.2450676 68.6459259,18.2450676 C67.5733333,18.2450676 66.6348148,17.8455405 65.9644444,17.1796622 C65.2940741,16.5137838 65.0259259,15.3152027 65.0259259,13.7170946 L65.0259259,7.32466216 L63.1488889,7.32466216 L63.1488889,5.19385135 L65.0259259,5.19385135 L65.0259259,2.79668919 L67.8414815,1.99763514 L67.8414815,5.19385135 L70.9251852,5.19385135 C71.1933333,5.19385135 71.1933333,7.32466216 71.1933333,7.32466216 Z"></path>
|
||||
<path d="M85.4051852,11.4531081 C85.4051852,13.4507432 84.8688889,15.0488514 83.6622222,16.2474324 C82.4555556,17.5791892 80.8466667,18.2450676 78.9696296,18.2450676 C77.0925926,18.2450676 75.6177778,17.5791892 74.4111111,16.3806081 C73.3385185,15.182027 72.6681481,13.5839189 72.6681481,11.7194595 C72.6681481,9.72182432 73.2044444,8.12371622 74.4111111,6.79195946 C75.6177778,5.59337838 77.0925926,4.9275 79.1037037,4.9275 C80.9807407,4.9275 82.4555556,5.59337838 83.6622222,6.79195946 C84.8688889,7.99054054 85.4051852,9.58864865 85.4051852,11.4531081 Z M82.4555556,11.5862838 C82.4555556,10.3877027 82.1874074,9.45547297 81.6511111,8.52324324 C81.1148148,7.45783784 80.1762963,7.05831081 79.1037037,7.05831081 C77.897037,7.05831081 77.0925926,7.59101351 76.4222222,8.52324324 C75.8859259,9.3222973 75.6177778,10.3877027 75.6177778,11.5862838 C75.6177778,12.7848649 75.8859259,13.7170946 76.4222222,14.6493243 C77.0925926,15.7147297 77.897037,16.1142568 78.9696296,16.1142568 C80.0422222,16.1142568 80.9807407,15.5815541 81.517037,14.5161486 C82.1874074,13.7170946 82.4555556,12.7848649 82.4555556,11.5862838 Z"></path>
|
||||
<path d="M94.6562963,7.72418919 C94.3881481,7.72418919 94.12,7.59101351 93.7177778,7.59101351 C92.6451852,7.59101351 91.9748148,7.99054054 91.4385185,8.78959459 C90.9022222,9.45547297 90.7681481,10.254527 90.7681481,11.3199324 L90.7681481,17.9787162 L87.9525926,17.9787162 L87.9525926,9.18912162 C87.9525926,7.72418919 87.9525926,6.39243243 87.8185185,5.19385135 L90.3659259,5.19385135 L90.5,7.59101351 L90.6340741,7.59101351 C90.9022222,6.79195946 91.4385185,6.12608108 92.1088889,5.59337838 C92.7792593,5.19385135 93.4496296,4.9275 94.12,4.9275 C94.3881481,4.9275 94.6562963,4.9275 94.7903704,4.9275 C94.6562963,5.06067568 94.6562963,7.72418919 94.6562963,7.72418919 Z"></path>
|
||||
<path d="M107.527407,11.0535811 C107.527407,11.5862838 107.527407,11.9858108 107.393333,12.3853378 L98.8125926,12.3853378 C98.8125926,13.5839189 99.2148148,14.6493243 100.019259,15.3152027 C100.68963,15.8479054 101.628148,16.2474324 102.834815,16.2474324 C104.041481,16.2474324 105.248148,15.9810811 106.320741,15.5815541 L106.722963,17.5791892 C105.516296,18.1118919 104.041481,18.3782432 102.432593,18.3782432 C100.421481,18.3782432 98.8125926,17.8455405 97.74,16.6469595 C96.6674074,15.4483784 95.997037,13.8502703 95.997037,11.9858108 C95.997037,10.1213514 96.5333333,8.39006757 97.6059259,7.19148649 C98.6785185,5.85972973 100.153333,5.19385135 102.164444,5.19385135 C104.041481,5.19385135 105.382222,5.85972973 106.320741,7.19148649 C107.125185,7.99054054 107.527407,9.45547297 107.527407,11.0535811 Z M104.711852,10.254527 C104.711852,9.45547297 104.577778,8.65641892 104.175556,8.12371622 C103.639259,7.32466216 102.968889,6.92513514 101.896296,6.92513514 C100.957778,6.92513514 100.153333,7.32466216 99.617037,8.12371622 C99.0807407,8.78959459 98.8125926,9.45547297 98.8125926,10.3877027 L104.711852,10.254527 Z"></path>
|
||||
</g>
|
||||
<g transform="translate(58.381691, 7.990541)" fill="#222A2E" id="Shape">
|
||||
<path d="M7.31460573,5.32702703 C7.31460573,6.92513514 6.77830944,8.12371622 5.83979092,8.92277027 C4.9012724,9.58864865 3.69460573,9.98817568 2.08571685,9.98817568 C1.2812724,9.98817568 0.610902031,9.98817568 0.0746057348,9.855 L0.0746057348,1.33175676 C0.879050179,1.19858108 1.68349462,1.19858108 2.48793907,1.19858108 C3.96275388,1.19858108 5.16942055,1.46493243 5.97386499,2.13081081 C6.91238351,2.92986486 7.31460573,3.99527027 7.31460573,5.32702703 Z M5.83979092,5.32702703 C5.83979092,4.26162162 5.57164277,3.59574324 5.03534648,2.92986486 C4.49905018,2.39716216 3.69460573,2.13081081 2.62201314,2.13081081 C2.21979092,2.13081081 1.8175687,2.13081081 1.54942055,2.26398649 L1.54942055,8.78959459 C1.68349462,8.78959459 2.08571685,8.78959459 2.48793907,8.78959459 C3.56053166,8.78959459 4.36497611,8.52324324 5.03534648,7.85736486 C5.57164277,7.32466216 5.83979092,6.52560811 5.83979092,5.32702703 Z"></path>
|
||||
<path d="M15.2249761,6.65878378 C15.2249761,7.59101351 14.956828,8.39006757 14.4205317,9.05594595 C13.8842354,9.72182432 13.0797909,9.98817568 12.1412724,9.98817568 C11.2027539,9.98817568 10.5323835,9.72182432 9.86201314,9.05594595 C9.32571685,8.39006757 9.0575687,7.72418919 9.0575687,6.79195946 C9.0575687,5.85972973 9.32571685,5.06067568 9.86201314,4.3947973 C10.3983094,3.72891892 11.2027539,3.46256757 12.1412724,3.46256757 C13.0797909,3.46256757 13.7501613,3.72891892 14.4205317,4.3947973 C14.956828,5.06067568 15.2249761,5.72655405 15.2249761,6.65878378 Z M13.7501613,6.79195946 C13.7501613,6.25925676 13.6160872,5.72655405 13.3479391,5.32702703 C13.0797909,4.79432432 12.6775687,4.52797297 12.1412724,4.52797297 C11.6049761,4.52797297 11.2027539,4.79432432 10.8005317,5.32702703 C10.5323835,5.72655405 10.3983094,6.25925676 10.3983094,6.79195946 C10.3983094,7.32466216 10.5323835,7.85736486 10.8005317,8.25689189 C11.0686798,8.78959459 11.470902,9.05594595 12.1412724,9.05594595 C12.6775687,9.05594595 13.0797909,8.78959459 13.3479391,8.25689189 C13.6160872,7.85736486 13.7501613,7.32466216 13.7501613,6.79195946 Z"></path>
|
||||
<path d="M25.816828,3.59574324 L23.8057168,9.855 L22.4649761,9.855 L21.6605317,7.19148649 C21.3923835,6.52560811 21.2583094,5.85972973 21.1242354,5.19385135 C20.9901613,5.85972973 20.8560872,6.52560811 20.5879391,7.19148649 L19.7834946,9.855 L18.4427539,9.855 L16.5657168,3.59574324 L18.0405317,3.59574324 L18.710902,6.52560811 C18.8449761,7.19148649 18.9790502,7.85736486 19.1131243,8.52324324 C19.2471983,7.99054054 19.3812724,7.32466216 19.6494205,6.52560811 L20.5879391,3.46256757 L21.7946057,3.46256757 L22.5990502,6.39243243 C22.8671983,7.05831081 23.0012724,7.85736486 23.1353465,8.52324324 C23.2694205,7.85736486 23.4034946,7.19148649 23.5375687,6.39243243 L24.3420131,3.46256757 L25.816828,3.59574324 Z"></path>
|
||||
<path d="M33.056828,9.855 L31.7160872,9.855 L31.7160872,6.25925676 C31.7160872,5.19385135 31.313865,4.66114865 30.3753465,4.66114865 C29.9731243,4.66114865 29.570902,4.79432432 29.3027539,5.06067568 C29.0346057,5.32702703 28.9005317,5.72655405 28.9005317,6.12608108 L28.9005317,9.855 L27.5597909,9.855 L27.5597909,5.32702703 C27.5597909,4.79432432 27.5597909,4.12844595 27.5597909,3.59574324 L28.7664576,3.59574324 L28.7664576,4.52797297 C28.9005317,4.26162162 29.1686798,3.99527027 29.436828,3.72891892 C29.8390502,3.46256757 30.2412724,3.32939189 30.6434946,3.32939189 C31.1797909,3.32939189 31.7160872,3.46256757 32.1183094,3.86209459 C32.6546057,4.26162162 32.7886798,5.06067568 32.7886798,5.99290541 L32.7886798,9.855 L33.056828,9.855 Z"></path>
|
||||
<polygon points="36.9449761 9.855 35.6042354 9.855 35.6042354 0.665878378 36.9449761 0.665878378"></polygon>
|
||||
<path d="M45.2575687,6.65878378 C45.2575687,7.59101351 44.9894205,8.39006757 44.4531243,9.05594595 C43.916828,9.72182432 43.1123835,9.98817568 42.173865,9.98817568 C41.2353465,9.98817568 40.5649761,9.72182432 39.8946057,9.05594595 C39.3583094,8.39006757 39.0901613,7.72418919 39.0901613,6.79195946 C39.0901613,5.85972973 39.3583094,5.06067568 39.8946057,4.3947973 C40.430902,3.72891892 41.2353465,3.46256757 42.173865,3.46256757 C43.1123835,3.46256757 43.7827539,3.72891892 44.4531243,4.3947973 C44.9894205,5.06067568 45.2575687,5.72655405 45.2575687,6.65878378 Z M43.7827539,6.79195946 C43.7827539,6.25925676 43.6486798,5.72655405 43.3805317,5.32702703 C43.1123835,4.79432432 42.7101613,4.52797297 42.173865,4.52797297 C41.6375687,4.52797297 41.2353465,4.79432432 40.8331243,5.32702703 C40.5649761,5.72655405 40.430902,6.25925676 40.430902,6.79195946 C40.430902,7.32466216 40.5649761,7.85736486 40.8331243,8.25689189 C41.1012724,8.78959459 41.5034946,9.05594595 42.173865,9.05594595 C42.7101613,9.05594595 43.1123835,8.78959459 43.3805317,8.25689189 C43.6486798,7.85736486 43.7827539,7.32466216 43.7827539,6.79195946 Z"></path>
|
||||
<path d="M51.9612724,9.855 L50.7546057,9.855 L50.6205317,9.18912162 C50.2183094,9.72182432 49.5479391,9.98817568 48.7434946,9.98817568 C48.2071983,9.98817568 47.670902,9.855 47.2686798,9.45547297 C47.0005317,9.05594595 46.7323835,8.65641892 46.7323835,8.12371622 C46.7323835,7.32466216 47.0005317,6.79195946 47.670902,6.39243243 C48.3412724,5.99290541 49.2797909,5.72655405 50.3523835,5.85972973 L50.3523835,5.72655405 C50.3523835,4.9275 49.9501613,4.52797297 49.0116428,4.52797297 C48.3412724,4.52797297 47.8049761,4.66114865 47.4027539,4.9275 L47.1346057,3.99527027 C47.670902,3.59574324 48.4753465,3.46256757 49.2797909,3.46256757 C50.8886798,3.46256757 51.8271983,4.26162162 51.8271983,5.99290541 L51.8271983,8.25689189 C51.9612724,9.05594595 51.9612724,9.58864865 51.9612724,9.855 Z M50.4864576,7.72418919 L50.4864576,6.79195946 C48.8775687,6.79195946 48.2071983,7.19148649 48.2071983,7.99054054 C48.2071983,8.25689189 48.3412724,8.52324324 48.4753465,8.78959459 C48.6094205,9.05594595 48.8775687,9.05594595 49.1457168,9.05594595 C49.413865,9.05594595 49.6820131,8.92277027 49.9501613,8.78959459 C50.2183094,8.65641892 50.3523835,8.39006757 50.4864576,7.99054054 C50.4864576,7.85736486 50.4864576,7.85736486 50.4864576,7.72418919 Z"></path>
|
||||
<path d="M60.0057168,9.855 L58.7990502,9.855 L58.7990502,8.78959459 C58.396828,9.58864865 57.7264576,9.98817568 56.7879391,9.98817568 C55.9834946,9.98817568 55.4471983,9.72182432 54.910902,9.05594595 C54.3746057,8.39006757 54.1064576,7.72418919 54.1064576,6.79195946 C54.1064576,5.72655405 54.3746057,4.9275 54.910902,4.26162162 C55.4471983,3.72891892 56.1175687,3.32939189 56.9220131,3.32939189 C57.7264576,3.32939189 58.396828,3.59574324 58.6649761,4.12844595 L58.6649761,0.532702703 L60.0057168,0.532702703 L60.0057168,7.99054054 C59.8716428,8.78959459 60.0057168,9.3222973 60.0057168,9.855 Z M58.530902,7.19148649 L58.530902,6.12608108 C58.530902,5.99290541 58.530902,5.85972973 58.530902,5.72655405 C58.396828,5.32702703 58.2627539,5.06067568 57.9946057,4.9275 C57.7264576,4.66114865 57.4583094,4.52797297 57.0560872,4.52797297 C56.5197909,4.52797297 56.1175687,4.79432432 55.8494205,5.19385135 C55.5812724,5.59337838 55.4471983,6.12608108 55.4471983,6.79195946 C55.4471983,7.45783784 55.5812724,7.85736486 55.8494205,8.25689189 C56.1175687,8.65641892 56.5197909,8.92277027 57.0560872,8.92277027 C57.4583094,8.92277027 57.8605317,8.78959459 58.1286798,8.39006757 C58.396828,8.12371622 58.530902,7.72418919 58.530902,7.19148649 Z"></path>
|
||||
<path d="M71.9383094,6.65878378 C71.9383094,7.59101351 71.6701613,8.39006757 71.133865,9.05594595 C70.5975687,9.72182432 69.7931243,9.98817568 68.8546057,9.98817568 C67.9160872,9.98817568 67.2457168,9.72182432 66.5753465,9.05594595 C66.0390502,8.39006757 65.770902,7.72418919 65.770902,6.79195946 C65.770902,5.85972973 66.0390502,5.06067568 66.5753465,4.3947973 C67.1116428,3.72891892 67.9160872,3.46256757 68.8546057,3.46256757 C69.7931243,3.46256757 70.4634946,3.72891892 71.133865,4.3947973 C71.6701613,5.06067568 71.9383094,5.72655405 71.9383094,6.65878378 Z M70.5975687,6.79195946 C70.5975687,6.25925676 70.4634946,5.72655405 70.1953465,5.32702703 C69.9271983,4.79432432 69.5249761,4.52797297 68.9886798,4.52797297 C68.4523835,4.52797297 68.0501613,4.79432432 67.6479391,5.32702703 C67.3797909,5.72655405 67.2457168,6.25925676 67.2457168,6.79195946 C67.2457168,7.32466216 67.3797909,7.85736486 67.6479391,8.25689189 C67.9160872,8.78959459 68.3183094,9.05594595 68.9886798,9.05594595 C69.5249761,9.05594595 69.9271983,8.78959459 70.1953465,8.25689189 C70.4634946,7.85736486 70.5975687,7.32466216 70.5975687,6.79195946 Z"></path>
|
||||
<path d="M79.5805317,9.855 L78.2397909,9.855 L78.2397909,6.25925676 C78.2397909,5.19385135 77.8375687,4.66114865 76.8990502,4.66114865 C76.496828,4.66114865 76.0946057,4.79432432 75.8264576,5.06067568 C75.5583094,5.32702703 75.4242354,5.72655405 75.4242354,6.12608108 L75.4242354,9.855 L74.0834946,9.855 L74.0834946,5.32702703 C74.0834946,4.79432432 74.0834946,4.12844595 74.0834946,3.59574324 L75.2901613,3.59574324 L75.2901613,4.52797297 C75.4242354,4.26162162 75.6923835,3.99527027 75.9605317,3.72891892 C76.3627539,3.46256757 76.7649761,3.32939189 77.3012724,3.32939189 C77.8375687,3.32939189 78.373865,3.46256757 78.7760872,3.86209459 C79.3123835,4.26162162 79.4464576,5.06067568 79.4464576,5.99290541 L79.4464576,9.855 L79.5805317,9.855 Z"></path>
|
||||
<path d="M88.9657168,4.66114865 L87.356828,4.66114865 L87.356828,7.72418919 C87.356828,8.52324324 87.6249761,8.92277027 88.1612724,8.92277027 C88.4294205,8.92277027 88.5634946,8.92277027 88.8316428,8.92277027 L88.8316428,9.98817568 C88.5634946,10.1213514 88.1612724,10.1213514 87.7590502,10.1213514 C87.2227539,10.1213514 86.8205317,9.98817568 86.4183094,9.58864865 C86.1501613,9.18912162 86.0160872,8.65641892 86.0160872,7.85736486 L86.0160872,4.66114865 L85.0775687,4.66114865 L85.0775687,3.59574324 L86.0160872,3.59574324 L86.0160872,2.39716216 L87.356828,1.99763514 L87.356828,3.59574324 L88.9657168,3.59574324 L88.9657168,4.66114865 Z"></path>
|
||||
<path d="M96.473865,9.855 L95.1331243,9.855 L95.1331243,6.25925676 C95.1331243,5.19385135 94.730902,4.52797297 93.9264576,4.52797297 C93.2560872,4.52797297 92.853865,4.79432432 92.5857168,5.4602027 C92.5857168,5.59337838 92.5857168,5.72655405 92.5857168,5.99290541 L92.5857168,9.72182432 L91.2449761,9.72182432 L91.2449761,0.532702703 L92.5857168,0.532702703 L92.5857168,4.26162162 C92.9879391,3.59574324 93.6583094,3.19621622 94.4627539,3.19621622 C94.9990502,3.19621622 95.5353465,3.32939189 95.9375687,3.72891892 C96.473865,4.26162162 96.6079391,4.9275 96.6079391,5.85972973 C96.473865,6.12608108 96.473865,9.855 96.473865,9.855 Z"></path>
|
||||
<path d="M104.116087,6.52560811 C104.116087,6.79195946 104.116087,6.92513514 104.116087,7.19148649 L99.9597909,7.19148649 C99.9597909,7.85736486 100.227939,8.25689189 100.630161,8.65641892 C101.032384,8.92277027 101.434606,9.05594595 101.970902,9.05594595 C102.641272,9.05594595 103.177569,8.92277027 103.713865,8.78959459 L103.982013,9.72182432 C103.445717,9.98817568 102.641272,10.1213514 101.836828,10.1213514 C100.898309,10.1213514 100.093865,9.855 99.5575687,9.3222973 C99.0212724,8.78959459 98.7531243,7.99054054 98.7531243,7.05831081 C98.7531243,6.12608108 99.0212724,5.32702703 99.5575687,4.66114865 C100.093865,3.99527027 100.898309,3.59574324 101.702754,3.59574324 C102.641272,3.59574324 103.311643,3.99527027 103.713865,4.66114865 C103.982013,5.06067568 104.116087,5.72655405 104.116087,6.52560811 Z M102.775346,6.12608108 C102.775346,5.72655405 102.641272,5.32702703 102.507198,5.06067568 C102.23905,4.66114865 101.836828,4.52797297 101.434606,4.52797297 C101.032384,4.52797297 100.630161,4.66114865 100.362013,5.06067568 C100.093865,5.32702703 99.9597909,5.72655405 99.9597909,6.12608108 C99.8257168,6.12608108 102.775346,6.12608108 102.775346,6.12608108 Z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 23 KiB |
BIN
static/img/favicon.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 180 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB |
@ -1,96 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="description" content="LNDhub - Open source Bank on top of the lightning network">
|
||||
<meta name="author" content="Nuno Coelho">
|
||||
<meta content="http://lndhub.io" property="og:url">
|
||||
<meta content="lndhub.io" property="og:title">
|
||||
<meta content="lndhub" property="og:description">
|
||||
<meta content="http://lndhub.io/img/lndhub.svg" property="og:image">
|
||||
<meta content="800" property="og:image:width">
|
||||
<meta content="800" property="og:image:height">
|
||||
<link rel="icon" href="/static/favicon.png">
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
|
||||
<title>LNDhub - Open source Bank</title>
|
||||
<!-- Custom styles for this template -->
|
||||
<link href="/static/css/lndhub.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="cover-container mx-auto">
|
||||
<header class="masthead mb-auto">
|
||||
<div class="inner">
|
||||
<h3 class="masthead-brand"><img src="/static/img/lndhub.svg"></h3>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main role="main" class="inner cover">
|
||||
<h1 class="cover-heading">An open source bank</h1>
|
||||
|
||||
<p class="lead"><strong>LNDHub is an opensource wrapper for Lightning Network Daemon. It provides <br/>
|
||||
separate accounts with minimum trust for end users.</strong></p>
|
||||
|
||||
<p class="lead">We want to hide the complexity of running a node, and take over the work of a hub operator. Users want to make payments without the hassle of operating a node, opening channels, take care of liquidity or watching these channels.</p>
|
||||
<p class="lead">We want to bring Lightning payments to everyone and speed up Bitcoin adoption.</p>
|
||||
<p class="lead">We called it <strong>LNDhub</strong>.</p>
|
||||
<div class="github">
|
||||
<!-- Place this tag where you want the button to render. -->
|
||||
<a class="github-button" href="https://github.com/bluewallet/lndhub" data-size="large" data-show-count="true" aria-label="Star bluewallet/lndhub on GitHub">Star</a>
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
</main>
|
||||
</div>
|
||||
<div class="explain">
|
||||
<div class="cover-container mx-auto">
|
||||
<h2>How does it work?</h2>
|
||||
<img class="lndhub-graph" src="/static/img/lndhub-graph.png">
|
||||
<p class="lead">When the user sends Bitcoin to a dedicated top-up address, this balance is added to his account on LNDhub. Then, the user can use this balance to pay Lightning invoices, from everything to coffees to any micro-transaction he wants. But under the hood, it’s actually LNDhub who pays the invoice, deducting the user’s account balance. It works the same way when the user wants to receive a Lightning payment - it’s LNDhub who creates a Lightning invoice and actually receives the value on one of its channels.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="promote">
|
||||
<div class="cover-container mx-auto">
|
||||
<h2>Where can I test it?</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-6 text-center">
|
||||
<img class="lndhub-graph" src="/static/img/iphoneX.png">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p class="lead">Live on <strong>Blue wallet</strong>, a bitcoin wallet for iOS and android. <strong>LNDhub</strong> implementation on Blue wallet takes full advantage of this revolutionary payments network.</p>
|
||||
<p>Enabling Blue wallet users to have unfairly cheap and fast Bitcoin transactions..
|
||||
</p>
|
||||
<ul class="list-unstyled cta">
|
||||
<li><a class="badge-link2" href="https://itunes.apple.com/us/app/bluewallet-bitcoin-wallet/id1376878040?l=ru&ls=1&mt=8" target="_blank">
|
||||
<img src="/static/img/app-store-badge.svg" alt="app store link"></a></li>
|
||||
<li><a href="https://play.google.com/store/apps/details?id=io.bluewallet.bluewallet" target="_blank">
|
||||
<img src="/static/img/play-store-badge.svg" alt="play store link"></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="promote">
|
||||
<div class="cover-container mx-auto">
|
||||
<h2>Why should you use it?</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<p class="lead">Read our article on Medium on the reasoning behind the system and the pros and cons.</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<a href="https://medium.com/bluewallet/bluewallet-brings-zero-configuration-lightning-payments-to-ios-and-android-30137a69f071" class="btn btn-lg btn-block btn-dark">read here →</a>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="mastfoot mt-auto">
|
||||
<div class="inner">
|
||||
<p> </p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Place this tag in your head or just before your close body tag. -->
|
||||
<script async defer src="https://buttons.github.io/buttons.js"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -7,29 +7,23 @@
|
||||
*
|
||||
+ + + + + + + + + + + + + + + + + + + + + */
|
||||
let fs = require('fs');
|
||||
let winston = require('winston');
|
||||
let createLogger = winston.createLogger;
|
||||
let format = winston.format;
|
||||
let transports = winston.transports;
|
||||
import { createLogger, format, transports } from 'winston';
|
||||
|
||||
/* + + + + + + + + + + + + + + + + + + + + +
|
||||
// Start
|
||||
+ + + + + + + + + + + + + + + + + + + + + */
|
||||
const { combine, timestamp, printf } = format;
|
||||
const logFormat = printf(info => {
|
||||
const logFormat = printf((info) => {
|
||||
return `${info.timestamp} : ${info.level}: [${info.label}] : ${info.message}`;
|
||||
});
|
||||
const logger = createLogger({
|
||||
level: 'info',
|
||||
format: combine(timestamp(), logFormat),
|
||||
transports: [
|
||||
new transports.File({
|
||||
filename: './logs/error.log',
|
||||
new transports.Console({
|
||||
level: 'error',
|
||||
}),
|
||||
new transports.File({
|
||||
filename: './logs/out.log',
|
||||
}),
|
||||
new transports.Console(),
|
||||
],
|
||||
});
|
||||
|
||||
@ -40,12 +34,7 @@ if (!fs.existsSync('logs')) {
|
||||
fs.mkdirSync('logs');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} label group label
|
||||
* @param {string} message log message
|
||||
*/
|
||||
function log(label, message) {
|
||||
console.log(new Date(), label, message);
|
||||
logger.log({
|
||||
level: 'info',
|
||||
label: label,
|
||||
|
||||