diff --git a/package.json b/package.json index 81443eb5..f2f0196a 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "request-promise": "^4.2.2", "response-time": "^2.3.2", "shelljs": "^0.8.2", - "shock-common": "8.0.0", + "shock-common": "shocknet/shock-common#49aa269c723b2c2ee803662c98ba2ddd1f68f57e", "socket.io": "2.1.1", "text-encoding": "^0.7.0", "tingodb": "^0.6.1", diff --git a/services/gunDB/contact-api/actions.js b/services/gunDB/contact-api/actions.js index 74cc77b0..b0fa8070 100644 --- a/services/gunDB/contact-api/actions.js +++ b/services/gunDB/contact-api/actions.js @@ -935,7 +935,8 @@ const sendSpontaneousPayment = async (to, amount, memo, feeLimit) => { amount: amount.toString(), from: getUser()._.sea.pub, memo: memo || 'no memo', - timestamp: Date.now() + timestamp: Date.now(), + targetType: 'user' } logger.info(JSON.stringify(order)) diff --git a/services/gunDB/contact-api/jobs/onOrders.js b/services/gunDB/contact-api/jobs/onOrders.js index a297c058..ca72f2b8 100644 --- a/services/gunDB/contact-api/jobs/onOrders.js +++ b/services/gunDB/contact-api/jobs/onOrders.js @@ -2,6 +2,7 @@ * @format */ +const { performance } = require('perf_hooks') const logger = require('winston') const isFinite = require('lodash/isFinite') const isNumber = require('lodash/isNumber') @@ -38,9 +39,18 @@ const ordersProcessed = new Set() * @prop {boolean} private */ +/** + * @typedef {object} InvoiceResponse + * @prop {string} payment_request + * @prop {Buffer} r_hash + */ + let currentOrderAddr = '' -/** @param {InvoiceRequest} invoiceReq */ +/** + * @param {InvoiceRequest} invoiceReq + * @returns {Promise} + */ const _addInvoice = invoiceReq => new Promise((resolve, rej) => { const { @@ -49,12 +59,12 @@ const _addInvoice = invoiceReq => lightning.addInvoice(invoiceReq, ( /** @type {any} */ error, - /** @type {{ payment_request: string }} */ response + /** @type {InvoiceResponse} */ response ) => { if (error) { rej(error) } else { - resolve(response.payment_request) + resolve(response) } }) }) @@ -85,7 +95,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { return } - const listenerStartTime = Date.now() + const listenerStartTime = performance.now() ordersProcessed.add(orderID) @@ -95,7 +105,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { )} -- addr: ${addr}` ) - const orderAnswerStartTime = Date.now() + const orderAnswerStartTime = performance.now() const alreadyAnswered = await getUser() .get(Key.ORDER_TO_RESPONSE) @@ -107,11 +117,11 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { return } - const orderAnswerEndTime = Date.now() - orderAnswerStartTime + const orderAnswerEndTime = performance.now() - orderAnswerStartTime logger.info(`[PERF] Order Already Answered: ${orderAnswerEndTime}ms`) - const decryptStartTime = Date.now() + const decryptStartTime = performance.now() const senderEpub = await Utils.pubToEpub(order.from) const secret = await SEA.secret(senderEpub, getUser()._.sea) @@ -121,7 +131,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { SEA.decrypt(order.memo, secret) ]) - const decryptEndTime = Date.now() - decryptStartTime + const decryptEndTime = performance.now() - decryptStartTime 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)}` ) - const invoiceStartTime = Date.now() + const invoiceStartTime = performance.now() - /** - * @type {string} - */ const invoice = await _addInvoice(invoiceReq) - const invoiceEndTime = Date.now() - invoiceStartTime + const invoiceEndTime = performance.now() - invoiceStartTime 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' ) - 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`) @@ -189,7 +196,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { type: 'invoice' } - const invoicePutStartTime = Date.now() + const invoicePutStartTime = performance.now() await new Promise((res, rej) => { 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`) - const listenerEndTime = Date.now() - listenerStartTime + 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') { + 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) { logger.error( `error inside onOrders, orderAddr: ${addr}, orderID: ${orderID}, order: ${JSON.stringify( diff --git a/services/gunDB/contact-api/key.js b/services/gunDB/contact-api/key.js index 0c130712..5bc5ad04 100644 --- a/services/gunDB/contact-api/key.js +++ b/services/gunDB/contact-api/key.js @@ -55,3 +55,8 @@ exports.CONTENT_ITEMS = 'contentItems' exports.FOLLOWS = 'follows' exports.POSTS = 'posts' + +// Tips counter for posts +exports.TOTAL_TIPS = 'totalTips' + +exports.TIPS_PAYMENT_STATUS = 'tipsPaymentStatus' diff --git a/src/routes.js b/src/routes.js index aa1e00ed..f537a502 100644 --- a/src/routes.js +++ b/src/routes.js @@ -35,6 +35,7 @@ const { sendPaymentV2Invoice, listPayments } = require('../utils/lightningServices/v2') +const { startTipStatusJob } = require('../utils/lndJobs') const DEFAULT_MAX_NUM_ROUTES_TO_QUERY = 10 const SESSION_ID = uuid() @@ -686,6 +687,7 @@ module.exports = async ( } onNewChannelBackup() + startTipStatusJob() // Generate auth token and send it as a JSON response const token = await auth.generateToken() diff --git a/src/server.js b/src/server.js index c741ae45..9d908d84 100644 --- a/src/server.js +++ b/src/server.js @@ -26,15 +26,16 @@ const server = program => { sensitiveRoutes, nonEncryptedRoutes } = require('../utils/protectedRoutes') + // load app default configuration data const defaults = require('../config/defaults')(program.mainnet) const rootFolder = process.resourcesPath || __dirname - // define useful global variables ====================================== + + // define env variables Dotenv.config() - module.useTLS = program.usetls - module.serverPort = program.serverport || defaults.serverPort - module.httpsPort = module.serverPort - module.serverHost = program.serverhost || defaults.serverHost + + const serverPort = program.serverport || defaults.serverPort + const serverHost = program.serverhost || defaults.serverHost // setup winston logging ========== const logger = require('../config/log')( @@ -274,8 +275,8 @@ const server = program => { const Sockets = require('./sockets')(io) require('./routes')(app, defaults, Sockets, { - serverHost: module.serverHost, - serverPort: module.serverPort, + serverHost, + serverPort, usetls: program.usetls, CA, CA_KEY @@ -290,20 +291,11 @@ const server = program => { app.use(modifyResponseBody) } - serverInstance.listen(module.serverPort, module.serverhost) + serverInstance.listen(serverPort, serverHost) - logger.info( - 'App listening on ' + module.serverHost + ' port ' + module.serverPort - ) + logger.info('App listening on ' + serverHost + ' port ' + serverPort) module.server = serverInstance - - // const localtunnel = require('localtunnel'); - // - // const tunnel = localtunnel(port, (err, t) => { - // logger.info('err', err); - // logger.info('t', t.url); - // }); } catch (err) { logger.info(err) logger.info('Restarting server in 30 seconds...') diff --git a/utils/lndJobs.js b/utils/lndJobs.js new file mode 100644 index 00000000..97f5062f --- /dev/null +++ b/utils/lndJobs.js @@ -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 +} diff --git a/yarn.lock b/yarn.lock index 097d1bed..79d65d2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6016,10 +6016,9 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== -shock-common@8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/shock-common/-/shock-common-8.0.0.tgz#4dbc8c917adfb221a00b6d1e815c4d26d205ce66" - integrity sha512-X9jkSxNUjQOcVdEAGBl6dlBgBxF9MpjV50Cih4hoqLqeGfrAYHK/iqgXgDyaHkLraHRxdP6FWJ2DoWOpuBgpDQ== +shock-common@shocknet/shock-common#49aa269c723b2c2ee803662c98ba2ddd1f68f57e: + version "10.0.0" + resolved "https://codeload.github.com/shocknet/shock-common/tar.gz/49aa269c723b2c2ee803662c98ba2ddd1f68f57e" dependencies: immer "^6.0.6" lodash "^4.17.19"