diff --git a/class/User.js b/class/User.js index a34596c4..6aef7f13 100644 --- a/class/User.js +++ b/class/User.js @@ -21,6 +21,7 @@ export class User { this._bitcoindrpc = bitcoindrpc; this._lightning = lightning; this._userid = false; + this._publicid = false; this._login = false; this._password = false; this._balance = 0; @@ -43,6 +44,18 @@ export class User { return this._refresh_token; } + async loadByPublicId(publicId) { + if (!publicId) return false; + let userid = await this._redis.get('userid_for_' + publicId); + + if (userid) { + this._userid = userid; + return true; + } + + return false; + } + async loadByAuthorization(authorization) { if (!authorization) return false; let access_token = authorization.replace('Bearer ', ''); @@ -76,9 +89,14 @@ export class User { buffer = crypto.randomBytes(24); let userid = buffer.toString('hex'); + + buffer = crypto.randomBytes(24); + let publicid = buffer.toString('hex'); + this._login = login; this._password = password; this._userid = userid; + this._publicid = publicid; await this._saveUserToDatabase(); } @@ -131,6 +149,21 @@ export class User { if (config.bitcoind) return this._bitcoindrpc.request('importaddress', [address, address, false]); } + async getPublicId() { + if (!this._userid) return; + + // Old user without publicid compatibility + let publicid = await this._redis.get('publicid_for_' + this._userid); + if (!publicid) { + const buffer = crypto.randomBytes(24); + publicid = buffer.toString('hex'); + await this._redis.set('publicid_for_' + this._userid, publicid); + await this._redis.set('userid_for_' + publicid, this._userid); + } + + return publicid; + } + /** * LndHub no longer relies on redis balance as source of truth, this is * more a cache now. See `this.getCalculatedBalance()` to get correct balance. @@ -494,6 +527,8 @@ export class User { async _saveUserToDatabase() { let key; await this._redis.set((key = 'user_' + this._login + '_' + this._hash(this._password)), this._userid); + await this._redis.set('userid_for_' + this._publicid, this._userid); + await this._redis.set('publicid_for_' + this._userid, this._publicid); } /** diff --git a/controllers/api.js b/controllers/api.js index 487c2019..47a3cef9 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -4,6 +4,8 @@ const config = require('../config'); let express = require('express'); let router = express.Router(); let logger = require('../utils/logger'); +let crypto = require('crypto'); +let { bech32 } = require('bech32'); const MIN_BTC_BLOCK = 670000; console.log('using config', JSON.stringify(config)); @@ -164,6 +166,17 @@ router.post('/auth', postLimiter, async function (req, res) { } }); +router.get('/publicid', postLimiter, async function (req, res) { + logger.log('/publicid', [req.id]); + let u = new User(redis, bitcoinclient, lightning); + if (!(await u.loadByAuthorization(req.headers.authorization))) { + return errorBadAuth(res); + } + + logger.log('/publicid', [req.id, 'userid: ' + u.getUserId(), 'invoice: ' + req.body.invoice]); + res.send(await u.getPublicId()); +}); + router.post('/addinvoice', postLimiter, async function (req, res) { logger.log('/addinvoice', [req.id]); let u = new User(redis, bitcoinclient, lightning); @@ -192,6 +205,71 @@ router.post('/addinvoice', postLimiter, async function (req, res) { ); }); +router.get('/lnurlp/:publicid/qr', postLimiter, async function (req, res) { + logger.log('/lnurlp/:publicid', [req.id]); + let u = new User(redis, bitcoinclient, lightning); + const publicid = req.params.publicid; + if (!(await u.loadByPublicId(publicid))) { + return errorBadArguments(res); + } + + let host = req.headers.host; + const url = req.protocol + '://' + host + '/lnurlp/' + publicid; + const words = bech32.toWords(Buffer.from(url, 'utf8')); + const lnurlp = bech32.encode('lnurl', words, 1023); + res.send({ lnurlp }); +}); + +router.get('/lnurlp/:publicid', postLimiter, async function (req, res) { + logger.log('/lnurlp/:publicid', [req.id]); + let u = new User(redis, bitcoinclient, lightning); + const publicid = req.params.publicid; + if (!(await u.loadByPublicId(publicid))) { + return errorBadArguments(res); + } + + const metadata = JSON.stringify([['text/plain', 'Static QR code provided by BlueWallet.io']]); + + if (!req.query.amount) { + let host = req.headers.host; + return res.send({ + callback: req.protocol + '://' + host + '/lnurlp/' + publicid, + maxSendable: 1000000000, + minSendable: 1000, + metadata, + tag: 'payRequest', + }); + } + + const satAmount = req.query.amount / 1000; + + const invoice = new Invo(redis, bitcoinclient, lightning); + const r_preimage = invoice.makePreimageHex(); + lightning.addInvoice( + { + memo: '', + value: satAmount, + expiry: 3600 * 24, + r_preimage: Buffer.from(r_preimage, 'hex').toString('base64'), + description_hash: crypto.createHash('sha256').update(metadata).digest(), + }, + async function (err, info) { + if (err) return errorLnd(res); + + await u.saveUserInvoice(info); + await invoice.savePreimage(r_preimage); + + res.send({ + status: 'OK', + successAction: { tag: 'message', message: 'Payment received!' }, + routes: [], + pr: info.payment_request, + disposable: false, + }); + }, + ); +}); + router.post('/payinvoice', async function (req, res) { let u = new User(redis, bitcoinclient, lightning); if (!(await u.loadByAuthorization(req.headers.authorization))) { diff --git a/package-lock.json b/package-lock.json index 4a550e6f..12a2b6de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1624,9 +1624,9 @@ } }, "bech32": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", - "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" }, "bigi": { "version": "1.4.2", @@ -1711,6 +1711,13 @@ "typeforce": "^1.11.3", "varuint-bitcoin": "^1.0.4", "wif": "^2.0.1" + }, + "dependencies": { + "bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" + } } }, "bluebird": { @@ -1755,6 +1762,11 @@ "secp256k1": "^3.4.0" }, "dependencies": { + "bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" + }, "bitcoinjs-lib": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-3.3.2.tgz", diff --git a/package.json b/package.json index ae59899d..c470e945 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@babel/register": "^7.14.5", "@grpc/grpc-js": "^1.3.7", "@grpc/proto-loader": "^0.6.4", + "bech32": "^2.0.0", "bignumber.js": "^9.0.1", "bitcoinjs-lib": "^5.2.0", "bolt11": "^1.3.2",