146 Commits

Author SHA1 Message Date
Overtorment
fc0d2a4c86 RE: v1.2.0 2020-05-04 14:11:56 +01:00
Overtorment
b125d68fff Merge branch 'master' of github.com:BlueWallet/LndHub 2020-05-04 14:10:22 +01:00
Overtorment
e390a06aae ADD: process-unpaid-invoices; REF: stuck payments script; REF: website; REF: important channels script; DOC: some docs 2020-05-04 14:09:37 +01:00
snyk-bot
c8ef1e59e2 fix: upgrade ioredis from 4.16.1 to 4.16.2
Snyk has created this PR to upgrade ioredis from 4.16.1 to 4.16.2.

See this package in NPM:
https://www.npmjs.com/package/ioredis

See this project in Snyk:
https://app.snyk.io/org/bluewallet/project/29c066bc-abce-44d9-b68e-064466e610e7?utm_source=github&utm_medium=upgrade-pr
2020-05-04 11:45:14 +01:00
snyk-bot
3ad92da8a6 fix: upgrade ioredis from 4.16.1 to 4.16.2
Snyk has created this PR to upgrade ioredis from 4.16.1 to 4.16.2.

See this package in NPM:
https://www.npmjs.com/package/ioredis

See this project in Snyk:
https://app.snyk.io/org/bluewallet/project/29c066bc-abce-44d9-b68e-064466e610e7?utm_source=github&utm_medium=upgrade-pr
2020-05-04 11:45:14 +01:00
snyk-bot
61956f2a46 fix: upgrade prettier from 2.0.3 to 2.0.4
Snyk has created this PR to upgrade prettier from 2.0.3 to 2.0.4.

See this package in NPM:
https://www.npmjs.com/package/prettier

See this project in Snyk:
https://app.snyk.io/org/bluewallet/project/29c066bc-abce-44d9-b68e-064466e610e7?utm_source=github&utm_medium=upgrade-pr
2020-04-29 16:15:15 +01:00
snyk-bot
93899facd6 fix: upgrade prettier from 2.0.3 to 2.0.4
Snyk has created this PR to upgrade prettier from 2.0.3 to 2.0.4.

See this package in NPM:
https://www.npmjs.com/package/prettier

See this project in Snyk:
https://app.snyk.io/org/bluewallet/project/29c066bc-abce-44d9-b68e-064466e610e7?utm_source=github&utm_medium=upgrade-pr
2020-04-29 16:15:15 +01:00
snyk-bot
6723094ed6 fix: upgrade prettier from 2.0.2 to 2.0.3
Snyk has created this PR to upgrade prettier from 2.0.2 to 2.0.3.

See this package in NPM:
https://www.npmjs.com/package/prettier

See this project in Snyk:
https://app.snyk.io/org/bluewallet/project/29c066bc-abce-44d9-b68e-064466e610e7?utm_source=github&utm_medium=upgrade-pr
2020-04-28 16:59:39 +01:00
snyk-bot
ca1f5427cb fix: upgrade prettier from 2.0.2 to 2.0.3
Snyk has created this PR to upgrade prettier from 2.0.2 to 2.0.3.

See this package in NPM:
https://www.npmjs.com/package/prettier

See this project in Snyk:
https://app.snyk.io/org/bluewallet/project/29c066bc-abce-44d9-b68e-064466e610e7?utm_source=github&utm_medium=upgrade-pr
2020-04-28 16:59:39 +01:00
Overtorment
d4e8d9b476 Merge pull request #61 from bitarmy/master
Minimal changes
2020-04-02 10:46:53 +01:00
Agustin Kassis
3a2ee779b6 Revert config change 2020-04-01 23:08:02 -03:00
Agustin Kassis
ea1035e414 Lookup invoice on lnd if not already cached as paid on redis 2020-04-01 22:58:24 -03:00
Agustin Kassis
7afb56398a Updated npm packages 2020-04-01 20:52:22 -03:00
Agustin Kassis
a4cd81a106 Removed duplicated balance api 2020-04-01 20:51:14 -03:00
Agustin Kassis
7a94225d87 Error typos 2020-04-01 20:46:48 -03:00
Agustin Kassis
9be413a549 added tls.cert debug 2020-04-01 20:43:36 -03:00
Agustin Kassis
305559174f Removed checkinvoicepaid 2020-04-01 20:43:13 -03:00
Agustin Kassis
bfdd319070 Height check only on mainnet 2020-04-01 20:32:22 -03:00
Agustin Kassis
a6b08363a7 Merge branch 'bluewallet-lndhub'
# Conflicts:
#	controllers/api.js
2020-03-30 21:24:32 -03:00
Overtorment
9f45c31618 REL: ver bump 2020-03-27 21:09:55 +00:00
Overtorment
d7e91f51ec REF: get user invoices 2020-03-27 21:03:38 +00:00
Overtorment
b58e7dac88 stricted rate limiting for /getuserinvoices 2020-03-27 20:09:50 +00:00
Overtorment
7b5c6bc7e0 REF: more logging 2020-03-27 19:51:06 +00:00
Agustin Kassis
f9b2cca0bd Check individual invoice's payment 2020-03-23 13:44:44 +00:00
Agustin Kassis
376a3402bd Merge branch 'bluewallet-lndhub' 2020-03-20 20:32:07 -03:00
Agustin Kassis
38a3dea8c2 Check individual invoice's payment 2020-03-20 19:56:22 -03:00
snyk-bot
cdcd1fdd82 fix: upgrade babel-eslint from 10.0.3 to 10.1.0
Snyk has created this PR to upgrade babel-eslint from 10.0.3 to 10.1.0.

See this package in NPM:
https://www.npmjs.com/package/babel-eslint

