Compare commits
146 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
21
LICENSE
Normal file
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.
|
||||
30
README.md
30
README.md
@@ -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
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## Responsible disclosure
|
||||
|
||||
Found critical bugs/vulnerabilities? Please email them bluewallet@bluewallet.io
|
||||
Thanks!
|
||||
|
||||
@@ -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
86
class/Invo.js
Normal 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
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
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;
|
||||
}
|
||||
|
||||
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'];
|
||||
}
|
||||
}
|
||||
271
class/User.js
271
class/User.js
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
export * from './User';
|
||||
export * from './Lock';
|
||||
export * from './Paym';
|
||||
export * from './Invo';
|
||||
|
||||
@@ -11,6 +11,7 @@ let config = {
|
||||
},
|
||||
lnd: {
|
||||
url: '1.1.1.1:10009',
|
||||
password: '',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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?',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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, {}));
|
||||
|
||||
@@ -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
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,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} = []
|
||||
|
||||
15
index.js
15
index.js
@@ -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');
|
||||
|
||||
|
||||
21
lightning.js
21
lightning.js
@@ -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
2185
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -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
197
rpc.proto
@@ -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?
|
||||
|
||||
121
scripts/important-channels.js
Normal file
121
scripts/important-channels.js
Normal 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();
|
||||
});
|
||||
96
scripts/process-locked-payments.js
Normal file
96
scripts/process-locked-payments.js
Normal 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();
|
||||
})();
|
||||
35
scripts/process-unpaid-invoices.js
Normal file
35
scripts/process-unpaid-invoices.js
Normal 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
58
scripts/show_user.js
Normal 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();
|
||||
})();
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user