From 9c47746ebab19fc1d52adc065464ca7c911737af Mon Sep 17 00:00:00 2001 From: Overtorment Date: Mon, 21 Jan 2019 13:17:04 +0000 Subject: [PATCH 01/28] FIX: redis balance is only a cashe; tx list is a source of truth for user's balance --- class/User.js | 35 +++++++++++++++++++++++++++++++++-- controllers/api.js | 16 ++++++++-------- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/class/User.js b/class/User.js index fcb9912..88de2ba 100644 --- a/class/User.js +++ b/class/User.js @@ -114,11 +114,42 @@ export class User { } async getBalance() { - return (await this._redis.get('balance_for_' + this._userid)) * 1; + let balance = (await this._redis.get('balance_for_' + this._userid)) * 1; + console.log('balance from db ', balance); + if (!balance) { + balance = await this.getCalculatedBalance(); + console.log('calculated balance', balance); + await this.saveBalance(balance); + } + return balance; + } + + 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 += tx.amount * 100000000; + } else { + calculatedBalance -= +tx.value; + } + } + return calculatedBalance; } 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, 3600 * 24); } async savePaidLndInvoice(doc) { diff --git a/controllers/api.js b/controllers/api.js index 70f5215..3018f78 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -57,7 +57,7 @@ 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); + 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() }); @@ -67,7 +67,7 @@ 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); + let u = new User(redis, bitcoinclient, lightning); if (req.body.refresh_token) { // need to refresh token @@ -86,7 +86,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); + let u = new User(redis, bitcoinclient, lightning); if (!(await u.loadByAuthorization(req.headers.authorization))) { return errorBadAuth(res); } @@ -105,7 +105,7 @@ router.post('/addinvoice', async function(req, res) { router.post('/payinvoice', async function(req, res) { logger.log('/payinvoice', [req.id]); - let u = new User(redis); + let u = new User(redis, bitcoinclient, lightning); if (!(await u.loadByAuthorization(req.headers.authorization))) { return errorBadAuth(res); } @@ -151,7 +151,7 @@ router.post('/payinvoice', async function(req, res) { return errorLnd(res); } - let UserPayee = new User(redis); + 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; @@ -247,7 +247,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); + let u = new User(redis, bitcoinclient, lightning); if (!(await u.loadByAuthorization(req.headers.authorization))) { return errorBadAuth(res); } @@ -311,7 +311,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); + let u = new User(redis, bitcoinclient, lightning); if (!(await u.loadByAuthorization(req.headers.authorization))) { return errorBadAuth(res); } @@ -326,7 +326,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); + let u = new User(redis, bitcoinclient, lightning); if (!(await u.loadByAuthorization(req.headers.authorization))) { return errorBadAuth(res); } From ff2d98dc100459ee4b3137cba5fc0a7021b8d59e Mon Sep 17 00:00:00 2001 From: Overtorment Date: Mon, 21 Jan 2019 13:17:23 +0000 Subject: [PATCH 02/28] OPS: turn off explicit logging --- controllers/api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controllers/api.js b/controllers/api.js index 3018f78..f17b186 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -9,7 +9,7 @@ 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)); }); }); From f2386f0acf1dc50740af1513bd1f42830117a894 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Mon, 21 Jan 2019 14:18:09 +0000 Subject: [PATCH 03/28] REF: retry --- class/User.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/class/User.js b/class/User.js index 88de2ba..dfd364d 100644 --- a/class/User.js +++ b/class/User.js @@ -249,6 +249,10 @@ export class User { */ 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]); txs = txs.result; @@ -289,6 +293,10 @@ 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]); txs = txs.result; From e854626a457894142d4994740131e1b9763e9263 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Mon, 21 Jan 2019 19:49:19 +0000 Subject: [PATCH 04/28] FIX: more strict balance check (re #8) --- class/User.js | 2 +- controllers/api.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/class/User.js b/class/User.js index dfd364d..732f25c 100644 --- a/class/User.js +++ b/class/User.js @@ -352,7 +352,7 @@ export class User { return; } - let userBalance = await this.getBalance(); + let userBalance = await this.getCalculatedBalance(); userBalance += new BigNumber(tx.amount).multipliedBy(100000000).toNumber(); await this.saveBalance(userBalance); await this._redis.rpush('imported_txids_for_' + this._userid, tx.txid); diff --git a/controllers/api.js b/controllers/api.js index f17b186..f47fe1c 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -120,7 +120,7 @@ router.post('/payinvoice', async function(req, res) { return errorTryAgainLater(res); } - let userBalance = await u.getBalance(); + let userBalance = await u.getCalculatedBalance(); lightning.decodePayReq({ pay_req: req.body.invoice }, async function(err, info) { if (err) { From c1e13e9c8cbfbab103c93e27640c4d9754074051 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Mon, 21 Jan 2019 20:18:04 +0000 Subject: [PATCH 05/28] FIX: (re #8) --- class/User.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/class/User.js b/class/User.js index 732f25c..62cb2b0 100644 --- a/class/User.js +++ b/class/User.js @@ -138,7 +138,7 @@ export class User { for (let tx of txs) { if (tx.type === 'bitcoind_tx') { // topup - calculatedBalance += tx.amount * 100000000; + calculatedBalance += new BigNumber(tx.amount).multipliedBy(100000000).toNumber(); } else { calculatedBalance -= +tx.value; } @@ -353,7 +353,8 @@ export class User { } let userBalance = await this.getCalculatedBalance(); - userBalance += new BigNumber(tx.amount).multipliedBy(100000000).toNumber(); + // 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(); From b85097f7cc5becf74206806ec5a64e4ff5a89f3a Mon Sep 17 00:00:00 2001 From: Overtorment Date: Wed, 23 Jan 2019 11:55:36 +0000 Subject: [PATCH 06/28] OPS: dep --- package-lock.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index f98aba5..6d1fbb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "LndHub", - "version": "1.0.0", + "version": "1.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1196,6 +1196,7 @@ "bech32": "^1.1.2", "bitcoinjs-lib": "^3.3.1", "bn.js": "^4.11.8", + "coininfo": "git+https://github.com/cryptocoinjs/coininfo.git#c7e003b2fc0db165b89e6f98f6d6360ad22616b2", "lodash": "^4.17.4", "safe-buffer": "^5.1.1", "secp256k1": "^3.4.0" @@ -1486,6 +1487,13 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, + "coininfo": { + "version": "git+https://github.com/cryptocoinjs/coininfo.git#c7e003b2fc0db165b89e6f98f6d6360ad22616b2", + "from": "git+https://github.com/cryptocoinjs/coininfo.git#c7e003b2fc0db165b89e6f98f6d6360ad22616b2", + "requires": { + "safe-buffer": "^5.1.1" + } + }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", From dec313aa4cd05a0becee723ba84ec81ceb84b26a Mon Sep 17 00:00:00 2001 From: Overtorment Date: Wed, 23 Jan 2019 12:12:16 +0000 Subject: [PATCH 07/28] FIX: incorrect balance after invoice paid --- class/User.js | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/class/User.js b/class/User.js index 62cb2b0..f5bdedd 100644 --- a/class/User.js +++ b/class/User.js @@ -113,17 +113,27 @@ 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} Balance available to spend + */ async getBalance() { let balance = (await this._redis.get('balance_for_' + this._userid)) * 1; - console.log('balance from db ', balance); if (!balance) { balance = await this.getCalculatedBalance(); - console.log('calculated balance', balance); await this.saveBalance(balance); } return balance; } + /** + * Accounts for all possible transactions in user's account and + * sums their amounts. + * + * @returns {Promise} Balance available to spend + */ async getCalculatedBalance() { let calculatedBalance = 0; let userinvoices = await this.getUserInvoices(); @@ -146,6 +156,13 @@ export class User { 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} + */ async saveBalance(balance) { const key = 'balance_for_' + this._userid; await this._redis.set(key, balance); @@ -223,7 +240,7 @@ export class User { if (invoice.ispaid) { // so invoice was paid after all await this.setPaymentHashPaid(invoice.payment_hash); - await this.saveBalance((await this.getBalance()) + decoded.satoshis); + await this.saveBalance(await this.getCalculatedBalance()); } } From 0f75933bf06eb5d02035424350fff7e39ba1f473 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Thu, 24 Jan 2019 20:14:27 +0000 Subject: [PATCH 08/28] FIX: no crash --- controllers/website.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/controllers/website.js b/controllers/website.js index c0eb1f7..48682ff 100644 --- a/controllers/website.js +++ b/controllers/website.js @@ -12,14 +12,15 @@ function updateLightning() { try { lightning.getInfo({}, function(err, info) { if (err) { - console.error('lnd failure'); + console.error('lnd failure:', err); } lightningGetInfo = info; }); lightning.listChannels({}, function(err, response) { if (err) { - console.error('lnd failure'); + console.error('lnd failure:', err); + return; } lightningListChannels = response; let channels = []; From 1df24d0e80ffa134ba8bda08e88f8ff2d7cc9e94 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Fri, 25 Jan 2019 22:59:32 +0000 Subject: [PATCH 09/28] ADD: lnd wallet unlock attempt upon start --- config.js | 1 + controllers/website.js | 1 + lightning.js | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/config.js b/config.js index 22585cb..1cc68ec 100644 --- a/config.js +++ b/config.js @@ -11,6 +11,7 @@ let config = { }, lnd: { url: '1.1.1.1:10009', + password: '', }, }; diff --git a/controllers/website.js b/controllers/website.js index 48682ff..fe14531 100644 --- a/controllers/website.js +++ b/controllers/website.js @@ -45,6 +45,7 @@ function updateLightning() { } catch (Err) { console.log(Err); } + console.log('updated'); } updateLightning(); setInterval(updateLightning, 60000); diff --git a/lightning.js b/lightning.js index ff02047..676b33d 100644 --- a/lightning.js +++ b/lightning.js @@ -25,4 +25,22 @@ let macaroonCreds = grpc.credentials.createFromMetadataGenerator(function(args, callback(null, metadata); }); let creds = grpc.credentials.combineChannelCredentials(sslCreds, macaroonCreds); + +// trying to unlock the wallet: +if (config.lnd.password) { + var walletUnlocker = new lnrpc.WalletUnlocker(config.lnd.url, creds); + walletUnlocker.unlockWallet( + { + wallet_password: config.lnd.password, + }, + 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); From da37b5315db85df2111dbf345d0c7bc6ca4b4304 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Fri, 25 Jan 2019 23:44:07 +0000 Subject: [PATCH 10/28] REF: better logging --- controllers/api.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/controllers/api.js b/controllers/api.js index f47fe1c..dba719d 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -200,10 +200,8 @@ router.post('/payinvoice', async function(req, res) { } let inv = { payment_request: req.body.invoice, amt: info.num_satoshis }; // amt is used only for 'tip' invoices try { - logger.log('/payinvoice', [req.id, 'before write', JSON.stringify(inv)]); call.write(inv); } catch (Err) { - logger.log('/payinvoice', [req.id, 'exception', JSON.stringify(Err)]); await lock.releaseLock(); return errorLnd(res); } @@ -271,7 +269,7 @@ router.get('/gettxs', async function(req, res) { let txs = await u.getTxs(); res.send(txs); } catch (Err) { - console.log(Err); + logger.log('', [req.id, 'error:', Err]); res.send([]); } }); @@ -291,7 +289,7 @@ router.get('/getuserinvoices', async function(req, res) { res.send(invoices); } } catch (Err) { - console.log(Err); + logger.log('', [req.id, 'error:', Err]); res.send([]); } }); From d6bb2bbe25a8f8aad14017d6f649d133afdabee3 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Sat, 26 Jan 2019 00:15:29 +0000 Subject: [PATCH 11/28] REF --- lightning.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lightning.js b/lightning.js index 676b33d..ba9f929 100644 --- a/lightning.js +++ b/lightning.js @@ -28,6 +28,7 @@ let creds = grpc.credentials.combineChannelCredentials(sslCreds, macaroonCreds); // 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( { From e59ea298d1602e8623a5693ff1505a14ef0c5fa0 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Mon, 28 Jan 2019 21:39:34 +0000 Subject: [PATCH 12/28] REF: removing unsued by client fields to reduce size --- class/User.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/class/User.js b/class/User.js index f5bdedd..7042b66 100644 --- a/class/User.js +++ b/class/User.js @@ -297,6 +297,12 @@ export class User { invoice.timestamp = invoice.decoded.timestamp; invoice.memo = invoice.decoded.description; } + // removing unsued by client fields to reduce size + delete invoice.payment_error; + delete invoice.payment_preimage; + delete invoice.payment_route; + delete invoice.pay_req; + delete invoice.decoded; result.push(invoice); } From d04b155e118403eaf3d8cb6cf61a25af709a4f90 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Fri, 1 Feb 2019 19:06:47 +0000 Subject: [PATCH 13/28] FIX: can pay negative free amount invoices --- controllers/api.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/controllers/api.js b/controllers/api.js index dba719d..189d70c 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -112,7 +112,10 @@ router.post('/payinvoice', async function(req, res) { 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); + } // obtaining a lock let lock = new Lock(redis, 'invoice_paying_for_' + u.getUserId()); From dd288d413faecec5309a6ba3ca717c5434e84e7c Mon Sep 17 00:00:00 2001 From: Igor Korsakov Date: Fri, 1 Feb 2019 19:28:26 +0000 Subject: [PATCH 14/28] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7705bb8..eb2d705 100644 --- a/README.md +++ b/README.md @@ -27,4 +27,9 @@ Add config vars : ### Tests -Acceptance tests are in https://github.com/BlueWallet/BlueWallet/blob/master/LightningCustodianWallet.test.js \ No newline at end of file +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! From 6d2abdfcceceaa526d814b8fb54b70312524a829 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Sat, 2 Feb 2019 23:22:33 +0000 Subject: [PATCH 15/28] OPS: logging --- controllers/api.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/controllers/api.js b/controllers/api.js index 189d70c..53e6f66 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -104,12 +104,13 @@ router.post('/addinvoice', async function(req, res) { }); router.post('/payinvoice', async function(req, res) { - logger.log('/payinvoice', [req.id]); 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()]); + if (!req.body.invoice) return errorBadArguments(res); let freeAmount = false; if (req.body.amount) { From 2f5f300a90f27114a76bbfb33b7343aec08d5a26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Rodriguez=20V=C3=A9lez?= Date: Sun, 3 Feb 2019 16:22:20 -0500 Subject: [PATCH 16/28] ADD: Added '1.ln.aantonop.com', --- controllers/website.js | 1 + 1 file changed, 1 insertion(+) diff --git a/controllers/website.js b/controllers/website.js index fe14531..b7893a4 100644 --- a/controllers/website.js +++ b/controllers/website.js @@ -63,6 +63,7 @@ const pubkey2name = { '024655b768ef40951b20053a5c4b951606d4d86085d51238f2c67c7dec29c792ca': 'satoshis.place', '03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda': 'tippin.me', '022c699df736064b51a33017abfc4d577d133f7124ac117d3d9f9633b6297a3b6a': 'globee.com', + '0237fefbe8626bf888de0cad8c73630e32746a22a2c4faa91c1d9877a3826e1174': '1.ln.aantonop.com', }; router.get('/', function(req, res) { From efd31067ba99b44a0ff0b966ecaf7a3e203c698a Mon Sep 17 00:00:00 2001 From: Overtorment Date: Wed, 6 Feb 2019 20:50:58 +0000 Subject: [PATCH 17/28] REF: default lock time 2 min -> 5 min --- class/Lock.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/class/Lock.js b/class/Lock.js index f95c53a..e696624 100644 --- a/class/Lock.js +++ b/class/Lock.js @@ -24,8 +24,8 @@ export class Lock { } // success - got lock - await this._redis.expire(this._lock_key, 2 * 60); - // lock expires in 2 mins just for any case + await this._redis.expire(this._lock_key, 5 * 60); + // lock expires in 5 mins just for any case return true; } From fe6cf0e86049342f3158d818f353ab9c2d4d4b4a Mon Sep 17 00:00:00 2001 From: Igor Korsakov Date: Thu, 7 Feb 2019 14:46:00 +0000 Subject: [PATCH 18/28] Update README.md --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index eb2d705..6059cbf 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,13 @@ 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 : @@ -29,6 +31,10 @@ Add config vars : Acceptance tests are in https://github.com/BlueWallet/BlueWallet/blob/master/LightningCustodianWallet.test.js +![image](https://user-images.githubusercontent.com/1913337/52418916-f30beb00-2ae6-11e9-9d63-17189dc1ae8c.png) + + + ## Responsible disclosure Found critical bugs/vulnerabilities? Please email them bluewallet@bluewallet.io From daa25d1b75fabeafe56a0505c9b2b6a05a71d2c6 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Sat, 16 Feb 2019 12:58:34 +0000 Subject: [PATCH 19/28] OPS --- controllers/website.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/controllers/website.js b/controllers/website.js index b7893a4..cc437d0 100644 --- a/controllers/website.js +++ b/controllers/website.js @@ -64,6 +64,9 @@ const pubkey2name = { '03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda': 'tippin.me', '022c699df736064b51a33017abfc4d577d133f7124ac117d3d9f9633b6297a3b6a': 'globee.com', '0237fefbe8626bf888de0cad8c73630e32746a22a2c4faa91c1d9877a3826e1174': '1.ln.aantonop.com', + '036a54f02d2186de192e4bcec3f7b47adb43b1fa965793387cd2471990ce1d236b': 'capacity.network', + '026c7d28784791a4b31a64eb34d9ab01552055b795919165e6ae886de637632efb': 'LivingRoomOfSatoshi.com_LND_1', + '02816caed43171d3c9854e3b0ab2cf0c42be086ff1bd4005acc2a5f7db70d83774': 'ln.pizza', }; router.get('/', function(req, res) { From a7cfbca1e75c1cc4020c3957d6913bae5f8b1858 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Sat, 16 Feb 2019 13:07:08 +0000 Subject: [PATCH 20/28] DOC --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index eb2d705..ab43de8 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,11 @@ 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 From 1a740d6b06e1554bbb52e27d38842938c1c813d4 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Thu, 28 Feb 2019 22:23:21 +0000 Subject: [PATCH 21/28] REF --- class/Lock.js | 2 +- class/User.js | 1 + controllers/api.js | 8 +++++--- controllers/website.js | 1 + 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/class/Lock.js b/class/Lock.js index e696624..db0209c 100644 --- a/class/Lock.js +++ b/class/Lock.js @@ -24,7 +24,7 @@ export class Lock { } // success - got lock - await this._redis.expire(this._lock_key, 5 * 60); + await this._redis.expire(this._lock_key, 10 * 60); // lock expires in 5 mins just for any case return true; } diff --git a/class/User.js b/class/User.js index 7042b66..7eb1c3a 100644 --- a/class/User.js +++ b/class/User.js @@ -261,6 +261,7 @@ export class User { /** * User's onchain txs that are >= 3 confs + * Queries bitcoind RPC. * * @returns {Promise} */ diff --git a/controllers/api.js b/controllers/api.js index 53e6f66..c8e6753 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -109,7 +109,7 @@ router.post('/payinvoice', async function(req, res) { return errorBadAuth(res); } - logger.log('/payinvoice', [req.id, 'userid: ' + u.getUserId()]); + logger.log('/payinvoice', [req.id, 'userid: ' + u.getUserId(), 'invoice: ' + req.body.invoice]); if (!req.body.invoice) return errorBadArguments(res); let freeAmount = false; @@ -137,6 +137,8 @@ router.post('/payinvoice', async function(req, res) { info.num_satoshis = freeAmount; } + logger.log('/payinvoice', [req.id, 'userBalance: ' + userBalance, 'num_satoshis: ' + info.num_satoshis]); + if (userBalance >= info.num_satoshis) { // got enough balance @@ -181,14 +183,14 @@ router.post('/payinvoice', async function(req, res) { // else - regular lightning network payment: var call = lightning.sendPayment(); - call.on('data', function(payment) { + call.on('data', async function(payment) { // payment callback 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); payment.pay_req = req.body.invoice; payment.decoded = info; - u.savePaidLndInvoice(payment); + await u.savePaidLndInvoice(payment); lock.releaseLock(); res.send(payment); } else { diff --git a/controllers/website.js b/controllers/website.js index cc437d0..4363df2 100644 --- a/controllers/website.js +++ b/controllers/website.js @@ -67,6 +67,7 @@ const pubkey2name = { '036a54f02d2186de192e4bcec3f7b47adb43b1fa965793387cd2471990ce1d236b': 'capacity.network', '026c7d28784791a4b31a64eb34d9ab01552055b795919165e6ae886de637632efb': 'LivingRoomOfSatoshi.com_LND_1', '02816caed43171d3c9854e3b0ab2cf0c42be086ff1bd4005acc2a5f7db70d83774': 'ln.pizza', + '024a2e265cd66066b78a788ae615acdc84b5b0dec9efac36d7ac87513015eaf6ed': 'Bitrefill.com/lightning', }; router.get('/', function(req, res) { From 17cd8adf41061febc62fe11322de537aef27c4b6 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Thu, 28 Feb 2019 23:07:31 +0000 Subject: [PATCH 22/28] REF --- controllers/api.js | 2 +- utils/logger.js | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/controllers/api.js b/controllers/api.js index c8e6753..4f0a4ff 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -401,6 +401,6 @@ function errorTryAgainLater(res) { return res.send({ error: true, code: 9, - message: 'Try again later', + message: 'Your previous payment is in transit. Try again in 5 minutes', }); } diff --git a/utils/logger.js b/utils/logger.js index 72e54bb..eb7364d 100644 --- a/utils/logger.js +++ b/utils/logger.js @@ -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(), ], }); @@ -45,7 +42,6 @@ if (!fs.existsSync('logs')) { * @param {string} message log message */ function log(label, message) { - console.log(new Date(), label, message); logger.log({ level: 'info', label: label, From 3cbe38ed28eae2bf666a5436f628f58c187bc573 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Fri, 1 Mar 2019 22:41:50 +0000 Subject: [PATCH 23/28] FIX: increase lock --- class/Lock.js | 2 +- class/Paym.js | 20 ++++++++++++++++++++ class/User.js | 1 + 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 class/Paym.js diff --git a/class/Lock.js b/class/Lock.js index db0209c..6c6412e 100644 --- a/class/Lock.js +++ b/class/Lock.js @@ -24,7 +24,7 @@ export class Lock { } // success - got lock - await this._redis.expire(this._lock_key, 10 * 60); + await this._redis.expire(this._lock_key, 3600); // lock expires in 5 mins just for any case return true; } diff --git a/class/Paym.js b/class/Paym.js new file mode 100644 index 0000000..d043bfb --- /dev/null +++ b/class/Paym.js @@ -0,0 +1,20 @@ +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; + } + + async decodePayReq(invoice) { + return new Promise(function(resolve, reject) { + this._lightning.decodePayReq({ pay_req: invoice }, function(err, info) { + if (err) return reject(err); + return resolve(info); + }); + }); + } +} diff --git a/class/User.js b/class/User.js index 7eb1c3a..629c9d3 100644 --- a/class/User.js +++ b/class/User.js @@ -285,6 +285,7 @@ export class User { let range = await this._redis.lrange('txs_for_' + this._userid, 0, -1); for (let invoice of range) { invoice = JSON.parse(invoice); + // console.log(invoice);process.exit(); invoice.type = 'paid_invoice'; // for internal invoices it might not have properties `payment_route` and `decoded`... From afb07aa9ba1ceadc4e6bed3021ea769fddfbce29 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Fri, 1 Mar 2019 22:47:43 +0000 Subject: [PATCH 24/28] REF: remove debug --- class/User.js | 1 - 1 file changed, 1 deletion(-) diff --git a/class/User.js b/class/User.js index 629c9d3..7eb1c3a 100644 --- a/class/User.js +++ b/class/User.js @@ -285,7 +285,6 @@ export class User { let range = await this._redis.lrange('txs_for_' + this._userid, 0, -1); for (let invoice of range) { invoice = JSON.parse(invoice); - // console.log(invoice);process.exit(); invoice.type = 'paid_invoice'; // for internal invoices it might not have properties `payment_route` and `decoded`... From 8f2d98774b8ac4ddc9751a8e6fbda212b7bd8a8f Mon Sep 17 00:00:00 2001 From: Overtorment Date: Sun, 3 Mar 2019 15:59:30 +0000 Subject: [PATCH 25/28] FIX: properly locked payments --- class/Paym.js | 9 +++++-- class/User.js | 60 ++++++++++++++++++++++++++++++++++++++++++++++ controllers/api.js | 2 ++ doc/schema.md | 2 ++ 4 files changed, 71 insertions(+), 2 deletions(-) diff --git a/class/Paym.js b/class/Paym.js index d043bfb..f63f40b 100644 --- a/class/Paym.js +++ b/class/Paym.js @@ -2,14 +2,15 @@ var crypto = require('crypto'); var lightningPayReq = require('bolt11'); import { BigNumber } from 'bignumber.js'; -export class Paym { +export class Payment { constructor(redis, bitcoindrpc, lightning) { this._redis = redis; this._bitcoindrpc = bitcoindrpc; this._lightning = lightning; + this._decoded = false; } - async decodePayReq(invoice) { + async decodePayReqViaRpc(invoice) { return new Promise(function(resolve, reject) { this._lightning.decodePayReq({ pay_req: invoice }, function(err, info) { if (err) return reject(err); @@ -17,4 +18,8 @@ export class Paym { }); }); } + + decodePayReq(payReq) { + this._decoded = lightningPayReq.decode(payReq); + } } diff --git a/class/User.js b/class/User.js index 7eb1c3a..a2b62e0 100644 --- a/class/User.js +++ b/class/User.js @@ -153,6 +153,13 @@ export class User { calculatedBalance -= +tx.value; } } + + let lockedPayments = await this.getLockedPayments(); + for (let paym of lockedPayments) { + // TODO: check if payment in determined state and actually evict it from this list + calculatedBalance -= +paym.amount; + } + return calculatedBalance; } @@ -386,6 +393,59 @@ export class User { } } + /** + * 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} + */ + 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} + */ + 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') diff --git a/controllers/api.js b/controllers/api.js index 4f0a4ff..82a968e 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -185,6 +185,7 @@ router.post('/payinvoice', async function(req, res) { var call = lightning.sendPayment(); 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); @@ -206,6 +207,7 @@ router.post('/payinvoice', async function(req, res) { } let inv = { payment_request: req.body.invoice, amt: info.num_satoshis }; // amt is used only for 'tip' invoices try { + await u.lockFunds(req.body.invoice, info); call.write(inv); } catch (Err) { await lock.releaseLock(); diff --git a/doc/schema.md b/doc/schema.md index ec5fb10..79bb075 100644 --- a/doc/schema.md +++ b/doc/schema.md @@ -20,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_invoices_for_{userod} = [] `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} = [] From 91f552ca7e3e270aa5dd931882d02a2efd1fda11 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Sun, 3 Mar 2019 18:57:45 +0000 Subject: [PATCH 26/28] ADD: fees --- controllers/api.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/controllers/api.js b/controllers/api.js index 82a968e..5d44aa1 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -169,8 +169,8 @@ router.post('/payinvoice', async function(req, res) { 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 * 0.01), + fee: Math.floor(info.num_satoshis * 0.01), memo: decodeURIComponent(info.description), }); @@ -187,6 +187,7 @@ router.post('/payinvoice', async function(req, res) { // payment callback await u.unlockFunds(req.body.invoice); if (payment && payment.payment_route && payment.payment_route.total_amt_msat) { + payment.payment_route.total_fees += Math.floor(+payment.payment_route.total_amt * 0.01); userBalance -= +payment.payment_route.total_fees + +payment.payment_route.total_amt; u.saveBalance(userBalance); payment.pay_req = req.body.invoice; @@ -205,7 +206,11 @@ router.post('/payinvoice', async function(req, res) { await lock.releaseLock(); return errorBadArguments(res); } - let inv = { payment_request: req.body.invoice, amt: info.num_satoshis }; // amt is used only for 'tip' invoices + 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) }, + }; try { await u.lockFunds(req.body.invoice, info); call.write(inv); From 23e835c888116264206694803498e4d25d0cea34 Mon Sep 17 00:00:00 2001 From: Overtorment Date: Sun, 3 Mar 2019 22:01:15 +0000 Subject: [PATCH 27/28] FIX: fees --- controllers/api.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/controllers/api.js b/controllers/api.js index 5d44aa1..d219d0b 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -139,8 +139,8 @@ router.post('/payinvoice', async function(req, res) { logger.log('/payinvoice', [req.id, 'userBalance: ' + userBalance, 'num_satoshis: ' + info.num_satoshis]); - if (userBalance >= info.num_satoshis) { - // got enough balance + 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 From 77fb7c0daa74c34184c6961375fd4eaf3931b91f Mon Sep 17 00:00:00 2001 From: Overtorment Date: Mon, 4 Mar 2019 12:16:03 +0000 Subject: [PATCH 28/28] FIX: display negative balance as zero --- controllers/api.js | 1 + 1 file changed, 1 insertion(+) diff --git a/controllers/api.js b/controllers/api.js index d219d0b..ab2760b 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -253,6 +253,7 @@ router.get('/balance', async function(req, res) { 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 } }); });