Merge pull request #271 from shocknet/tip-counter-fix

Tip counter fix
This commit is contained in:
Daniel Lugo 2020-12-10 16:10:12 -04:00 committed by GitHub
commit 2c744fb92a
9 changed files with 187 additions and 244 deletions

View file

@ -1,5 +1,6 @@
{
"eslint.enable": true,
"typescript.tsdk": "node_modules/typescript/lib",
"debug.node.autoAttach": "on"
"debug.node.autoAttach": "on",
"editor.formatOnSave": true
}

View file

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

122
config/invoices.proto Normal file
View file

@ -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;
}

View file

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

View file

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

View file

@ -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
@ -21,6 +22,7 @@ const errorConstants = require("../../constants/errors");
* @prop {any} lightning
* @prop {any} walletUnlocker
* @prop {any} router
* @prop {any} invoices
*/
/**
@ -30,6 +32,7 @@ const errorConstants = require("../../constants/errors");
module.exports = async ({
lnrpcProtoPath,
routerProtoPath,
invoicesProtoPath,
walletUnlockerProtoPath,
lndHost,
lndCertPath,
@ -46,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 () => {
@ -93,11 +102,13 @@ module.exports = async ({
const walletUnlocker = new walletunlockerrpc.WalletUnlocker(lndHost, credentials);
// @ts-ignore
const router = new routerrpc.Router(lndHost, credentials);
// @ts-ignore
const invoices = new invoicesrpc.Invoices(lndHost, credentials);
return {
lightning,
walletUnlocker,
router
router,
invoices
};
}

View file

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

View file

@ -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,
@ -127,10 +129,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,

View file

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