Post Tipping

This commit is contained in:
emad-salah 2020-10-03 16:11:19 +01:00
parent e6b2d0f896
commit c8dc984469
8 changed files with 267 additions and 43 deletions

View file

@ -48,7 +48,7 @@
"request-promise": "^4.2.2", "request-promise": "^4.2.2",
"response-time": "^2.3.2", "response-time": "^2.3.2",
"shelljs": "^0.8.2", "shelljs": "^0.8.2",
"shock-common": "8.0.0", "shock-common": "shocknet/shock-common#49aa269c723b2c2ee803662c98ba2ddd1f68f57e",
"socket.io": "2.1.1", "socket.io": "2.1.1",
"text-encoding": "^0.7.0", "text-encoding": "^0.7.0",
"tingodb": "^0.6.1", "tingodb": "^0.6.1",

View file

@ -935,7 +935,8 @@ const sendSpontaneousPayment = async (to, amount, memo, feeLimit) => {
amount: amount.toString(), amount: amount.toString(),
from: getUser()._.sea.pub, from: getUser()._.sea.pub,
memo: memo || 'no memo', memo: memo || 'no memo',
timestamp: Date.now() timestamp: Date.now(),
targetType: 'user'
} }
logger.info(JSON.stringify(order)) logger.info(JSON.stringify(order))

View file

