From 48f5edf34e2f8dbecacd87766a3616184e5325b3 Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Thu, 3 Dec 2020 11:35:47 -0400 Subject: [PATCH 1/5] invoices lnd service --- services/lnd/lightning.js | 7 ++++++- utils/lightningServices/lightning-services.js | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/services/lnd/lightning.js b/services/lnd/lightning.js index fbf0f237..f919ef6b 100644 --- a/services/lnd/lightning.js +++ b/services/lnd/lightning.js @@ -21,6 +21,7 @@ const errorConstants = require("../../constants/errors"); * @prop {any} lightning * @prop {any} walletUnlocker * @prop {any} router + * @prop {any} invoices */ /** @@ -93,11 +94,15 @@ module.exports = async ({ const walletUnlocker = new walletunlockerrpc.WalletUnlocker(lndHost, credentials); // @ts-ignore const router = new routerrpc.Router(lndHost, credentials); + // @ts-expect-error + const invoices = new lnrpc.Invoices(lndHost, credentials) + return { lightning, walletUnlocker, - router + router, + invoices }; } diff --git a/utils/lightningServices/lightning-services.js b/utils/lightningServices/lightning-services.js index a0631f1b..5ef7779a 100644 --- a/utils/lightningServices/lightning-services.js +++ b/utils/lightningServices/lightning-services.js @@ -127,10 +127,11 @@ class LightningServices { if (!lnServices) { throw new Error(`Could not init lnServices`) } - const { lightning, walletUnlocker, router } = lnServices + const { lightning, walletUnlocker, router, invoices } = lnServices this.lightning = lightning this.walletUnlocker = walletUnlocker this.router = router + this.invoices = invoices this.lnServicesData = { lndProto: this.defaults.lndProto, lndHost, From be21ab433879089af706d6753fd3decca14a71c9 Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Thu, 3 Dec 2020 11:44:11 -0400 Subject: [PATCH 2/5] remove tip counter job --- services/gunDB/contact-api/key.js | 2 - src/routes.js | 2 - utils/lndJobs.js | 211 ------------------------------ 3 files changed, 215 deletions(-) delete mode 100644 utils/lndJobs.js diff --git a/services/gunDB/contact-api/key.js b/services/gunDB/contact-api/key.js index ece0c1ed..837ffc7d 100644 --- a/services/gunDB/contact-api/key.js +++ b/services/gunDB/contact-api/key.js @@ -59,8 +59,6 @@ exports.POSTS = 'posts' // Tips counter for posts exports.TOTAL_TIPS = 'totalTips' -exports.TIPS_PAYMENT_STATUS = 'tipsPaymentStatus' - exports.PROFILE_BINARY = 'profileBinary' exports.POSTS_NEW = 'posts' diff --git a/src/routes.js b/src/routes.js index 2fd43091..a23546d8 100644 --- a/src/routes.js +++ b/src/routes.js @@ -35,7 +35,6 @@ const { sendPaymentV2Invoice, listPayments } = require('../utils/lightningServices/v2') -const { startTipStatusJob } = require('../utils/lndJobs') const GunWriteRPC = require('../services/gunDB/rpc') const DEFAULT_MAX_NUM_ROUTES_TO_QUERY = 10 @@ -690,7 +689,6 @@ module.exports = async ( } onNewChannelBackup() - startTipStatusJob() res.json({ authorization: token, diff --git a/utils/lndJobs.js b/utils/lndJobs.js deleted file mode 100644 index 0c5791f2..00000000 --- a/utils/lndJobs.js +++ /dev/null @@ -1,211 +0,0 @@ -/** - * @prettier - */ -const Logger = require('winston') -const { wait } = require('./helpers') -const Key = require('../services/gunDB/contact-api/key') -const { getUser } = require('../services/gunDB/Mediator') -const LightningServices = require('./lightningServices') - -const ERROR_TRIES_THRESHOLD = 3 -const ERROR_TRIES_DELAY = 500 -const INVOICE_STATE = { - OPEN: 'OPEN', - SETTLED: 'SETTLED', - CANCELLED: 'CANCELLED', - ACCEPTED: 'ACCEPTED' -} - -const _lookupInvoice = hash => - new Promise((resolve, reject) => { - const { lightning } = LightningServices.services - lightning.lookupInvoice({ r_hash: hash }, (err, response) => { - if (err) { - Logger.error( - '[TIP] An error has occurred while trying to lookup invoice:', - err, - '\nInvoice Hash:', - hash - ) - reject(err) - return - } - - Logger.info('[TIP] Invoice lookup result:', response) - resolve(response) - }) - }) - -const _getPostTipInfo = ({ postID }) => - new Promise((resolve, reject) => { - getUser() - .get(Key.POSTS_NEW) - .get(postID) - .once(post => { - if (post && post.date) { - const { tipCounter, tipValue } = post - resolve({ - tipCounter: typeof tipCounter === 'number' ? tipCounter : 0, - tipValue: typeof tipValue === 'number' ? tipValue : 0 - }) - return - } - - resolve(post) - }) - }) - -const _incrementPost = ({ postID, orderAmount }) => - new Promise((resolve, reject) => { - const parsedAmount = parseFloat(orderAmount) - - if (typeof parsedAmount !== 'number') { - reject(new Error('Invalid order amount specified')) - return - } - - Logger.info('[POST TIP] Getting Post Tip Values...') - - return _getPostTipInfo({ postID }) - .then(({ tipValue, tipCounter }) => { - const updatedTip = { - tipCounter: tipCounter + 1, - tipValue: tipValue + parsedAmount - } - - getUser() - .get(Key.POSTS_NEW) - .get(postID) - .put(updatedTip, () => { - Logger.info('[POST TIP] Successfully updated Post tip info') - resolve(updatedTip) - }) - }) - .catch(err => { - Logger.error(err) - reject(err) - }) - }) - -const _updateTipData = (invoiceHash, data) => - new Promise((resolve, reject) => { - try { - getUser() - .get(Key.TIPS_PAYMENT_STATUS) - .get(invoiceHash) - .put(data, tip => { - if (tip === undefined) { - reject(new Error('Tip update failed')) - return - } - - console.log(tip) - - resolve(tip) - }) - } catch (err) { - Logger.error('An error has occurred while updating tip^data') - throw err - } - }) - -const _getTipData = (invoiceHash, tries = 0) => - new Promise((resolve, reject) => { - if (tries >= ERROR_TRIES_THRESHOLD) { - reject(new Error('Malformed data')) - return - } - - getUser() - .get(Key.TIPS_PAYMENT_STATUS) - .get(invoiceHash) - .once(async tip => { - try { - if (tip === undefined) { - await wait(ERROR_TRIES_DELAY) - const tip = await _getTipData(invoiceHash, tries + 1) - - if (tip) { - resolve(tip) - return - } - - reject(new Error('Malformed data')) - return - } - - resolve(tip) - } catch (err) { - reject(err) - } - }) - }) - -const executeTipAction = (tip, invoice) => { - if (invoice.state !== INVOICE_STATE.SETTLED) { - return - } - - // Execute actions once invoice is settled - Logger.info('Invoice settled!', invoice) - - if (tip.targetType === 'post') { - _incrementPost({ - postID: tip.postID, - orderAmount: invoice.amt_paid_sat - }) - } -} - -const updateUnverifiedTips = () => { - getUser() - .get(Key.TIPS_PAYMENT_STATUS) - .map() - .once(async (tip, id) => { - try { - if ( - !tip || - tip.state !== INVOICE_STATE.OPEN || - (tip._errorCount && tip._errorCount >= ERROR_TRIES_THRESHOLD) - ) { - return - } - Logger.info('Unverified invoice found!', tip) - const invoice = await _lookupInvoice(tip.hash) - Logger.info('Invoice located:', invoice) - if (invoice.state !== tip.state) { - await _updateTipData(id, { state: invoice.state }) - - // Actions to be executed when the tip's state is updated - executeTipAction(tip, invoice) - } - } catch (err) { - Logger.error('[TIP] An error has occurred while updating invoice', err) - const errorCount = tip._errorCount ? tip._errorCount : 0 - _updateTipData(id, { - _errorCount: errorCount + 1 - }) - } - }) -} - -const startTipStatusJob = () => { - const { lightning } = LightningServices.services - const stream = lightning.subscribeInvoices({}) - updateUnverifiedTips() - stream.on('data', async invoice => { - const hash = invoice.r_hash.toString('base64') - const tip = await _getTipData(hash) - if (tip.state !== invoice.state) { - await _updateTipData(hash, { state: invoice.state }) - executeTipAction(tip, invoice) - } - }) - stream.on('error', err => { - Logger.error('Tip Job error' + err.details) - }) -} - -module.exports = { - startTipStatusJob -} From 41019d3409e122291f914b6dae231d359d967561 Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Thu, 3 Dec 2020 14:52:44 -0400 Subject: [PATCH 3/5] vscode setting --- .vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 5171a933..347760c1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "eslint.enable": true, "typescript.tsdk": "node_modules/typescript/lib", - "debug.node.autoAttach": "on" + "debug.node.autoAttach": "on", + "editor.formatOnSave": true } From d16d359c60cf962bdffe827a400d8173887a62c3 Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Thu, 3 Dec 2020 17:29:31 -0400 Subject: [PATCH 4/5] new tip counting mechanism --- services/gunDB/contact-api/jobs/onOrders.js | 62 ++++++++++++++------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/services/gunDB/contact-api/jobs/onOrders.js b/services/gunDB/contact-api/jobs/onOrders.js index 3a73dd73..2f5190dc 100644 --- a/services/gunDB/contact-api/jobs/onOrders.js +++ b/services/gunDB/contact-api/jobs/onOrders.js @@ -1,16 +1,17 @@ /** * @format */ - +// @ts-check const { performance } = require('perf_hooks') const logger = require('winston') const isFinite = require('lodash/isFinite') const isNumber = require('lodash/isNumber') const isNaN = require('lodash/isNaN') +const Common = require('shock-common') const { Constants: { ErrorCode }, Schema -} = require('shock-common') +} = Common const LightningServices = require('../../../../utils/lightningServices') @@ -226,30 +227,49 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { const invoicePutEndTime = performance.now() - invoicePutStartTime + const hash = invoice.r_hash.toString('base64') + + // invoices should be settled right away so we can rely on this single + // subscription instead of life-long all invoices subscription + if (order.targetType === 'post') { + const { subscribeSingleInvoice } = LightningServices.invoices + const { postID } = order + + if (!Common.isPopulatedString(postID)) { + throw new TypeError(`postID not a a populated string`) + } + + const stream = subscribeSingleInvoice({ r_hash: hash }) + + /** + * @param {Common.Invoice} invoice + */ + const onData = invoice => { + if (invoice.settled) { + getUser() + .get('postToTipCount') + .get(postID) + .set(null) // each item in the set is a tip + + stream.off() + } + } + + stream.on('data', onData) + + stream.on('status', (/** @type {any} */ status) => { + logger.info(`Post tip, post: ${postID}, invoice status: ${status}`) + }) + stream.on('end', () => { + logger.warn(`Post tip, post: ${postID}, invoice stream ended`) + }) + } + logger.info(`[PERF] Added invoice to GunDB in ${invoicePutEndTime}ms`) const listenerEndTime = performance.now() - listenerStartTime logger.info(`[PERF] Invoice generation completed in ${listenerEndTime}ms`) - - const hash = invoice.r_hash.toString('base64') - - if (order.targetType === 'post') { - /** @type {TipPaymentStatus} */ - const paymentStatus = { - hash, - state: 'OPEN', - targetType: order.targetType, - postID: order.postID - } - getUser() - .get(Key.TIPS_PAYMENT_STATUS) - .get(hash) - // @ts-ignore - .put(paymentStatus, response => { - console.log(response) - }) - } } catch (err) { logger.error( `error inside onOrders, orderAddr: ${addr}, orderID: ${orderID}, order: ${JSON.stringify( From 1ea0771013d4c4c53d0cfcbb21a6dfb5d8df61e8 Mon Sep 17 00:00:00 2001 From: hatim boufnichel Date: Sat, 5 Dec 2020 14:18:30 +0100 Subject: [PATCH 5/5] routerrpc --- config/defaults.js | 1 + config/invoices.proto | 122 ++++++++++++++++++ services/lnd/lightning.js | 22 ++-- utils/lightningServices/lightning-services.js | 2 + 4 files changed, 139 insertions(+), 8 deletions(-) create mode 100644 config/invoices.proto diff --git a/config/defaults.js b/config/defaults.js index c6cce6b3..356edbb1 100644 --- a/config/defaults.js +++ b/config/defaults.js @@ -35,6 +35,7 @@ module.exports = (mainnet = false) => { maxNumRoutesToQuery: 20, lndProto: parsePath(`${__dirname}/rpc.proto`), routerProto: parsePath(`${__dirname}/router.proto`), + invoicesProto: parsePath(`${__dirname}/invoices.proto`), walletUnlockerProto: parsePath(`${__dirname}/walletunlocker.proto`), lndHost: "localhost:10009", lndCertPath: parsePath(`${lndDirectory}/tls.cert`), diff --git a/config/invoices.proto b/config/invoices.proto new file mode 100644 index 00000000..df52b8c8 --- /dev/null +++ b/config/invoices.proto @@ -0,0 +1,122 @@ +syntax = "proto3"; + +import "rpc.proto"; + +package invoicesrpc; + +option go_package = "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"; + +// Invoices is a service that can be used to create, accept, settle and cancel +// invoices. +service Invoices { + /* + SubscribeSingleInvoice returns a uni-directional stream (server -> client) + to notify the client of state transitions of the specified invoice. + Initially the current invoice state is always sent out. + */ + rpc SubscribeSingleInvoice (SubscribeSingleInvoiceRequest) + returns (stream lnrpc.Invoice); + + /* + CancelInvoice cancels a currently open invoice. If the invoice is already + canceled, this call will succeed. If the invoice is already settled, it will + fail. + */ + rpc CancelInvoice (CancelInvoiceMsg) returns (CancelInvoiceResp); + + /* + AddHoldInvoice creates a hold invoice. It ties the invoice to the hash + supplied in the request. + */ + rpc AddHoldInvoice (AddHoldInvoiceRequest) returns (AddHoldInvoiceResp); + + /* + SettleInvoice settles an accepted invoice. If the invoice is already + settled, this call will succeed. + */ + rpc SettleInvoice (SettleInvoiceMsg) returns (SettleInvoiceResp); +} + +message CancelInvoiceMsg { + // Hash corresponding to the (hold) invoice to cancel. + bytes payment_hash = 1; +} +message CancelInvoiceResp { +} + +message AddHoldInvoiceRequest { + /* + An optional memo to attach along with the invoice. Used for record keeping + purposes for the invoice's creator, and will also be set in the description + field of the encoded payment request if the description_hash field is not + being used. + */ + string memo = 1; + + // The hash of the preimage + bytes hash = 2; + + /* + The value of this invoice in satoshis + + The fields value and value_msat are mutually exclusive. + */ + int64 value = 3; + + /* + The value of this invoice in millisatoshis + + The fields value and value_msat are mutually exclusive. + */ + int64 value_msat = 10; + + /* + Hash (SHA-256) of a description of the payment. Used if the description of + payment (memo) is too long to naturally fit within the description field + of an encoded payment request. + */ + bytes description_hash = 4; + + // Payment request expiry time in seconds. Default is 3600 (1 hour). + int64 expiry = 5; + + // Fallback on-chain address. + string fallback_addr = 6; + + // Delta to use for the time-lock of the CLTV extended to the final hop. + uint64 cltv_expiry = 7; + + /* + Route hints that can each be individually used to assist in reaching the + invoice's destination. + */ + repeated lnrpc.RouteHint route_hints = 8; + + // Whether this invoice should include routing hints for private channels. + bool private = 9; +} + +message AddHoldInvoiceResp { + /* + A bare-bones invoice for a payment within the Lightning Network. With the + details of the invoice, the sender has all the data necessary to send a + payment to the recipient. + */ + string payment_request = 1; +} + +message SettleInvoiceMsg { + // Externally discovered pre-image that should be used to settle the hold + // invoice. + bytes preimage = 1; +} + +message SettleInvoiceResp { +} + +message SubscribeSingleInvoiceRequest { + reserved 1; + + // Hash corresponding to the (hold) invoice to subscribe to. + bytes r_hash = 2; +} diff --git a/services/lnd/lightning.js b/services/lnd/lightning.js index f919ef6b..18237d44 100644 --- a/services/lnd/lightning.js +++ b/services/lnd/lightning.js @@ -10,6 +10,7 @@ const errorConstants = require("../../constants/errors"); * @typedef LightningConfig * @prop {string} lnrpcProtoPath * @prop {string} routerProtoPath + * @prop {string} invoicesProtoPath * @prop {string} walletUnlockerProtoPath * @prop {string} lndHost * @prop {string} lndCertPath @@ -31,10 +32,11 @@ const errorConstants = require("../../constants/errors"); module.exports = async ({ lnrpcProtoPath, routerProtoPath, + invoicesProtoPath, walletUnlockerProtoPath, - lndHost, - lndCertPath, - macaroonPath + lndHost, + lndCertPath, + macaroonPath }) => { try { process.env.GRPC_SSL_CIPHER_SUITES = "HIGH+ECDSA"; @@ -47,9 +49,15 @@ module.exports = async ({ includeDirs: ["node_modules/google-proto-files", "proto", Path.resolve(__dirname, "../../config")] } - const [lnrpcProto, routerProto, walletUnlockerProto] = await Promise.all([protoLoader.load(lnrpcProtoPath, protoLoaderConfig), protoLoader.load(routerProtoPath, protoLoaderConfig), protoLoader.load(walletUnlockerProtoPath, protoLoaderConfig)]); + const [lnrpcProto, routerProto, walletUnlockerProto, invoicesProto] = await Promise.all([ + protoLoader.load(lnrpcProtoPath, protoLoaderConfig), + protoLoader.load(routerProtoPath, protoLoaderConfig), + protoLoader.load(walletUnlockerProtoPath, protoLoaderConfig), + protoLoader.load(invoicesProtoPath, protoLoaderConfig) + ]); const { lnrpc } = grpc.loadPackageDefinition(lnrpcProto); const { routerrpc } = grpc.loadPackageDefinition(routerProto); + const { invoicesrpc } = grpc.loadPackageDefinition(invoicesProto); const { lnrpc: walletunlockerrpc } = grpc.loadPackageDefinition(walletUnlockerProto); const getCredentials = async () => { @@ -94,10 +102,8 @@ module.exports = async ({ const walletUnlocker = new walletunlockerrpc.WalletUnlocker(lndHost, credentials); // @ts-ignore const router = new routerrpc.Router(lndHost, credentials); - // @ts-expect-error - const invoices = new lnrpc.Invoices(lndHost, credentials) - - + // @ts-ignore + const invoices = new invoicesrpc.Invoices(lndHost, credentials); return { lightning, walletUnlocker, diff --git a/utils/lightningServices/lightning-services.js b/utils/lightningServices/lightning-services.js index 5ef7779a..9c198c3b 100644 --- a/utils/lightningServices/lightning-services.js +++ b/utils/lightningServices/lightning-services.js @@ -18,6 +18,7 @@ const lnrpc = require('../../services/lnd/lightning') * @prop {string} macaroonPath * @prop {string} lndProto * @prop {string} routerProto + * @prop {string} invoicesProto * @prop {string} walletUnlockerProto */ @@ -119,6 +120,7 @@ class LightningServices { const lnServices = await lnrpc({ lnrpcProtoPath: this.defaults.lndProto, routerProtoPath: this.defaults.routerProto, + invoicesProtoPath: this.defaults.invoicesProto, walletUnlockerProtoPath: this.defaults.walletUnlockerProto, lndHost, lndCertPath,