See this project in Snyk:
https://app.snyk.io/org/bluewallet/project/29c066bc-abce-44d9-b68e-064466e610e7?utm_source=github&utm_medium=upgrade-pr
2020-03-19 10:34:11 +00:00
dependabot[bot]
f4105292a3 Build(deps): Bump acorn from 6.3.0 to 6.4.1
Bumps [acorn](https://github.com/acornjs/acorn) from 6.3.0 to 6.4.1.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/6.3.0...6.4.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-16 11:57:49 +00:00
Agustin Kassis
6074dc6c2c Merge branch 'bluewallet-lndhub'
# Conflicts:
#	package.json
2020-03-15 00:19:51 -03:00
snyk-bot
76421f2850 fix: upgrade ioredis from 4.15.1 to 4.16.0
Snyk has created this PR to upgrade ioredis from 4.15.1 to 4.16.0.

See this package in NPM:
https://www.npmjs.com/package/ioredis

See this project in Snyk:
https://app.snyk.io/org/bluewallet/project/29c066bc-abce-44d9-b68e-064466e610e7?utm_source=github&utm_medium=upgrade-pr
2020-03-13 11:42:52 +00:00
Overtorment
f01c533899 REF: minor 2020-03-11 16:01:35 +00:00
Overtorment
6b67839414 REF 2020-03-02 12:34:43 +00:00
Overtorment
dfe55a4f9f Merge branch 'master' of github.com:BlueWallet/LndHub 2020-01-25 15:41:55 +00:00
Overtorment
14d42a7aed REF 2020-01-25 15:41:49 +00:00
snyk-bot
a4b24ec00a fix: upgrade mustache from 3.1.0 to 3.2.1
Snyk has created this PR to upgrade mustache from 3.1.0 to 3.2.1.

See this package in NPM:
https://www.npmjs.com/package/mustache

See this project in Snyk:
https://app.snyk.io/org/bluewallet/project/29c066bc-abce-44d9-b68e-064466e610e7?utm_source=github&utm_medium=upgrade-pr
2020-01-25 15:00:02 +00:00
snyk-bot
0a0bf5caa3 fix: upgrade ioredis from 4.14.1 to 4.15.1
Snyk has created this PR to upgrade ioredis from 4.14.1 to 4.15.1.

See this package in NPM:
https://www.npmjs.com/package/ioredis

See this project in Snyk:
https://app.snyk.io/org/bluewallet/project/29c066bc-abce-44d9-b68e-064466e610e7?utm_source=github&utm_medium=upgrade-pr
2020-01-25 14:59:49 +00:00
snyk-bot
2f1c89bb8f fix: upgrade prettier from 1.18.2 to 1.19.1
Snyk has created this PR to upgrade prettier from 1.18.2 to 1.19.1.

See this package in NPM:
https://www.npmjs.com/package/prettier

See this project in Snyk:
https://app.snyk.io/org/bluewallet/project/29c066bc-abce-44d9-b68e-064466e610e7?utm_source=github&utm_medium=upgrade-pr
2020-01-25 14:58:36 +00:00
snyk-bot
243902faad fix: upgrade eslint-plugin-prettier from 3.1.1 to 3.1.2
Snyk has created this PR to upgrade eslint-plugin-prettier from 3.1.1 to 3.1.2.

See this package in NPM:
https://www.npmjs.com/package/eslint-plugin-prettier

See this project in Snyk:
https://app.snyk.io/org/bluewallet/project/29c066bc-abce-44d9-b68e-064466e610e7?utm_source=github&utm_medium=upgrade-pr
2020-01-25 14:57:47 +00:00
Overtorment
03ddddbf6a Merge branch 'master' of github.com:BlueWallet/LndHub 2020-01-24 21:20:03 +00:00
Overtorment
8e06bef3b9 REF: better error handling 2020-01-24 21:19:54 +00:00
Overtorment
b55e0f0327 FIX: fees 2020-01-24 09:44:13 +00:00
Overtorment
ec9a71f4e9 FIX: account for msats in /payinvoice 2020-01-24 00:19:27 +00:00
Overtorment
9d8466b595 FIX: invoice description decode 2020-01-23 23:09:50 +00:00
Overtorment
0f6a4d8cba FIX: default invoice expiry is 24h 2020-01-19 23:43:45 +00:00
Overtorment
b56064a7ba error logging 2020-01-18 13:39:55 +00:00
Agustin Kassis
6507de9770 Updated npm packages to latest version
Remove blockcount check for regtest
FIX
FIX: change default ln invoice expiry hour -> day
REF
FIX: added some caching; skip checking old invoices
Update README.md
2020-01-14 13:25:51 -03:00
Overtorment
1e17b9925b FIX 2019-12-21 15:00:36 +00:00
Overtorment
41736cccd8 FIX: change default ln invoice expiry hour -> day 2019-12-14 19:51:44 +00:00
Overtorment
dd409cc04f REF 2019-12-14 19:26:06 +00:00
Overtorment
daeae3c72d FIX: added some caching; skip checking old invoices 2019-12-14 19:25:14 +00:00
Overtorment
3af9cda96d Update README.md 2019-12-09 21:41:25 +00:00
Agustin Kassis
8f28fd2865 Updated npm packages to latest version 2019-11-26 18:12:00 -03:00
Agustin Kassis
013cadc5c8 Remove blockcount check for regtest 2019-11-26 18:05:12 -03:00
Overtorment
ba38ed330d OPS 2019-11-04 15:09:19 +00:00
Overtorment
bac9480f31 OPS 2019-11-04 14:51:51 +00:00
Overtorment
5895573112 Merge branch 'master' of github.com:BlueWallet/LndHub 2019-11-04 14:36:23 +00:00
Overtorment
d595120a05 OPS 2019-11-04 14:36:15 +00:00
Kim Neunert
60ea4b2368 MIT License added see #40 2019-11-04 14:23:43 +00:00
Overtorment
8aa496938e DOC 2019-05-18 12:23:49 +01:00
Overtorment
070a7d8232 ADD: some util cli scripts 2019-05-03 22:33:54 +01:00
Overtorment
cc4503ecbd ADD: preimage in /gettxs response 2019-05-03 22:31:22 +01:00
Overtorment
cf5441c08a FIX: NaN 2019-04-14 20:06:50 +01:00
Overtorment
2c25a2090e FIX: better stuck payments handling 2019-04-13 18:56:12 +01:00
Overtorment
35d8ff5599 FIX: improve stuck payments processing 2019-04-13 00:27:07 +01:00
Overtorment
4a871a7d17 FIX: listtransactions caching 2019-04-06 20:21:13 +01:00
alerobrod
6a6611e9b6 Issue #12: Fix to ensure the Bluewallet app does not see on-chain Tx(s) if LND was to funnel channels using the user's BTC address for deposits. 2019-04-06 18:38:07 +01:00
alerobrod
16c6d167da Issue #12: Fetching all on-chain txs and filtering based on user's address. Note: This permits running LndHub without -deprecatedrpc=accounts switch. 2019-04-06 18:38:07 +01:00
Overtorment
70262b1059 FIX 2019-04-04 17:22:54 +01:00
Overtorment
724bc3d656 REF 2019-04-04 14:10:05 +01:00
Overtorment
862cda69fa FIX: internal invoice fee 2019-04-04 13:38:09 +01:00
Overtorment
75bfac565c REF 2019-04-04 12:51:46 +01:00
Overtorment
0f423817c3 ADD: rate limiting 2019-03-21 21:45:22 +00:00
Overtorment
995374ff48 ADD: rate limiting 2019-03-21 20:49:45 +00:00
Overtorment
0fed216b32 FIX: fee_limit when paying 2019-03-19 00:11:25 +00:00
Overtorment
067f2aeae4 FIX 2019-03-14 00:04:01 +00:00
Overtorment
f0d9fee177 Merge branch 'master' of github.com:BlueWallet/LndHub 2019-03-13 23:51:39 +00:00
Overtorment
a74f1e9ff2 FIX: adjusted fees; cache invalidation 2019-03-13 23:51:28 +00:00
Dimitris Tsapakidis
43ceee11ce Fix typos in Send-requirements.md 2019-03-13 20:25:17 +00:00
Overtorment
7c71c5e775 DOC 2019-03-13 00:21:27 +00:00
Igor Korsakov
92f1370dba Merge pull request #25 from BlueWallet/unlock-payments-script_ver2
ADD: unlock stuck payments script
2019-03-10 23:41:10 +00:00
Overtorment
c1ac67a853 ADD: unlock stuck payments script 2019-03-10 23:39:48 +00:00
Igor Korsakov
7517b02d1e Merge pull request #24 from BlueWallet/revert-23-unlock-payments-script
Revert "ADD: unlock stuck payments script"
2019-03-10 23:38:36 +00:00
Igor Korsakov
454dc50693 Revert "ADD: unlock stuck payments script" 2019-03-10 23:38:23 +00:00
Igor Korsakov
6cf75686da Merge pull request #23 from BlueWallet/unlock-payments-script
ADD: unlock stuck payments script
2019-03-10 23:34:18 +00:00
Overtorment
7eea6c650f ADD: unlock stuck payments script 2019-03-10 23:32:34 +00:00
Overtorment
1ad26e21b8 ADD: Proper error message when payment fails 2019-03-06 23:59:06 +00:00
Overtorment
f7ed99173f Merge branch 'master' of github.com:BlueWallet/LndHub 2019-03-06 20:21:51 +00:00
Overtorment
402d07a97f FIX: sometimes feex are x10 bigger than they should 2019-03-06 20:21:37 +00:00
Igor Korsakov
6fa5791d89 Merge pull request #21 from BlueWallet/revert-20-master
Revert "Some minor fixes"
2019-03-06 20:14:54 +00:00
Igor Korsakov
31b718e115 Revert "Some minor fixes" 2019-03-06 20:14:39 +00:00
Igor Korsakov
1da578e8a4 Merge pull request #20 from Mikefluff/master
Some minor fixes
2019-03-06 19:36:12 +00:00
Mike Fluff
3317e620dd Linter fixes 2019-03-06 11:36:19 +07:00
Mike Fluff
cb722e1608 Fix after merge 2019-03-06 11:34:17 +07:00
Mike Fluff
f624ced5ce Fix with null result TXs fetcher 2019-03-06 11:27:40 +07:00
Mike Fluff
27ebe36b4b Merge branch 'master' of github.com:BlueWallet/LndHub 2019-03-06 11:02:45 +07:00
Mike Fluff
65a9541673 TLS config fix 2019-03-06 11:01:59 +07:00
Overtorment
77fb7c0daa FIX: display negative balance as zero 2019-03-04 12:16:03 +00:00
Overtorment
23e835c888 FIX: fees 2019-03-03 22:01:15 +00:00
Overtorment
91f552ca7e ADD: fees 2019-03-03 18:57:45 +00:00
Overtorment
8f2d98774b FIX: properly locked payments 2019-03-03 15:59:30 +00:00
Overtorment
afb07aa9ba REF: remove debug 2019-03-01 22:47:43 +00:00
Overtorment
3cbe38ed28 FIX: increase lock 2019-03-01 22:41:50 +00:00
Overtorment
17cd8adf41 REF 2019-02-28 23:07:31 +00:00
Overtorment
1a740d6b06 REF 2019-02-28 22:23:21 +00:00
Overtorment
ef9cc6d388 Merge branch 'master' of github.com:BlueWallet/LndHub 2019-02-16 13:07:19 +00:00
Overtorment
a7cfbca1e7 DOC 2019-02-16 13:07:08 +00:00
Overtorment
daa25d1b75 OPS 2019-02-16 12:58:34 +00:00
Igor Korsakov
fe6cf0e860 Update README.md 2019-02-07 14:46:00 +00:00
Overtorment
e9bf03b58b Merge branch 'master' of github.com:BlueWallet/LndHub 2019-02-06 20:52:22 +00:00
Overtorment
efd31067ba REF: default lock time 2 min -> 5 min 2019-02-06 20:50:58 +00:00
Igor Korsakov
db9ccb9dbe Merge pull request #10 from marcosrdz/patch-1
ADD: Added  '1.ln.aantonop.com',
2019-02-03 21:56:12 +00:00
Marcos Rodriguez Vélez
2f5f300a90 ADD: Added '1.ln.aantonop.com', 2019-02-03 16:22:20 -05:00
Overtorment
216de588a0 Merge branch 'master' of github.com:BlueWallet/LndHub 2019-02-02 23:22:49 +00:00
Overtorment
6d2abdfcce OPS: logging 2019-02-02 23:22:33 +00:00
Igor Korsakov
dd288d413f Update README.md 2019-02-01 19:28:26 +00:00
Overtorment
d04b155e11 FIX: can pay negative free amount invoices 2019-02-01 19:06:47 +00:00
Overtorment
e59ea298d1 REF: removing unsued by client fields to reduce size 2019-01-28 21:39:34 +00:00
Overtorment
d6bb2bbe25 REF 2019-01-26 00:15:29 +00:00
Overtorment
da37b5315d REF: better logging 2019-01-25 23:44:07 +00:00
Overtorment
1df24d0e80 ADD: lnd wallet unlock attempt upon start 2019-01-25 22:59:32 +00:00
Overtorment
0f75933bf0 FIX: no crash 2019-01-24 20:14:27 +00:00
Overtorment
4a9c4ef996 Merge branch 'master' of github.com:BlueWallet/LndHub 2019-01-23 12:12:27 +00:00
Overtorment
dec313aa4c FIX: incorrect balance after invoice paid 2019-01-23 12:12:16 +00:00
Overtorment
b85097f7cc OPS: dep 2019-01-23 11:55:36 +00:00
Overtorment
c1e13e9c8c FIX: (re #8) 2019-01-21 20:18:04 +00:00
Overtorment
e854626a45 FIX: more strict balance check (re #8) 2019-01-21 19:49:19 +00:00
Overtorment
f2386f0acf REF: retry 2019-01-21 14:18:09 +00:00
Overtorment
ff2d98dc10 OPS: turn off explicit logging 2019-01-21 13:17:23 +00:00
Overtorment
9c47746eba FIX: redis balance is only a cashe; tx list is a source of truth for user's balance 2019-01-21 13:17:04 +00:00
Overtorment
a08f02572f ADD: limit to getUserInvoices 2019-01-10 17:01:46 +00:00
Overtorment
f2616044eb ADD: globee 2019-01-10 12:32:01 +00:00
Overtorment
4317123ffe FIX (rel #3) 2019-01-09 02:38:54 +00:00
Overtorment
dd7430503d FIX: able to pay same internal invoice twice (closes #5) 2019-01-09 01:56:09 +00:00
Overtorment
d8bd2aacdf FIX: better locking when importing onchain tx 2019-01-07 17:25:04 +00:00
Overtorment
b7073abb0e FIX: better locks (rel #3) 2019-01-07 16:40:32 +00:00
Overtorment
98ba940342 REL 2019-01-07 16:21:35 +00:00
Overtorment
6851769be6 FIX: no crashes, just hangs 2019-01-06 14:36:23 +00:00
Overtorment
d3100a1390 FIX: occasional duplicate payments (closes #3) 2019-01-06 14:35:14 +00:00
Overtorment
6a57559b16 REF 2019-01-06 13:37:37 +00:00
Overtorment
4f345b20a8 FIX: potential crash 2019-01-06 13:17:52 +00:00
Overtorment
a42d331e38 FIX: potential crash 2019-01-06 12:44:41 +00:00
Overtorment
130a8dc773 FIX: potential crash 2019-01-06 12:19:08 +00:00
Overtorment
b3c0873e4c FIX: more logging, more error handling 2019-01-05 22:01:50 +00:00
Overtorment
6a3de700f4 FIX: race condition when processing onchain topup 2019-01-05 21:27:34 +00:00
Overtorment
51b0f89fd1 REF: better channels display 2019-01-05 18:10:05 +00:00
Overtorment
55ac146f1e FIX: potential crash 2019-01-05 18:09:30 +00:00
25 changed files with 2606 additions and 1134 deletions

21
LICENSE Normal file
View 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.

View File

@@ -6,17 +6,24 @@ Wrapper for Lightning Network Daemon. It provides separate accounts with minimum
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`.
Edit `config.js` and set it up correctly.
Install `bitcoind`, `lnd` and `redis`. Edit `config.js` and set it up correctly.
Copy `admin.macaroon` and `tls.cert` in root folder of LndHub.
`bitcoind` should run with `-deprecatedrpc=accounts`, for now. Lndhub expects Lnd's wallet to be unlocked, if not - it will attempt to unlock it with password stored in `config.lnd.password`.
Don't forget to enable disk-persistance for `redis`.
### Deploy to Heroku
Add config vars :
@@ -24,7 +31,22 @@ Add config vars :
* `MACAROON`: hex-encoded `admin.macaroon`
* `TLSCERT`: hex-encoded `tls.cert`
### Reference client implementation
Can be used in ReactNative or Nodejs environment
* https://github.com/BlueWallet/BlueWallet/blob/master/class/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/LightningCustodianWallet.test.js
![image](https://user-images.githubusercontent.com/1913337/52418916-f30beb00-2ae6-11e9-9d63-17189dc1ae8c.png)
## Responsible disclosure
Found critical bugs/vulnerabilities? Please email them bluewallet@bluewallet.io
Thanks!

View File

@@ -3,5 +3,5 @@ const config = require('./config');
let jayson = require('jayson/promise');
let url = require('url');
let rpc = url.parse(config.bitcoind.rpc);
rpc.timeout = 5000;
rpc.timeout = 15000;
module.exports = jayson.client.http(rpc);

86
class/Invo.js Normal file
View File

@@ -0,0 +1,86 @@
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, true);
}
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, isPaid) {
if (isPaid) {
return await this._redis.set('ispaid_' + paymentHash, 1);
} else {
return await this._redis.del('ispaid_' + paymentHash);
}
}
async _getIsPaymentHashMarkedPaidInDatabase(paymentHash) {
return await this._redis.get('ispaid_' + paymentHash);
}
/**
* Queries LND ofr all user invoices
*
* @return {Promise<array>}
*/
async listInvoices() {
return new Promise((resolve, reject) => {
this._lightning.listInvoices(
{
num_max_invoices: 9000111,
},
function(err, response) {
if (err) return reject(err);
resolve(response);
},
);
});
}
}

35
class/Lock.js Normal file
View 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
View 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;
}
static get fee() {
return 0.003;
}
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 * 0.01) + 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 * Paym.fee);
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 * 0.01); // 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('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'];
}
}

View File

@@ -1,7 +1,14 @@
import { Lock } from './Lock';
var crypto = require('crypto');
var lightningPayReq = require('bolt11');
import { BigNumber } from 'bignumber.js';
// static cache:
let _invoice_ispaid_cache = {};
let _listtransactions_cache = false;
let _listtransactions_cache_expiry_ts = 0;
export class User {
/**
*
@@ -111,12 +118,72 @@ export class User {
});
}
/**
* 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() {
return (await this._redis.get('balance_for_' + this._userid)) * 1;
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 * 0.01);
}
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) {
@@ -145,6 +212,8 @@ 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);
@@ -162,13 +231,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);
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);
@@ -176,26 +261,29 @@ 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));
invoice.ispaid = _invoice_ispaid_cache[invoice.payment_hash] || !!(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;
if (invoice.ispaid) {
// so invoice was paid after all
await this.setPaymentHashPaid(invoice.payment_hash);
await this.saveBalance((await this.getBalance()) + decoded.satoshis);
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);
}
} else {
_invoice_ispaid_cache[invoice.payment_hash] = true;
}
invoice.amt = decoded.satoshis;
invoice.expire_time = 3600;
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';
@@ -211,17 +299,22 @@ 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) {
await this.generateAddress();
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]);
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);
}
@@ -236,6 +329,13 @@ export class User {
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;
}
@@ -243,12 +343,60 @@ export class User {
invoice.timestamp = invoice.decoded.timestamp;
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
}
}
}
let txs = await this._bitcoindrpc.request('listtransactions', ['*', 100500, 0, true]);
// now, compacting response a bit
let ret = { result: [] };
for (const tx of txs.result) {
ret.result.push({
category: tx.category,
amount: tx.amount,
confirmations: tx.confirmations,
address: tx.address,
time: tx.time,
});
}
_listtransactions_cache = JSON.stringify(ret);
_listtransactions_cache_expiry_ts = +new Date() + 5 * 60 * 1000; // 5 min
this._redis.set('listtransactions', _listtransactions_cache); // backup, will use later TODO
return ret;
}
/**
* Returning onchain txs for user's address that are less than 3 confs
*
@@ -256,12 +404,16 @@ export class User {
*/
async getPendingTxs() {
let addr = await this.getAddress();
if (!addr) {
await this.generateAddress();
addr = await this.getAddress();
}
if (!addr) throw new Error('cannot get transactions: no onchain address assigned to user');
let txs = await this._bitcoindrpc.request('listtransactions', [addr, 100500, 0, true]);
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);
}
}
@@ -294,8 +446,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;
@@ -304,14 +457,76 @@ 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;
}
_hash(string) {
return crypto
.createHash('sha256')
@@ -319,4 +534,20 @@ export class User {
.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));
}
}

View File

@@ -1 +1,4 @@
export * from './User';
export * from './Lock';
export * from './Paym';
export * from './Invo';

View File

@@ -11,6 +11,7 @@ let config = {
},
lnd: {
url: '1.1.1.1:10009',
password: '',
},
};

View File

@@ -1,14 +1,15 @@
import { User } from '../class/User';
import { User, Lock, Paym } from '../class/';
const config = require('../config');
let express = require('express');
let router = express.Router();
let logger = require('../utils/logger');
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));
// console.log('REDIS', JSON.stringify(args));
});
});
@@ -19,12 +20,12 @@ let identity_pubkey = false;
bitcoinclient.request('getblockchaininfo', false, function(err, info) {
if (info && info.result && info.result.blocks) {
if (info.result.blocks < 550000) {
if (info.result.chain === 'mainnet' && info.result.blocks < 550000) {
console.error('bitcoind is not caught up');
process.exit(1);
}
} else {
console.error('bitcoind failure');
console.error('bitcoind failure:', err, info);
process.exit(2);
}
});
@@ -32,9 +33,11 @@ bitcoinclient.request('getblockchaininfo', false, function(err, info) {
lightning.getInfo({}, function(err, info) {
if (err) {
console.error('lnd failure');
console.dir(err);
process.exit(3);
}
if (info) {
console.info(info);
if (!info.synced_to_chain) {
console.error('lnd not synced');
process.exit(4);
@@ -52,19 +55,27 @@ redis.info(function(err, info) {
// ######################## ROUTES ########################
router.post('/create', async function(req, res) {
const rateLimit = require('express-rate-limit');
const postLimiter = rateLimit({
windowMs: 30 * 60 * 1000,
max: 100,
});
router.post('/create', postLimiter, async function(req, res) {
logger.log('/create', [req.id]);
if (!(req.body.partnerid && req.body.partnerid === 'bluewallet' && req.body.accounttype)) return errorBadArguments(res);
let u = new User(redis);
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,15 +92,17 @@ 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) {
lightning.addInvoice({ memo: req.body.memo, value: req.body.amt, expiry: 3600 * 24 }, async function(err, info) {
if (err) return errorLnd(res);
info.pay_req = info.payment_request; // client backwards compatibility
@@ -100,80 +113,134 @@ router.post('/addinvoice', async function(req, res) {
});
router.post('/payinvoice', async function(req, res) {
let u = new User(redis);
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 (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);
}
let userBalance;
try {
userBalance = await u.getCalculatedBalance();
} catch (Error) {
logger.log('', [req.id, 'error running getCalculatedBalance():', Error.message]);
lock.releaseLock();
return errorTryAgainLater(res);
}
lightning.decodePayReq({ pay_req: req.body.invoice }, async function(err, info) {
if (err) return errorNotAValidInvoice(res);
if (err) {
await lock.releaseLock();
return errorNotAValidInvoice(res);
}
if (+info.num_satoshis === 0) {
// 'tip' invoices
info.num_satoshis = freeAmount;
}
if (userBalance >= info.num_satoshis) {
// got enough balance
logger.log('/payinvoice', [req.id, 'userBalance: ' + userBalance, 'num_satoshis: ' + info.num_satoshis]);
if (userBalance >= +info.num_satoshis + Math.floor(info.num_satoshis * 0.01)) {
// 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 * 1,
fee: 0, // internal invoices are free
value: +info.num_satoshis + Math.floor(info.num_satoshis * Paym.fee),
fee: Math.floor(info.num_satoshis * Paym.fee),
memo: decodeURIComponent(info.description),
pay_req: req.body.invoice,
});
await UserPayee.setPaymentHashPaid(info.payment_hash);
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, amt: info.num_satoshis }; // amt is used only for 'tip' invoices
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 * 0.005) + 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) {
logger.log('/getbtc', [req.id]);
let u = new User(redis, bitcoinclient, lightning);
await u.loadByAuthorization(req.headers.authorization);
@@ -190,20 +257,45 @@ router.get('/getbtc', async function(req, res) {
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);
if (!(await u.loadByAuthorization(req.headers.authorization))) {
await u.loadByAuthorization(req.headers.authorization);
if (!u.getUserId()) {
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 } });
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('/getinfo', async function(req, res) {
let u = new User(redis);
router.get('/balance', postLimiter, async function(req, res) {
try {
logger.log('/balance', [req.id]);
let u = new User(redis, bitcoinclient, lightning);
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.message, '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);
}
@@ -215,37 +307,58 @@ router.get('/getinfo', 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
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 * 0.01) /* feelimit */,
value: locked.amount + Math.floor(locked.amount * 0.01) /* feelimit */,
timestamp: locked.timestamp,
memo: 'Payment in transition',
});
}
res.send(txs);
} catch (Err) {
console.log(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) {
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();
@@ -254,7 +367,8 @@ router.get('/getpending', async function(req, res) {
});
router.get('/decodeinvoice', async function(req, res) {
let u = new User(redis, bitcoinclient);
logger.log('/decodeinvoice', [req.id]);
let u = new User(redis, bitcoinclient, lightning);
if (!(await u.loadByAuthorization(req.headers.authorization))) {
return errorBadAuth(res);
}
@@ -268,7 +382,8 @@ router.get('/decodeinvoice', async function(req, res) {
});
router.get('/checkrouteinvoice', async function(req, res) {
let u = new User(redis, bitcoinclient);
logger.log('/checkrouteinvoice', [req.id]);
let u = new User(redis, bitcoinclient, lightning);
if (!(await u.loadByAuthorization(req.headers.authorization))) {
return errorBadAuth(res);
}
@@ -299,7 +414,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',
});
}
@@ -323,7 +438,7 @@ function errorGeneralServerError(res) {
return res.send({
error: true,
code: 6,
message: 'Server fault',
message: 'Something went wrong. Please try again later',
});
}
@@ -334,3 +449,19 @@ 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?',
});
}

View File

@@ -3,39 +3,49 @@ let router = express.Router();
let fs = require('fs');
let mustache = require('mustache');
let lightning = require('../lightning');
let logger = require('../utils/logger');
let lightningGetInfo = {};
let lightningListChannels = {};
function updateLightning() {
lightning.getInfo({}, function(err, info) {
if (err) {
console.error('lnd failure');
process.exit(3);
}
lightningGetInfo = info;
});
console.log('updateLightning()');
try {
lightning.getInfo({}, function(err, info) {
if (err) {
console.error('lnd failure:', err);
}
lightningGetInfo = info;
});
lightning.listChannels({}, function(err, response) {
if (err) {
console.error('lnd failure');
process.exit(3);
}
lightningListChannels = response;
let channels = [];
for (let channel of lightningListChannels.channels) {
let divider = 524287;
let ascii_length1 = channel.local_balance / divider;
let ascii_length2 = channel.remote_balance / divider;
channel.ascii = '[';
channel.ascii += '-'.repeat(Math.round(ascii_length1));
channel.ascii += '/' + '-'.repeat(Math.round(ascii_length2));
channel.ascii += ']';
channel.capacity_btc = channel.capacity / 100000000;
channel.name = pubkey2name[channel.remote_pubkey];
channels.push(channel);
}
lightningListChannels.channels = channels;
});
lightning.listChannels({}, function(err, response) {
if (err) {
console.error('lnd failure:', err);
return;
}
lightningListChannels = response;
let channels = [];
for (let channel of lightningListChannels.channels) {
let divider = 5242870;
let ascii_length1 = channel.local_balance / divider;
let ascii_length2 = channel.remote_balance / divider;
channel.ascii = '[';
channel.ascii += '-'.repeat(Math.round(ascii_length1));
channel.ascii += '/' + '-'.repeat(Math.round(ascii_length2));
channel.ascii += ']';
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);
}
console.log('updated');
}
updateLightning();
setInterval(updateLightning, 60000);
@@ -51,9 +61,25 @@ const pubkey2name = {
'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',
'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',
};
router.get('/', function(req, res) {
logger.log('/', [req.id]);
if (!lightningGetInfo) {
console.error('lnd failure');
process.exit(3);
@@ -64,6 +90,7 @@ router.get('/', function(req, res) {
});
router.get('/about', function(req, res) {
logger.log('/about', [req.id]);
let html = fs.readFileSync('./templates/about.html').toString('utf8');
res.setHeader('Content-Type', 'text/html');
return res.status(200).send(mustache.render(html, {}));

View File

@@ -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.
@@ -36,7 +36,7 @@ associated with corresponding user id.
| 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 |
| 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.
@@ -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:
@@ -351,7 +351,7 @@ 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:
@@ -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:
@@ -508,8 +508,8 @@ Response:
## 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

18
doc/recover.md Normal file
View 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

View File

@@ -9,6 +9,8 @@ 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`
@@ -18,6 +20,8 @@ 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} = []

View File

@@ -1,3 +1,8 @@
process.on('uncaughtException', function(err) {
console.error(err);
console.log('Node NOT Exiting...');
});
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
let express = require('express');
let morgan = require('morgan');
@@ -9,6 +14,14 @@ morgan.token('id', function getId(req) {
});
let app = express();
app.enable('trust proxy');
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 200,
});
app.use(limiter);
app.use(function(req, res, next) {
req.id = uuid.v4();
@@ -21,8 +34,6 @@ app.use(
),
);
app.set('trust proxy', 'loopback');
let bodyParser = require('body-parser');
let config = require('./config');

View File

@@ -25,4 +25,23 @@ let macaroonCreds = grpc.credentials.createFromMetadataGenerator(function(args,
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) {
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) {
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 });

2185
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "LndHub",
"version": "1.1.0",
"version": "1.2.0",
"description": "",
"main": "index.js",
"scripts": {
@@ -14,25 +14,25 @@
"dependencies": {
"babel": "^6.23.0",
"babel-cli": "^6.26.0",
"babel-eslint": "^10.0.1",
"babel-eslint": "^10.1.0",
"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",
"bignumber.js": "^9.0.0",
"bolt11": "^1.2.6",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.1",
"eslint-plugin-prettier": "^3.1.2",
"express": "^4.16.4",
"express-rate-limit": "^5.0.0",
"grpc": "^1.17.0-pre1",
"ioredis": "^4.2.0",
"jayson": "^2.1.0",
"ioredis": "^4.16.2",
"jayson": "^3.1.2",
"morgan": "^1.9.1",
"mustache": "^3.0.1",
"mustache": "^4.0.1",
"node-uuid": "^1.4.8",
"prettier": "^1.15.3",
"prettier": "^2.0.4",
"request": "^2.88.0",
"request-promise": "^4.2.2",
"winston": "^3.1.0"

197
rpc.proto
View File

@@ -3,6 +3,9 @@ syntax = "proto3";
// import "google/api/annotations.proto";
package lnrpc;
option go_package = "github.com/lightningnetwork/lnd/lnrpc";
/**
* Comments in this file will be directly parsed into the API
* Documentation as descriptions of the associated method, message, or field.
@@ -231,6 +234,16 @@ service Lightning {
};
}
/** lncli: `listunspent`
ListUnspent returns a list of all utxos spendable by the wallet with a
number of confirmations between the specified minimum and maximum.
*/
rpc ListUnspent (ListUnspentRequest) returns (ListUnspentResponse) {
option (google.api.http) = {
get: "/v1/utxos"
};
}
/**
SubscribeTransactions creates a uni-directional stream from the server to
the client in which any newly discovered transactions relevant to the
@@ -260,7 +273,12 @@ service Lightning {
signature string is `zbase32` encoded and pubkey recoverable, meaning that
only the message digest and signature are needed for verification.
*/
rpc SignMessage (SignMessageRequest) returns (SignMessageResponse);
rpc SignMessage (SignMessageRequest) returns (SignMessageResponse) {
option (google.api.http) = {
post: "/v1/signmessage"
body: "*"
};
}
/** lncli: `verifymessage`
VerifyMessage verifies a signature over a msg. The signature must be
@@ -268,7 +286,12 @@ service Lightning {
channel database. In addition to returning the validity of the signature,
VerifyMessage also returns the recovered pubkey from the signature.
*/
rpc VerifyMessage (VerifyMessageRequest) returns (VerifyMessageResponse);
rpc VerifyMessage (VerifyMessageRequest) returns (VerifyMessageResponse) {
option (google.api.http) = {
post: "/v1/verifymessage"
body: "*"
};
}
/** lncli: `connect`
ConnectPeer attempts to establish a connection to a remote peer. This is at
@@ -336,6 +359,14 @@ service Lightning {
};
}
/** lncli: `subscribechannelevents`
SubscribeChannelEvents creates a uni-directional stream from the server to
the client in which any updates relevant to the state of the channels are
sent over. Events include new active channels, inactive channels, and closed
channels.
*/
rpc SubscribeChannelEvents (ChannelEventSubscription) returns (stream ChannelEventUpdate);
/** lncli: `closedchannels`
ClosedChannels returns a description of all the closed channels that
this node was a participant in.
@@ -455,10 +486,8 @@ service Lightning {
paginated responses, allowing users to query for specific invoices through
their add_index. This can be done by using either the first_index_offset or
last_index_offset fields included in the response as the index_offset of the
next request. The reversed flag is set by default in order to paginate
backwards. If you wish to paginate forwards, you must explicitly set the
flag to false. If none of the parameters are specified, then the last 100
invoices will be returned.
next request. By default, the first 100 invoices created will be returned.
Backwards pagination is also supported through the Reversed flag.
*/
rpc ListInvoices (ListInvoiceRequest) returns (ListInvoiceResponse) {
option (google.api.http) = {
@@ -647,6 +676,26 @@ service Lightning {
};
}
message Utxo {
/// The type of address
AddressType type = 1 [json_name = "address_type"];
/// The address
string address = 2 [json_name = "address"];
/// The value of the unspent coin in satoshis
int64 amount_sat = 3 [json_name = "amount_sat"];
/// The pkscript in hex
string pk_script = 4 [json_name = "pk_script"];
/// The outpoint in format txid:n
OutPoint outpoint = 5 [json_name = "outpoint"];
/// The number of confirmations for the Utxo
int64 confirmations = 6 [json_name = "confirmations"];
}
message Transaction {
/// The transaction hash
string tx_hash = 1 [ json_name = "tx_hash" ];
@@ -725,11 +774,18 @@ message SendRequest {
send the payment.
*/
FeeLimit fee_limit = 8;
/**
The channel id of the channel that must be taken to the first hop. If zero,
any channel may be used.
*/
uint64 outgoing_chan_id = 9;
}
message SendResponse {
string payment_error = 1 [json_name = "payment_error"];
bytes payment_preimage = 2 [json_name = "payment_preimage"];
Route payment_route = 3 [json_name = "payment_route"];
bytes payment_hash = 4 [json_name = "payment_hash"];
}
message SendToRouteRequest {
@@ -739,8 +795,16 @@ message SendToRouteRequest {
/// An optional hex-encoded payment hash to be used for the HTLC.
string payment_hash_string = 2;
/// The set of routes that should be used to attempt to complete the payment.
repeated Route routes = 3;
/**
Deprecated. The set of routes that should be used to attempt to complete the
payment. The possibility to pass in multiple routes is deprecated and
instead the single route field below should be used in combination with the
streaming variant of SendToRoute.
*/
repeated Route routes = 3 [deprecated = true];
/// Route that should be used to attempt to complete the payment.
Route route = 4;
}
message ChannelPoint {
@@ -756,6 +820,17 @@ message ChannelPoint {
uint32 output_index = 3 [json_name = "output_index"];
}
message OutPoint {
/// Raw bytes representing the transaction id.
bytes txid_bytes = 1 [json_name = "txid_bytes"];
/// Reversed, hex-encoded string representing the transaction id.
string txid_str = 2 [json_name = "txid_str"];
/// The index of the output on the transaction.
uint32 output_index = 3 [json_name = "output_index"];
}
message LightningAddress {
/// The identity pubkey of the Lightning node
string pubkey = 1 [json_name = "pubkey"];
@@ -791,24 +866,45 @@ message SendCoinsRequest {
/// A manual fee rate set in sat/byte that should be used when crafting the transaction.
int64 sat_per_byte = 5;
/**
If set, then the amount field will be ignored, and lnd will attempt to
send all the coins under control of the internal wallet to the specified
address.
*/
bool send_all = 6;
}
message SendCoinsResponse {
/// The transaction ID of the transaction
string txid = 1 [json_name = "txid"];
}
message ListUnspentRequest {
/// The minimum number of confirmations to be included.
int32 min_confs = 1;
/// The maximum number of confirmations to be included.
int32 max_confs = 2;
}
message ListUnspentResponse {
/// A list of utxos
repeated Utxo utxos = 1 [json_name = "utxos"];
}
/**
`AddressType` has to be one of:
- `p2wkh`: Pay to witness key hash (`WITNESS_PUBKEY_HASH` = 0)
- `np2wkh`: Pay to nested witness key hash (`NESTED_PUBKEY_HASH` = 1)
*/
message NewAddressRequest {
enum AddressType {
enum AddressType {
WITNESS_PUBKEY_HASH = 0;
NESTED_PUBKEY_HASH = 1;
}
UNUSED_WITNESS_PUBKEY_HASH = 2;
UNUSED_NESTED_PUBKEY_HASH = 3;
}
message NewAddressRequest {
/// The address type
AddressType type = 1;
}
@@ -944,8 +1040,11 @@ message Channel {
*/
uint32 csv_delay = 16 [json_name = "csv_delay"];
/// Whether this channel is advertised to the network or not
/// Whether this channel is advertised to the network or not.
bool private = 17 [json_name = "private"];
/// True if we were the ones that created the channel.
bool initiator = 18 [json_name = "initiator"];
}
@@ -1075,11 +1174,13 @@ message GetInfoResponse {
/// Whether the wallet's view is synced to the main chain
bool synced_to_chain = 9 [json_name = "synced_to_chain"];
/// Whether the current node is connected to testnet
bool testnet = 10 [json_name = "testnet"];
/**
Whether the current node is connected to testnet. This field is
deprecated and the network field should be used instead
**/
bool testnet = 10 [json_name = "testnet", deprecated = true];
/// A list of active chains the node is connected to
repeated string chains = 11 [json_name = "chains"];
reserved 11;
/// The URIs of the current node.
repeated string uris = 12 [json_name = "uris"];
@@ -1092,6 +1193,17 @@ message GetInfoResponse {
/// Number of inactive channels
uint32 num_inactive_channels = 15 [json_name = "num_inactive_channels"];
/// A list of active chains the node is connected to
repeated Chain chains = 16 [json_name = "chains"];
}
message Chain {
/// The blockchain the node is on (eg bitcoin, litecoin)
string chain = 1 [json_name = "chain"];
/// The network the node is on (eg regtest, testnet, mainnet)
string network = 2 [json_name = "network"];
}
message ConfirmationUpdate {
@@ -1132,7 +1244,6 @@ message CloseChannelRequest {
message CloseStatusUpdate {
oneof update {
PendingUpdate close_pending = 1 [json_name = "close_pending"];
ConfirmationUpdate confirmation = 2 [json_name = "confirmation"];
ChannelCloseUpdate chan_close = 3 [json_name = "chan_close"];
}
}
@@ -1179,7 +1290,6 @@ message OpenChannelRequest {
message OpenStatusUpdate {
oneof update {
PendingUpdate chan_pending = 1 [json_name = "chan_pending"];
ConfirmationUpdate confirmation = 2 [json_name = "confirmation"];
ChannelOpenUpdate chan_open = 3 [json_name = "chan_open"];
}
}
@@ -1306,6 +1416,27 @@ message PendingChannelsResponse {
repeated WaitingCloseChannel waiting_close_channels = 5 [ json_name = "waiting_close_channels" ];
}
message ChannelEventSubscription {
}
message ChannelEventUpdate {
oneof channel {
Channel open_channel = 1 [ json_name = "open_channel" ];
ChannelCloseSummary closed_channel = 2 [ json_name = "closed_channel" ];
ChannelPoint active_channel = 3 [ json_name = "active_channel" ];
ChannelPoint inactive_channel = 4 [ json_name = "inactive_channel" ];
}
enum UpdateType {
OPEN_CHANNEL = 0;
CLOSED_CHANNEL = 1;
ACTIVE_CHANNEL = 2;
INACTIVE_CHANNEL = 3;
}
UpdateType type = 5 [ json_name = "type" ];
}
message WalletBalanceRequest {
}
message WalletBalanceResponse {
@@ -1468,6 +1599,7 @@ message RoutingPolicy {
int64 fee_base_msat = 3 [json_name = "fee_base_msat"];
int64 fee_rate_milli_msat = 4 [json_name = "fee_rate_milli_msat"];
bool disabled = 5 [json_name = "disabled"];
uint64 max_htlc_msat = 6 [json_name = "max_htlc_msat"];
}
/**
@@ -1626,8 +1758,10 @@ message Invoice {
*/
string memo = 1 [json_name = "memo"];
/// An optional cryptographic receipt of payment
bytes receipt = 2 [json_name = "receipt"];
/** Deprecated. An optional cryptographic receipt of payment which is not
implemented.
*/
bytes receipt = 2 [json_name = "receipt", deprecated = true];
/**
The hex-encoded preimage (32 byte) which will allow settling an incoming
@@ -1642,7 +1776,7 @@ message Invoice {
int64 value = 5 [json_name = "value"];
/// Whether this invoice has been fulfilled
bool settled = 6 [json_name = "settled"];
bool settled = 6 [json_name = "settled", deprecated = true];
/// When this invoice was created
int64 creation_date = 7 [json_name = "creation_date"];
@@ -1720,7 +1854,19 @@ message Invoice {
here as well.
*/
int64 amt_paid_msat = 20 [json_name = "amt_paid_msat"];
enum InvoiceState {
OPEN = 0;
SETTLED = 1;
CANCELED = 2;
}
/**
The state the invoice is in.
*/
InvoiceState state = 21 [json_name = "state"];
}
message AddInvoiceResponse {
bytes r_hash = 1 [json_name = "r_hash"];
@@ -1953,15 +2099,18 @@ message ForwardingEvent {
/// The outgoing channel ID that carried the preimage that completed the circuit.
uint64 chan_id_out = 4 [json_name = "chan_id_out"];
/// The total amount of the incoming HTLC that created half the circuit.
/// The total amount (in satoshis) of the incoming HTLC that created half the circuit.
uint64 amt_in = 5 [json_name = "amt_in"];
/// The total amount of the outgoign HTLC that created the second half of the circuit.
/// The total amount (in satoshis) of the outgoing HTLC that created the second half of the circuit.
uint64 amt_out = 6 [json_name = "amt_out"];
/// The total fee that this payment circuit carried.
/// The total fee (in satoshis) that this payment circuit carried.
uint64 fee = 7 [json_name = "fee"];
/// The total fee (in milli-satoshis) that this payment circuit carried.
uint64 fee_msat = 8 [json_name = "fee_msat"];
// TODO(roasbeef): add settlement latency?
// * use FPE on the chan id?
// * also list failures?

View File

@@ -0,0 +1,121 @@
const important_channels = {
'03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f': {
name: 'ACINQ',
uri: '03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f@34.239.230.56:9735',
},
'03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e': {
name: 'OpenNode',
uri: '03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e@18.221.23.28: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,
},
'025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5': {
name: 'paywithmoon.com',
uri: '025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5@52.86.210.65:9735',
},
'0279c22ed7a068d10dc1a38ae66d2d6461e269226c60258c021b1ddcdfe4b00bc4': {
name: 'ln1.satoshilabs.com',
uri: '0279c22ed7a068d10dc1a38ae66d2d6461e269226c60258c021b1ddcdfe4b00bc4@157.230.28.160:9735',
},
'02004c625d622245606a1ea2c1c69cfb4516b703b47945a3647713c05fe4aaeb1c': {
name: 'LivingRoomOfSatoshi',
uri: '02004c625d622245606a1ea2c1c69cfb4516b703b47945a3647713c05fe4aaeb1c@172.81.178.151:9735',
},
'02816caed43171d3c9854e3b0ab2cf0c42be086ff1bd4005acc2a5f7db70d83774': {
name: 'ln.pizza aka fold',
uri: '02816caed43171d3c9854e3b0ab2cf0c42be086ff1bd4005acc2a5f7db70d83774@35.238.153.25:9735',
wumbo: 1,
},
'0331f80652fb840239df8dc99205792bba2e559a05469915804c08420230e23c7c': {
name: 'LightningPowerUsers.com',
uri: '0331f80652fb840239df8dc99205792bba2e559a05469915804c08420230e23c7c@34.200.181.109:9735',
},
'033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025': {
name: 'bfx-lnd0',
uri: '033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025@34.65.85.39:9735',
},
};
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 < 0.05 / 100000000) {
console.log(
'lncli closechannel',
channel.channel_point.replace(':', ' '),
(!channel.active && '--force') || '',
'; sleep 10 #',
'low capacity channel',
channel.capacity / 100000000,
'btc',
);
}
}
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) {
console.log(
'lncli disconnect',
channel.remote_pubkey,
'; sleep 5;',
'lncli connect',
important_channels[channel.remote_pubkey].uri,
'#',
important_channels[channel.remote_pubkey].name,
);
}
}
}
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 ? '167772150' : '16777215',
'#',
important_channels[important].name,
);
}
}
process.exit();
});

View File

@@ -0,0 +1,96 @@
/**
* 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, Lock, Paym } from '../class/';
const config = require('../config');
const fs = require('fs');
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();
console.log('done', 'got', listPayments['payments'].length, 'payments');
fs.writeFileSync('listPayments.json', JSON.stringify(listPayments['payments'], null, 2));
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();
// lockedPayments = [{pay_req : 'lnbc2m1pwgd4tdpp5vjz80mm8murdkskrnre6w4kphzy3d6gap5jyffr93u02ruaj0wtsdq2xgcrqvpsxqcqzysk34zva4h9ce9jdf08nfdm2sh2ek4y4hjse8ww9jputneltjl24krkv50sene4jh0wpull6ujgrg632u2qt3lkva74vpkqr5e5tuuljspasqfhx'}];
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);
if (daysPassed < 2) {
// if (!await payment.isExpired()) {
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 > 4) {
// 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);
process.exit();
break;
}
}
if (!isPaid) {
console.log('very old payment, evict the lock');
await user.unlockFunds(lockedPayment.pay_req);
}
}
}
}
console.log('done');
process.exit();
})();

View File

@@ -0,0 +1,35 @@
/**
* 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', JSON.stringify(listinvoices['invoices'], null, 2));
let markedInvoices = 0;
for (const invoice of listinvoices['invoices']) {
if (invoice.state === 'SETTLED' && +invoice.creation_date >= +new Date() / 1000 - 3600 * 24 * 7) {
tempInv.setInvoice(invoice.payment_request);
await tempInv.markAsPaidInDatabase();
markedInvoices++;
process.stdout.write(markedInvoices + '\r');
}
}
console.log('done, marked', markedInvoices, 'invoices');
process.exit();
})();

58
scripts/show_user.js Normal file
View File

@@ -0,0 +1,58 @@
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 * 0.01), new Date(loc.timestamp * 1000).toString(), '[locked]');
}
console.log('\ncalculatedBalance\n================\n', calculatedBalance, await U.getCalculatedBalance());
process.exit();
})();

View File

@@ -50,9 +50,10 @@
<table>
{{#channels}}
<tr>
<td>{{name}}</td>
<td><pre class="line">{{ascii}}</pre></td>
<td><pre class="line">{{capacity_btc}} BTC </pre></td>
<td><pre class="line"><a href="https://1ml.com/node/{{remote_pubkey}}" target="_blank">{{remote_pubkey}}</a> {{name}} </pre></td>
<td><pre class="line"><a href="https://1ml.com/node/{{remote_pubkey}}" target="_blank">{{remote_pubkey}}</a> {{^active}}<span class="dyer-orange">[INACTIVE]</span>{{/active}}</pre></td>
</tr>
{{/channels}}
</table>
@@ -61,9 +62,18 @@
<pre class="line"><span class="dyer-white">num_active_channels:</span></pre>
<pre class="line">{{num_active_channels}}</pre>
<pre class="line"> </pre>
<pre class="line"><span class="dyer-white">num_pending_channels:</span></pre>
<pre class="line">{{num_pending_channels}}</pre>
<pre class="line"> </pre>
<pre class="line"><span class="dyer-white">num_peers:</span></pre>
<pre class="line">{{num_peers}}</pre>
<pre class="line"> </pre>
<pre class="line"><span class="dyer-white">block_height:</span></pre>
<pre class="line">{{block_height}}</pre>
<pre class="line"> </pre>
<pre class="line"><span class="dyer-white">synced_to_chain:</span></pre>
<pre class="line">{{synced_to_chain}}</pre>
<pre class="line"> </pre>
<pre class="line"><span class="dyer-white">version:</span></pre>
<pre class="line">{{version}}</pre>
<pre class="line"> </pre>

View File

@@ -23,13 +23,10 @@ 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 +37,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,