@ -2,6 +2,7 @@
* @format * @format
*/ */
const { performance } = require('perf_hooks')
const logger = require('winston') const logger = require('winston')
const isFinite = require('lodash/isFinite') const isFinite = require('lodash/isFinite')
const isNumber = require('lodash/isNumber') const isNumber = require('lodash/isNumber')
@ -38,9 +39,18 @@ const ordersProcessed = new Set()
* @prop {boolean} private * @prop {boolean} private
*/ */
/**
* @typedef {object} InvoiceResponse
* @prop {string} payment_request
* @prop {Buffer} r_hash
*/
let currentOrderAddr = '' let currentOrderAddr = ''
/** @param {InvoiceRequest} invoiceReq */ /**
* @param {InvoiceRequest} invoiceReq
* @returns {Promise<InvoiceResponse>}
*/
const _addInvoice = invoiceReq => const _addInvoice = invoiceReq =>
new Promise((resolve, rej) => { new Promise((resolve, rej) => {
const { const {
@ -49,12 +59,12 @@ const _addInvoice = invoiceReq =>
lightning.addInvoice(invoiceReq, ( lightning.addInvoice(invoiceReq, (
/** @type {any} */ error, /** @type {any} */ error,
/** @type {{ payment_request: string }} */ response /** @type {InvoiceResponse} */ response
) => { ) => {
if (error) { if (error) {
rej(error) rej(error)
} else { } else {
resolve(response.payment_request) resolve(response)
} }
}) })
}) })
@ -85,7 +95,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
return return
} }
const listenerStartTime = Date.now() const listenerStartTime = performance.now()
ordersProcessed.add(orderID) ordersProcessed.add(orderID)
@ -95,7 +105,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
)} -- addr: ${addr}` )} -- addr: ${addr}`
) )
const orderAnswerStartTime = Date.now() const orderAnswerStartTime = performance.now()
const alreadyAnswered = await getUser() const alreadyAnswered = await getUser()
.get(Key.ORDER_TO_RESPONSE) .get(Key.ORDER_TO_RESPONSE)
@ -107,11 +117,11 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
return return
} }
const orderAnswerEndTime = Date.now() - orderAnswerStartTime const orderAnswerEndTime = performance.now() - orderAnswerStartTime
logger.info(`[PERF] Order Already Answered: ${orderAnswerEndTime}ms`) logger.info(`[PERF] Order Already Answered: ${orderAnswerEndTime}ms`)
const decryptStartTime = Date.now() const decryptStartTime = performance.now()
const senderEpub = await Utils.pubToEpub(order.from) const senderEpub = await Utils.pubToEpub(order.from)
const secret = await SEA.secret(senderEpub, getUser()._.sea) const secret = await SEA.secret(senderEpub, getUser()._.sea)
@ -121,7 +131,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
SEA.decrypt(order.memo, secret) SEA.decrypt(order.memo, secret)
]) ])
const decryptEndTime = Date.now() - decryptStartTime const decryptEndTime = performance.now() - decryptStartTime
logger.info(`[PERF] Decrypt invoice info: ${decryptEndTime}ms`) logger.info(`[PERF] Decrypt invoice info: ${decryptEndTime}ms`)
@ -156,14 +166,11 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
`onOrders() -> Will now create an invoice : ${JSON.stringify(invoiceReq)}` `onOrders() -> Will now create an invoice : ${JSON.stringify(invoiceReq)}`
) )
const invoiceStartTime = Date.now() const invoiceStartTime = performance.now()
/**
* @type {string}
*/
const invoice = await _addInvoice(invoiceReq) const invoice = await _addInvoice(invoiceReq)
const invoiceEndTime = Date.now() - invoiceStartTime const invoiceEndTime = performance.now() - invoiceStartTime
logger.info(`[PERF] LND Invoice created in ${invoiceEndTime}ms`) logger.info(`[PERF] LND Invoice created in ${invoiceEndTime}ms`)
@ -171,11 +178,11 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
'onOrders() -> Successfully created the invoice, will now encrypt it' 'onOrders() -> Successfully created the invoice, will now encrypt it'
) )
const invoiceEncryptStartTime = Date.now() const invoiceEncryptStartTime = performance.now()
const encInvoice = await SEA.encrypt(invoice, secret) const encInvoice = await SEA.encrypt(invoice.payment_request, secret)
const invoiceEncryptEndTime = Date.now() - invoiceEncryptStartTime const invoiceEncryptEndTime = performance.now() - invoiceEncryptStartTime
logger.info(`[PERF] Invoice encrypted in ${invoiceEncryptEndTime}ms`) logger.info(`[PERF] Invoice encrypted in ${invoiceEncryptEndTime}ms`)
@ -189,7 +196,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
type: 'invoice' type: 'invoice'
} }
const invoicePutStartTime = Date.now() const invoicePutStartTime = performance.now()
await new Promise((res, rej) => { await new Promise((res, rej) => {
getUser() getUser()
@ -209,13 +216,35 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
}) })
}) })
const invoicePutEndTime = Date.now() - invoicePutStartTime const invoicePutEndTime = performance.now() - invoicePutStartTime
logger.info(`[PERF] Added invoice to GunDB in ${invoicePutEndTime}ms`) logger.info(`[PERF] Added invoice to GunDB in ${invoicePutEndTime}ms`)
const listenerEndTime = Date.now() - listenerStartTime const listenerEndTime = performance.now() - listenerStartTime
logger.info(`[PERF] Invoice generation completed in ${listenerEndTime}ms`) logger.info(`[PERF] Invoice generation completed in ${listenerEndTime}ms`)
const hash = invoice.r_hash.toString('base64')
if (order.targetType === 'post') {
getUser()
.get(Key.TIPS_PAYMENT_STATUS)
.get(hash)
.put(
{
hash,
state: 'OPEN',
targetType: order.targetType,
// @ts-ignore
postID: order.postID,
// @ts-ignore
postPage: order.postPage
},
response => {
console.log(response)
}
)
}
} catch (err) { } catch (err) {
logger.error( logger.error(
`error inside onOrders, orderAddr: ${addr}, orderID: ${orderID}, order: ${JSON.stringify( `error inside onOrders, orderAddr: ${addr}, orderID: ${orderID}, order: ${JSON.stringify(

View file

@ -55,3 +55,8 @@ exports.CONTENT_ITEMS = 'contentItems'
exports.FOLLOWS = 'follows' exports.FOLLOWS = 'follows'
exports.POSTS = 'posts' exports.POSTS = 'posts'
// Tips counter for posts
exports.TOTAL_TIPS = 'totalTips'
exports.TIPS_PAYMENT_STATUS = 'tipsPaymentStatus'

View file

@ -35,6 +35,7 @@ const {
sendPaymentV2Invoice, sendPaymentV2Invoice,
listPayments listPayments
} = require('../utils/lightningServices/v2') } = require('../utils/lightningServices/v2')
const { startTipStatusJob } = require('../utils/lndJobs')
const DEFAULT_MAX_NUM_ROUTES_TO_QUERY = 10 const DEFAULT_MAX_NUM_ROUTES_TO_QUERY = 10
const SESSION_ID = uuid() const SESSION_ID = uuid()
@ -686,6 +687,7 @@ module.exports = async (
} }
onNewChannelBackup() onNewChannelBackup()
startTipStatusJob()
// Generate auth token and send it as a JSON response // Generate auth token and send it as a JSON response
const token = await auth.generateToken() const token = await auth.generateToken()

View file

@ -26,15 +26,16 @@ const server = program => {
sensitiveRoutes, sensitiveRoutes,
nonEncryptedRoutes nonEncryptedRoutes
} = require('../utils/protectedRoutes') } = require('../utils/protectedRoutes')
// load app default configuration data // load app default configuration data
const defaults = require('../config/defaults')(program.mainnet) const defaults = require('../config/defaults')(program.mainnet)
const rootFolder = process.resourcesPath || __dirname const rootFolder = process.resourcesPath || __dirname
// define useful global variables ======================================
// define env variables
Dotenv.config() Dotenv.config()
module.useTLS = program.usetls
module.serverPort = program.serverport || defaults.serverPort const serverPort = program.serverport || defaults.serverPort
module.httpsPort = module.serverPort const serverHost = program.serverhost || defaults.serverHost
module.serverHost = program.serverhost || defaults.serverHost
// setup winston logging ========== // setup winston logging ==========
const logger = require('../config/log')( const logger = require('../config/log')(
@ -274,8 +275,8 @@ const server = program => {
const Sockets = require('./sockets')(io) const Sockets = require('./sockets')(io)
require('./routes')(app, defaults, Sockets, { require('./routes')(app, defaults, Sockets, {
serverHost: module.serverHost, serverHost,
serverPort: module.serverPort, serverPort,
usetls: program.usetls, usetls: program.usetls,
CA, CA,
CA_KEY CA_KEY
@ -290,20 +291,11 @@ const server = program => {
app.use(modifyResponseBody) app.use(modifyResponseBody)
} }
serverInstance.listen(module.serverPort, module.serverhost) serverInstance.listen(serverPort, serverHost)
logger.info( logger.info('App listening on ' + serverHost + ' port ' + serverPort)
'App listening on ' + module.serverHost + ' port ' + module.serverPort
)
module.server = serverInstance module.server = serverInstance
// const localtunnel = require('localtunnel');
//
// const tunnel = localtunnel(port, (err, t) => {
// logger.info('err', err);
// logger.info('t', t.url);
// });
} catch (err) { } catch (err) {
logger.info(err) logger.info(err)
logger.info('Restarting server in 30 seconds...') logger.info('Restarting server in 30 seconds...')

196
utils/lndJobs.js Normal file
View file

@ -0,0 +1,196 @@
/**
* @prettier
*/
const Logger = require('winston')
const Key = require('../services/gunDB/contact-api/key')
const { getUser } = require('../services/gunDB/Mediator')
const LightningServices = require('./lightningServices')
const ERROR_TRIES_THRESHOLD = 3
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, page }) =>
new Promise((resolve, reject) => {
getUser()
.get(Key.WALL)
.get(Key.PAGES)
.get(page)
.get(Key.POSTS)
.get(postID)
.once(post => {
if (post && post.date) {
const { tipCounter, tipValue } = post
console.log(post)
resolve({
tipCounter: typeof tipCounter === 'number' ? tipCounter : 0,
tipValue: typeof tipValue === 'number' ? tipValue : 0
})
}
resolve(post)
})
})
const _incrementPost = ({ postID, page, 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, page })
.then(({ tipValue, tipCounter }) => {
const updatedTip = {
tipCounter: tipCounter + 1,
tipValue: tipValue + parsedAmount
}
getUser()
.get(Key.WALL)
.get(Key.PAGES)
.get(page)
.get(Key.POSTS)
.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 =>
new Promise((resolve, reject) => {
getUser()
.get(Key.TIPS_PAYMENT_STATUS)
.get(invoiceHash)
.once(tip => {
if (tip === undefined) {
reject(new Error('Malformed data'))
return
}
resolve(tip)
})
})
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,
page: tip.postPage,
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)
}
})
}
module.exports = {
startTipStatusJob
}

View file

@ -6016,10 +6016,9 @@ shellwords@^0.1.1:
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
shock-common@8.0.0: shock-common@shocknet/shock-common#49aa269c723b2c2ee803662c98ba2ddd1f68f57e:
version "8.0.0" version "10.0.0"
resolved "https://registry.yarnpkg.com/shock-common/-/shock-common-8.0.0.tgz#4dbc8c917adfb221a00b6d1e815c4d26d205ce66" resolved "https://codeload.github.com/shocknet/shock-common/tar.gz/49aa269c723b2c2ee803662c98ba2ddd1f68f57e"
integrity sha512-X9jkSxNUjQOcVdEAGBl6dlBgBxF9MpjV50Cih4hoqLqeGfrAYHK/iqgXgDyaHkLraHRxdP6FWJ2DoWOpuBgpDQ==
dependencies: dependencies:
immer "^6.0.6" immer "^6.0.6"
lodash "^4.17.19" lodash "^4.17.19"