diff --git a/class/User.js b/class/User.js index a4f8e867..d0627a5e 100644 --- a/class/User.js +++ b/class/User.js @@ -320,28 +320,51 @@ export class User { return result; } + async getUserInvoiceByHash(hash) { + const invoices = await this.getUserInvoices(); + return invoices.find(i => Buffer.from(i.r_hash).toString('hex') === hash); + } + async addAddress(address) { await this._redis.set('bitcoin_address_for_' + this._userid, address); } /** - * User's onchain txs that are >= 3 confs + * User's onchain txs that are >= configured target confirmations * Queries bitcoind RPC. * * @returns {Promise} */ async getTxs() { + let onchainTx = await this.getOnchainTxs() + let lightningTxs = await this.getLightningTxs() + + return [...onchainTx, ...lightningTxs] + } + + async getOnchainTxs() { const addr = await this.getOrGenerateAddress(); + const targetConfirmations = config.bitcoin.confirmations; let txs = await this._listtransactions(); txs = txs.result; let result = []; for (let tx of txs) { - if (tx.confirmations >= 3 && tx.address === addr && tx.category === 'receive') { + if (tx.confirmations >= targetConfirmations && tx.address === addr && tx.category === 'receive') { tx.type = 'bitcoind_tx'; result.push(tx); } } + return result + } + + async getOnchainTxById(id) { + const txs = await this.getOnchainTxs(); + return txs.find(tx => id === `${tx.txid}${tx.vout}`); + } + + async getLightningTxs() { + const result = [] let range = await this._redis.lrange('txs_for_' + this._userid, 0, -1); for (let invoice of range) { invoice = JSON.parse(invoice); @@ -368,17 +391,25 @@ export class User { if (invoice.payment_preimage) { invoice.payment_preimage = Buffer.from(invoice.payment_preimage, 'hex').toString('hex'); } + let hash = lightningPayReq.decode(invoice.pay_req).tags.find(t => t.tagName === 'payment_hash') + invoice.r_hash = Buffer.from(hash.data, '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; } + async getLightningTxByHash(hash) { + const txs = await this.getLightningTxs() + return txs.find(tx => Buffer.from(tx.r_hash).toString('hex') === hash); + } + /** * Simple caching for this._bitcoindrpc.request('listtransactions', ['*', 100500, 0, true]); * since its too much to fetch from bitcoind every time @@ -408,8 +439,11 @@ export class User { // now, compacting response a bit for (const tx of txs.result) { ret.result.push({ + txid: tx.txid, + vout: tx.vout, category: tx.category, amount: tx.amount, + fee: tx.fee, confirmations: tx.confirmations, address: tx.address, time: tx.blocktime || tx.time, @@ -443,12 +477,15 @@ export class User { .filter((tx) => tx.label !== 'external' && !tx.label.includes('openchannel')) .map((tx) => { const decodedTx = decodeRawHex(tx.raw_tx_hex); - decodedTx.outputs.forEach((vout) => + decodedTx.outputs.forEach((vout, i) => outTxns.push({ // mark all as received, since external is filtered out + txid: tx.hash, + vout: i, category: 'receive', - confirmations: tx.num_confirmations, amount: Number(vout.value), + fee: Math.ceil(decodedTx.fees / decodedTx.vout_sz), + confirmations: tx.num_confirmations, address: vout.scriptPubKey.addresses[0], time: tx.time_stamp, }), @@ -461,17 +498,18 @@ export class User { } /** - * Returning onchain txs for user's address that are less than 3 confs + * Returning onchain txs for user's address that are less than configured target confirmations * * @returns {Promise} */ async getPendingTxs() { const addr = await this.getOrGenerateAddress(); + const targetConfirmations = config.bitcoin.confirmations; let txs = await this._listtransactions(); txs = txs.result; let result = []; for (let tx of txs) { - if (tx.confirmations < 3 && tx.address === addr && tx.category === 'receive') { + if (tx.confirmations < targetConfirmations && tx.address === addr && tx.category === 'receive') { result.push(tx); } } @@ -547,6 +585,8 @@ export class User { amount: +decodedInvoice.num_satoshis, timestamp: Math.floor(+new Date() / 1000), }; + const hash = lightningPayReq.decode(pay_req).tags.find(t => t.tagName === 'payment_hash') + doc.r_hash = Buffer.from(hash.data, 'hex') return this._redis.rpush('locked_payments_for_' + this._userid, JSON.stringify(doc)); } diff --git a/config.js b/config.js index 840a265a..6f3d73c2 100644 --- a/config.js +++ b/config.js @@ -18,6 +18,9 @@ let config = { url: '1.1.1.1:10009', password: '', }, + bitcoin: { + confirmations: 3, + }, }; if (process.env.CONFIG) { diff --git a/controllers/api.js b/controllers/api.js index 2a582e63..eee8bdbc 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -415,6 +415,50 @@ router.get('/getinfo', postLimiter, async function (req, res) { }); }); +router.get('/gettxs/:tx_hash', async function (req, res) { + const txHash = req.params.tx_hash; + + logger.log('/gettxs/' + txHash, [req.id]); + let u = new User(redis, bitcoinclient, lightning); + if (!(await u.loadByAuthorization(req.headers.authorization))) { + return errorBadAuth(res); + } + logger.log('/gettxs/' + txHash, [req.id, 'userid: ' + u.getUserId()]); + + if (!(await u.getAddress())) await u.generateAddress(); // onchain addr needed further + try { + await u.accountForPosibleTxids(); + + let tx = await u.getLightningTxByHash(txHash) + if (tx) { + return res.send(tx) + } + + let lockedPayments = await u.getLockedPayments(); + tx = lockedPayments.find(tx => Buffer.from(tx.r_hash).toString('hex') === hash) + if (tx) { + return res.send({ + type: 'paid_invoice', + fee: Math.floor(tx.amount * forwardFee) /* feelimit */, + value: tx.amount + Math.floor(tx.amount * forwardFee) /* feelimit */, + timestamp: tx.timestamp, + memo: 'Payment in transition', + r_hash: tx.r_hash + }) + } + + tx = await u.getOnchainTxById(txHash); + if (tx) { + return res.send(tx) + } + + res.send({}); + } catch (Err) { + logger.log('', [req.id, 'error gettxs:', Err.message, 'userid:', u.getUserId()]); + res.send({}); + } +}); + router.get('/gettxs', async function (req, res) { logger.log('/gettxs', [req.id]); let u = new User(redis, bitcoinclient, lightning); @@ -435,6 +479,7 @@ router.get('/gettxs', async function (req, res) { value: locked.amount + Math.floor(locked.amount * forwardFee) /* feelimit */, timestamp: locked.timestamp, memo: 'Payment in transition', + r_hash: locked.r_hash, }); } res.send(txs); @@ -444,6 +489,26 @@ router.get('/gettxs', async function (req, res) { } }); +router.get('/getuserinvoices/:invoice_hash', postLimiter, async function (req, res) { + const invoiceHash = req.params.invoice_hash; + logger.log('/getuserinvoices/' + invoiceHash, [req.id]); + + let u = new User(redis, bitcoinclient, lightning); + if (!(await u.loadByAuthorization(req.headers.authorization))) { + return errorBadAuth(res); + } + + logger.log('/getuserinvoices'/ + invoiceHash, [req.id, 'userid: ' + u.getUserId()]); + + try { + const invoice = await u.getUserInvoiceByHash(invoiceHash) + res.send(invoice || {}) + } catch (Err) { + logger.log('', [req.id, 'error getting user invoice ' + invoiceHash + ':', Err.message, 'userid:', u.getUserId()]); + res.send({}); + } +}); + router.get('/getuserinvoices', postLimiter, async function (req, res) { logger.log('/getuserinvoices', [req.id]); let u = new User(redis, bitcoinclient, lightning);