24 Commits

Author SHA1 Message Date
Overtorment
98ba940342 REL 2019-01-07 16:21:35 +00:00
Overtorment
6851769be6 FIX: no crashes, just hangs 2019-01-06 14:36:23 +00:00
Overtorment
d3100a1390 FIX: occasional duplicate payments (closes #3) 2019-01-06 14:35:14 +00:00
Overtorment
6a57559b16 REF 2019-01-06 13:37:37 +00:00
Overtorment
4f345b20a8 FIX: potential crash 2019-01-06 13:17:52 +00:00
Overtorment
a42d331e38 FIX: potential crash 2019-01-06 12:44:41 +00:00
Overtorment
130a8dc773 FIX: potential crash 2019-01-06 12:19:08 +00:00
Overtorment
b3c0873e4c FIX: more logging, more error handling 2019-01-05 22:01:50 +00:00
Overtorment
6a3de700f4 FIX: race condition when processing onchain topup 2019-01-05 21:27:34 +00:00
Overtorment
51b0f89fd1 REF: better channels display 2019-01-05 18:10:05 +00:00
Overtorment
55ac146f1e FIX: potential crash 2019-01-05 18:09:30 +00:00
Overtorment
2904afaf26 FIX: possible crashes 2019-01-05 16:16:28 +00:00
Overtorment
2cd9a19d67 REL 2019-01-05 14:52:45 +00:00
Overtorment
765eeed9c9 ADD: pay for zero-amount (tip) invoices 2019-01-05 14:52:27 +00:00
Overtorment
bc5841d07d OPS 2019-01-03 01:28:26 +00:00
Overtorment
5b8ab18a59 OPS 2018-12-30 19:28:10 +00:00
Overtorment
7e1d4a3110 OPS 2018-12-27 00:44:28 +00:00
Overtorment
5100252ea5 FIX: internal invoice wasnt saving record in senders tx list 2018-12-27 00:44:13 +00:00
Overtorment
b8bc4dd0cd FIX: user invoice 2018-12-25 17:40:21 +00:00
Overtorment
a7294cbbbd FIX: user invoice 2018-12-25 16:16:54 +00:00
Overtorment
950900b136 FIX: user invoice 2018-12-25 15:23:18 +00:00
Overtorment
4ad56aaebe OPS 2018-12-25 13:51:38 +00:00
Overtorment
f4b35e4895 ADD: channel list in lndhub console 2018-12-25 13:51:29 +00:00
Overtorment
9e70f123a6 FIX: user invoices are correctly processed if paid 2018-12-25 13:50:09 +00:00
8 changed files with 219 additions and 29 deletions

View File

@@ -35,6 +35,7 @@ export class User {
}
async loadByAuthorization(authorization) {
if (!authorization) return false;
let access_token = authorization.replace('Bearer ', '');
let userid = await this._redis.get('userid_for_' + access_token);
@@ -186,9 +187,18 @@ export class User {
// 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);
}
}
invoice.amt = decoded.satoshis;
invoice.expire_time = 3600;
// ^^^default; will keep for now. if we want to un-hardcode it - it should be among tags (`expire_time`)
invoice.timestamp = decoded.timestamp;
invoice.type = 'user_invoice';
result.push(invoice);
}
@@ -221,10 +231,18 @@ export class User {
for (let invoice of range) {
invoice = JSON.parse(invoice);
invoice.type = 'paid_invoice';
invoice.fee = +invoice.payment_route.total_fees;
invoice.value = +invoice.payment_route.total_fees + +invoice.payment_route.total_amt;
invoice.timestamp = invoice.decoded.timestamp;
invoice.memo = invoice.decoded.description;
// for internal invoices it might not have properties `payment_route` and `decoded`...
if (invoice.payment_route) {
invoice.fee = +invoice.payment_route.total_fees;
invoice.value = +invoice.payment_route.total_fees + +invoice.payment_route.total_amt;
} else {
invoice.fee = 0;
}
if (invoice.decoded) {
invoice.timestamp = invoice.decoded.timestamp;
invoice.memo = invoice.decoded.description;
}
result.push(invoice);
}
@@ -276,8 +294,8 @@ export class User {
* @returns {Promise<void>}
*/
async accountForPosibleTxids() {
let imported_txids = await this._redis.lrange('imported_txids_for_' + this._userid, 0, -1);
let onchain_txs = await this.getTxs();
let imported_txids = await this._redis.lrange('imported_txids_for_' + this._userid, 0, -1);
for (let tx of onchain_txs) {
if (tx.type !== 'bitcoind_tx') continue;
let already_imported = false;
@@ -286,6 +304,16 @@ export class User {
}
if (!already_imported && tx.category === 'receive') {
let locked = await this._redis.get('importing_' + tx.txid);
if (locked) {
// race condition, someone's already importing this tx
return;
}
// locking...
await this._redis.set('importing_' + tx.txid, 1);
await this._redis.expire('importing_' + tx.txid, 3600);
let userBalance = await this.getBalance();
userBalance += new BigNumber(tx.amount).multipliedBy(100000000).toNumber();
await this.saveBalance(userBalance);

View File

@@ -2,6 +2,7 @@ import { User } from '../class/User';
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');
@@ -53,6 +54,7 @@ redis.info(function(err, info) {
// ######################## ROUTES ########################
router.post('/create', 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);
@@ -62,6 +64,7 @@ router.post('/create', async function(req, res) {
});
router.post('/auth', 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);
@@ -82,6 +85,7 @@ router.post('/auth', async function(req, res) {
});
router.post('/addinvoice', async function(req, res) {
logger.log('/addinvoice', [req.id]);
let u = new User(redis);
if (!(await u.loadByAuthorization(req.headers.authorization))) {
return errorBadAuth(res);
@@ -100,18 +104,27 @@ router.post('/addinvoice', async function(req, res) {
});
router.post('/payinvoice', async function(req, res) {
logger.log('/payinvoice', [req.id]);
let u = new User(redis);
if (!(await u.loadByAuthorization(req.headers.authorization))) {
return errorBadAuth(res);
}
if (!req.body.invoice) return errorBadArguments(res);
let freeAmount = false;
if (req.body.amount) freeAmount = parseInt(req.body.amount);
const lock_key = 'invoice_paying_for_' + u.getUserId();
let userBalance = await u.getBalance();
lightning.decodePayReq({ pay_req: req.body.invoice }, async function(err, info) {
if (err) return errorNotAValidInvoice(res);
if (+info.num_satoshis === 0) {
// 'tip' invoices
info.num_satoshis = freeAmount;
}
if (userBalance >= info.num_satoshis) {
// got enough balance
@@ -121,6 +134,12 @@ router.post('/payinvoice', async function(req, res) {
let userid_payee = await u.getUseridByPaymentHash(info.payment_hash);
if (!userid_payee) return errorGeneralServerError(res);
if (await redis.get(lock_key)) {
return errorTryAgainLater(res);
}
await redis.set(lock_key, 1);
await redis.expire(lock_key, 2 * 60);
let UserPayee = new User(redis);
UserPayee._userid = userid_payee; // hacky, fixme
let payee_balance = await UserPayee.getBalance();
@@ -130,9 +149,17 @@ router.post('/payinvoice', async function(req, res) {
// sender spent his balance:
userBalance -= info.num_satoshis * 1;
await u.saveBalance(userBalance);
await u.savePaidLndInvoice({
timestamp: parseInt(+new Date() / 1000),
type: 'paid_invoice',
value: info.num_satoshis * 1,
fee: 0, // internal invoices are free
memo: decodeURIComponent(info.description),
});
await u.setPaymentHashPaid(info.payment_hash);
await UserPayee.setPaymentHashPaid(info.payment_hash);
await redis.del(lock_key, 1);
return res.send(info);
}
@@ -145,14 +172,31 @@ router.post('/payinvoice', async function(req, res) {
payment.pay_req = req.body.invoice;
payment.decoded = info;
u.savePaidLndInvoice(payment);
redis.del(lock_key);
res.send(payment);
} else {
// payment failed
redis.del(lock_key);
return errorLnd(res);
}
});
let inv = { payment_request: req.body.invoice };
call.write(inv);
if (!info.num_satoshis && !info.num_satoshis) {
// tip invoice, but someone forgot to specify amount
return errorBadArguments(res);
}
let inv = { payment_request: req.body.invoice, amt: info.num_satoshis }; // amt is used only for 'tip' invoices
try {
if (await redis.get(lock_key)) {
return errorTryAgainLater(res);
}
await redis.set(lock_key, 1);
await redis.expire(lock_key, 2 * 60);
logger.log('/payinvoice', [req.id, 'before write', JSON.stringify(inv)]);
call.write(inv);
} catch (Err) {
logger.log('/payinvoice', [req.id, 'exception', JSON.stringify(Err)]);
return errorLnd(res);
}
} else {
return errorNotEnougBalance(res);
}
@@ -160,6 +204,7 @@ router.post('/payinvoice', async function(req, res) {
});
router.get('/getbtc', async function(req, res) {
logger.log('/getbtc', [req.id]);
let u = new User(redis, bitcoinclient, lightning);
await u.loadByAuthorization(req.headers.authorization);
@@ -177,6 +222,7 @@ router.get('/getbtc', async function(req, res) {
});
router.get('/balance', async function(req, res) {
logger.log('/balance', [req.id]);
let u = new User(redis, bitcoinclient, lightning);
if (!(await u.loadByAuthorization(req.headers.authorization))) {
return errorBadAuth(res);
@@ -189,6 +235,7 @@ router.get('/balance', async function(req, res) {
});
router.get('/getinfo', async function(req, res) {
logger.log('/getinfo', [req.id]);
let u = new User(redis);
if (!(await u.loadByAuthorization(req.headers.authorization))) {
return errorBadAuth(res);
@@ -201,28 +248,41 @@ 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);
}
if (!(await u.getAddress())) await u.generateAddress(); // onchain addr needed further
await u.accountForPosibleTxids();
let txs = await u.getTxs();
res.send(txs);
try {
await u.accountForPosibleTxids();
let txs = await u.getTxs();
res.send(txs);
} catch (Err) {
console.log(Err);
res.send([]);
}
});
router.get('/getuserinvoices', 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);
}
let invoices = await u.getUserInvoices();
res.send(invoices);
try {
let invoices = await u.getUserInvoices();
res.send(invoices);
} catch (Err) {
console.log(Err);
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);
@@ -235,6 +295,7 @@ router.get('/getpending', async function(req, res) {
});
router.get('/decodeinvoice', async function(req, res) {
logger.log('/decodeinvoice', [req.id]);
let u = new User(redis, bitcoinclient);
if (!(await u.loadByAuthorization(req.headers.authorization))) {
return errorBadAuth(res);
@@ -249,6 +310,7 @@ router.get('/decodeinvoice', async function(req, res) {
});
router.get('/checkrouteinvoice', async function(req, res) {
logger.log('/checkrouteinvoice', [req.id]);
let u = new User(redis, bitcoinclient);
if (!(await u.loadByAuthorization(req.headers.authorization))) {
return errorBadAuth(res);
@@ -315,3 +377,11 @@ function errorBadArguments(res) {
message: 'Bad arguments',
});
}
function errorTryAgainLater(res) {
return res.send({
error: true,
code: 9,
message: 'Try again later',
});
}

View File

@@ -3,20 +3,78 @@ 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() {
console.log('updateLightning()');
try {
lightning.getInfo({}, function(err, info) {
if (err) {
console.error('lnd failure');
}
lightningGetInfo = info;
});
lightning.listChannels({}, function(err, response) {
if (err) {
console.error('lnd failure');
}
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];
if (channel.name) {
channels.unshift(channel);
} else {
channels.push(channel);
}
}
lightningListChannels.channels = channels;
});
} catch (Err) {
console.log(Err);
}
}
updateLightning();
setInterval(updateLightning, 60000);
const pubkey2name = {
'03e50492eab4107a773141bb419e107bda3de3d55652e6e1a41225f06a0bbf2d56': 'yalls.org',
'0232e20e7b68b9b673fb25f48322b151a93186bffe4550045040673797ceca43cf': 'zigzag.io',
'02df5ffe895c778e10f7742a6c5b8a0cefbe9465df58b92fadeb883752c8107c8f': 'blockstream store',
'030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f': 'bitrefill.com',
'03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f': 'ACINQ',
'03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e': 'OpenNode',
'0242a4ae0c5bef18048fbecf995094b74bfb0f7391418d71ed394784373f41e4f3': 'coingate.com',
'0279c22ed7a068d10dc1a38ae66d2d6461e269226c60258c021b1ddcdfe4b00bc4': 'ln1.satoshilabs.com',
'02c91d6aa51aa940608b497b6beebcb1aec05be3c47704b682b3889424679ca490': 'lnd-21.LNBIG.com',
'024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca': 'satoshis.place',
'03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda': 'tippin.me',
};
router.get('/', function(req, res) {
logger.log('/', [req.id]);
if (!lightningGetInfo) {
console.error('lnd failure');
process.exit(3);
}
res.setHeader('Content-Type', 'text/html');
let html = fs.readFileSync('./templates/index.html').toString('utf8');
lightning.getInfo({}, function(err, info) {
if (err) {
console.error('lnd failure');
process.exit(3);
}
res.setHeader('Content-Type', 'text/html');
return res.status(200).send(mustache.render(html, info));
});
return res.status(200).send(mustache.render(html, Object.assign({}, lightningGetInfo, lightningListChannels)));
});
router.get('/about', function(req, res) {
logger.log('/about', [req.id]);
let html = fs.readFileSync('./templates/about.html').toString('utf8');
res.setHeader('Content-Type', 'text/html');
return res.status(200).send(mustache.render(html, {}));

View File

@@ -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`

View File

@@ -1,3 +1,8 @@
process.on('uncaughtException', function(err) {
console.error(err);
console.log('Node NOT Exiting...');
});
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
let express = require('express');
let morgan = require('morgan');

28
package-lock.json generated
View File

@@ -2368,11 +2368,13 @@
},
"balanced-match": {
"version": "1.0.0",
"bundled": true
"bundled": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -2385,15 +2387,18 @@
},
"code-point-at": {
"version": "1.1.0",
"bundled": true
"bundled": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true
"bundled": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true
"bundled": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@@ -2496,7 +2501,8 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true
"bundled": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@@ -2506,6 +2512,7 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -2518,17 +2525,20 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true
"bundled": true,
"optional": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@@ -2545,6 +2555,7 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -2617,7 +2628,8 @@
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true
"bundled": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@@ -2627,6 +2639,7 @@
"once": {
"version": "1.4.0",
"bundled": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@@ -2732,6 +2745,7 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "LndHub",
"version": "1.0.0",
"version": "1.1.1",
"description": "",
"main": "index.js",
"scripts": {

View File

@@ -44,6 +44,19 @@
{{#uris}}
<pre class="line">{{.}}</pre>
{{/uris}}
<pre class="line"> </pre>
<pre class="line"><span class="dyer-white">Channels:</span></pre>
<table>
{{#channels}}
<tr>
<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}} {{^active}}<span class="dyer-orange">[INACTIVE]</span>{{/active}} </pre></td>
</tr>
{{/channels}}
</table>
<pre class="line"> </pre>
<pre class="line"><span class="dyer-white">num_active_channels:</span></pre>
<pre class="line">{{num_active_channels}}</pre>