From 7b2bd7c26ff6ef7a2e889af5beb06adb761418d8 Mon Sep 17 00:00:00 2001 From: hatim boufnichel Date: Wed, 30 Dec 2020 19:08:14 +0100 Subject: [PATCH 01/18] write data after invoice is paid --- services/gunDB/contact-api/jobs/onOrders.js | 79 ++++++++++++--------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/services/gunDB/contact-api/jobs/onOrders.js b/services/gunDB/contact-api/jobs/onOrders.js index d869b2eb..1d46cc5b 100644 --- a/services/gunDB/contact-api/jobs/onOrders.js +++ b/services/gunDB/contact-api/jobs/onOrders.js @@ -229,45 +229,58 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { // invoices should be settled right away so we can rely on this single // subscription instead of life-long all invoices subscription + /** + * @type {string|null} + */ + let maybePostId = null if (order.targetType === 'post') { const { postID } = order + maybePostId = postID || null if (!Common.isPopulatedString(postID)) { throw new TypeError(`postID not a a populated string`) } - - const { r_hash } = invoice - - // A post tip order lifecycle is short enough that we can do it like this. - const stream = LightningServices.invoices.subscribeSingleInvoice({ - r_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`) - }) - stream.on('error', (/** @type {any} */ e) => { - logger.warn(`Post tip, post: ${postID}, error:`, e) - }) } + const { r_hash } = invoice + + // A post tip order lifecycle is short enough that we can do it like this. + const stream = LightningServices.invoices.subscribeSingleInvoice({ + r_hash + }) + + /** + * @param {Common.Invoice} invoice + */ + const invoiceSubCb = invoice => { + if (!invoice.settled) { + return + } + if (order.targetType === 'post' && typeof maybePostId === 'string') { + getUser() + .get('postToTipCount') + .get(maybePostId) + .set(null) // each item in the set is a tip + } + const coordinate = 'lnPub + invoiceIndex + payment hash(?)' //.... + const orderData = { + someInfo: 'info ' + } + getUser() + .get('orders') + .get(coordinate) + .set(orderData) // each item in the set is a tip + } + + stream.on('data', invoiceSubCb) + + stream.on('status', (/** @type {any} */ status) => { + logger.info(`${r_hash}, invoice status:`, status) + }) + stream.on('end', () => { + logger.warn(`${r_hash}, invoice stream ended`) + }) + stream.on('error', (/** @type {any} */ e) => { + logger.warn(`${r_hash}, error:`, e) + }) logger.info(`[PERF] Added invoice to GunDB in ${invoicePutEndTime}ms`) From 7fc89a63f20d6612257b997941c4758d278f7abc Mon Sep 17 00:00:00 2001 From: hatim boufnichel Date: Wed, 30 Dec 2020 19:30:44 +0100 Subject: [PATCH 02/18] update with payer too --- services/gunDB/contact-api/actions.js | 9 +++++++++ services/gunDB/contact-api/jobs/onOrders.js | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/services/gunDB/contact-api/actions.js b/services/gunDB/contact-api/actions.js index f784cfe5..8c059238 100644 --- a/services/gunDB/contact-api/actions.js +++ b/services/gunDB/contact-api/actions.js @@ -1074,6 +1074,15 @@ const sendSpontaneousPayment = async ( payment_request: orderResponse.response }) + const coordinate = 'lnPub + invoiceIndex + payment hash(?)' //.... + const orderData = { + someInfo: 'info ' + } + getUser() + .get('orders') + .get(coordinate) + .set(orderData) + return payment } catch (e) { logger.error('Error inside sendPayment()') diff --git a/services/gunDB/contact-api/jobs/onOrders.js b/services/gunDB/contact-api/jobs/onOrders.js index 1d46cc5b..5bacd961 100644 --- a/services/gunDB/contact-api/jobs/onOrders.js +++ b/services/gunDB/contact-api/jobs/onOrders.js @@ -267,7 +267,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { getUser() .get('orders') .get(coordinate) - .set(orderData) // each item in the set is a tip + .set(orderData) } stream.on('data', invoiceSubCb) From f346bfbe99c020bd0d1f21ab52e13db12ebb8daf Mon Sep 17 00:00:00 2001 From: hatim boufnichel Date: Thu, 31 Dec 2020 20:50:08 +0100 Subject: [PATCH 03/18] create order entry --- services/gunDB/contact-api/key.js | 5 + services/schema/index.js | 186 ++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 services/schema/index.js diff --git a/services/gunDB/contact-api/key.js b/services/gunDB/contact-api/key.js index 837ffc7d..06aaa1fe 100644 --- a/services/gunDB/contact-api/key.js +++ b/services/gunDB/contact-api/key.js @@ -62,3 +62,8 @@ exports.TOTAL_TIPS = 'totalTips' exports.PROFILE_BINARY = 'profileBinary' exports.POSTS_NEW = 'posts' + +// For Coordinates +exports.COORDINATES = 'coordinates' + +exports.COORDINATE_INDEX = 'coordinateIndex' diff --git a/services/schema/index.js b/services/schema/index.js new file mode 100644 index 00000000..6bccac2a --- /dev/null +++ b/services/schema/index.js @@ -0,0 +1,186 @@ +const getGunUser = () => require('../gunDB/Mediator').getUser() +const Key = require('../gunDB/contact-api/key') +/** + * @typedef {import('../gunDB/contact-api/SimpleGUN').ISEA} ISEA + * @typedef { 'spontaneousPayment' | 'tip' | 'service' | 'product' | 'other' } OrderType + * + * This represents a settled order only, unsettled orders have no coordinate + * @typedef {object} CoordinateOrder + * @prop {string} fromLndPub + * @prop {string} toLndPub + * @prop {string} fromGunPub + * @prop {string} toGunPub + * @prop {boolean} inbound + * + * @prop {string=} ownerGunPub Reserved for buddy system: + * can be undefined, '', 'me', or node owner pub key to represent node owner, + * otherwise it represents a buddy + * + * @prop {number} coordinateIndex can be payment_index, or add_index depending on if it's a payment or an invoice + * @prop {string} coordinateHash can be payment_hash, or r_hash depending on if it's a payment or an invoice, + * if it's a r_hash, must be hex encoded + * + * @prop {OrderType} type + * @prop {number} amount + * @prop {string=} description + * @prop {string=} metadata JSON encoded string to store extra data for special use cases + * @prop {number=} timestamp timestamp will be added at processing time if empty + * + */ + +/** + * @param {CoordinateOrder} order + */ +const checkOrderInfo = (order = {}) => { + const { + fromLndPub, + toLndPub, + fromGunPub, + toGunPub, + inbound, + type, + amount, + description, + coordinateIndex, + coordinateHash, + metadata, + } = order + + if (typeof fromLndPub !== 'string' || fromLndPub === '') { + return 'invalid or no "fromLndPub" field provided to order coordinate' + } + if (typeof toLndPub !== 'string' || toLndPub === '') { + return 'invalid or no "toLndPub" field provided to order coordinate' + } + if (typeof fromGunPub !== 'string' || fromGunPub === '') { + return 'invalid or no "fromGunPub" field provided to order coordinate' + } + if (typeof toGunPub !== 'string' || toGunPub === '') { + return 'invalid or no "toGunPub" field provided to order coordinate' + } + if (typeof inbound !== 'boolean') { + return 'invalid or no "inbound" field provided to order coordinate' + } + if (typeof type !== 'string' || type === '') { + return 'invalid or no "type" field provided to order coordinate' + } + if (typeof amount !== 'number') { + return 'invalid or no "amount" field provided to order coordinate' + } + + if (typeof coordinateIndex !== 'number') { + return 'invalid or no "coordinateIndex" field provided to order coordinate' + } + if (typeof coordinateHash !== 'string' || coordinateHash === '') { + return 'invalid or no "coordinateHash" field provided to order coordinate' + } + + if (description && (typeof description !== 'string' || description === '')) { + return 'invalid "description" field provided to order coordinate' + } + if (metadata && (typeof metadata !== 'string' || metadata === '')) { + return 'invalid "metadata" field provided to order coordinate' + } + return null +} + +class SchemaManager { + constructor({ memIndex = false }) {//config flag? + this.memIndex = memIndex + this.orderCreateIndexCallbacks.push(this.dateIndexCreateCb) //create more Cbs and p + } + + dateIndexName = 'dateIndex' + + memIndex = false //save the index data in memory for faster access + + // MEM INDEX, will be used only if memIndex === true + memDateIndex = {} //not implemented yet + + memGunPubIndex = {} //not implemented yet + + memLndPubIndex = {} //not implemented yet + + memTypeIndex = {} //not implemented yet + // + + /** + * @type {((order : CoordinateOrder,coordinate : string)=>void)[]} + */ + orderCreateIndexCallbacks = [] + + /** + * @param {CoordinateOrder} orderInfo + * @param {ISEA} SEA + */ + async AddOrder(orderInfo, SEA) { + + const checkErr = checkOrderInfo(orderInfo) + if (checkErr) { + throw new Error(checkErr) + } + + /** + * @type {CoordinateOrder} + */ + const filteredOrder = { + fromLndPub: orderInfo.fromLndPub, + toLndPub: orderInfo.toLndPub, + fromGunPub: orderInfo.fromGunPub, + toGunPub: orderInfo.toGunPub, + inbound: orderInfo.inbound, + ownerGunPub: orderInfo.ownerGunPub, + coordinateIndex: orderInfo.coordinateIndex, + coordinateHash: orderInfo.coordinateHash, + type: orderInfo.type, + amount: orderInfo.amount, + description: orderInfo.description, + metadata: orderInfo.metadata, + + timestamp: orderInfo.timestamp || Date.now(), + } + const orderString = JSON.stringify(filteredOrder) + const mySecret = require('../gunDB/Mediator').getMySecret() + const encryptedOrderString = await SEA.encrypt(orderString, mySecret) + const coordinatePub = filteredOrder.inbound ? filteredOrder.toLndPub : filteredOrder.fromLndPub + const coordinate = `${coordinatePub}__${filteredOrder.coordinateIndex}__${filteredOrder.coordinateHash}` + await new Promise((res, rej) => { + getGunUser() + .get(Key.COORDINATES) + .get(coordinate) + .put(encryptedOrderString, ack => { + if (ack.err && typeof ack.err !== 'number') { + rej( + new Error( + `Error saving coordinate order to user-graph: ${ack}` + ) + ) + } else { + res() + } + }) + }) + + //update all indexes with + this.orderCreateIndexCallbacks.forEach(cb => cb(filteredOrder, coordinate)) + } + + /** + * + * @param {CoordinateOrder} orderInfo + * @param {string} coordinate + */ + dateIndexCreateCb(orderInfo, coordinate) { + const date = new Date(orderInfo.timestamp) + //use UTC for consistency? + const year = date.getUTCFullYear().toString() + const month = date.getUTCMonth().toString() + + getGunUser() + .get(Key.COORDINATE_INDEX) + .get(this.dateIndexName) + .get(year) + .get(month) + .set(coordinate) + } +} \ No newline at end of file From 9aca44f85b74e8c667fd948de51300b93162c84f Mon Sep 17 00:00:00 2001 From: hatim boufnichel Date: Sat, 2 Jan 2021 17:02:12 +0100 Subject: [PATCH 04/18] read --- services/schema/index.js | 85 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 77 insertions(+), 8 deletions(-) diff --git a/services/schema/index.js b/services/schema/index.js index 6bccac2a..56f2c04d 100644 --- a/services/schema/index.js +++ b/services/schema/index.js @@ -1,4 +1,7 @@ +const Crypto = require('crypto') +const { Utils: CommonUtils } = require('shock-common') const getGunUser = () => require('../gunDB/Mediator').getUser() +const SEA = require('../gunDB/Mediator').mySEA const Key = require('../gunDB/contact-api/key') /** * @typedef {import('../gunDB/contact-api/SimpleGUN').ISEA} ISEA @@ -105,15 +108,14 @@ class SchemaManager { // /** - * @type {((order : CoordinateOrder,coordinate : string)=>void)[]} + * @type {((order : CoordinateOrder,coordinateSHA256 : string)=>void)[]} */ orderCreateIndexCallbacks = [] /** * @param {CoordinateOrder} orderInfo - * @param {ISEA} SEA */ - async AddOrder(orderInfo, SEA) { + async AddOrder(orderInfo) { const checkErr = checkOrderInfo(orderInfo) if (checkErr) { @@ -144,10 +146,13 @@ class SchemaManager { const encryptedOrderString = await SEA.encrypt(orderString, mySecret) const coordinatePub = filteredOrder.inbound ? filteredOrder.toLndPub : filteredOrder.fromLndPub const coordinate = `${coordinatePub}__${filteredOrder.coordinateIndex}__${filteredOrder.coordinateHash}` + const coordinateSHA256 = Crypto.createHash('SHA256') + .update(coordinate) + .digest('hex') await new Promise((res, rej) => { getGunUser() .get(Key.COORDINATES) - .get(coordinate) + .get(coordinateSHA256) .put(encryptedOrderString, ack => { if (ack.err && typeof ack.err !== 'number') { rej( @@ -162,15 +167,18 @@ class SchemaManager { }) //update all indexes with - this.orderCreateIndexCallbacks.forEach(cb => cb(filteredOrder, coordinate)) + this.orderCreateIndexCallbacks.forEach(cb => cb(filteredOrder, coordinateSHA256)) } /** * * @param {CoordinateOrder} orderInfo - * @param {string} coordinate + * @param {string} coordinateSHA256 */ - dateIndexCreateCb(orderInfo, coordinate) { + dateIndexCreateCb(orderInfo, coordinateSHA256) { + if (this.memIndex) { + //update date memIndex + } const date = new Date(orderInfo.timestamp) //use UTC for consistency? const year = date.getUTCFullYear().toString() @@ -181,6 +189,67 @@ class SchemaManager { .get(this.dateIndexName) .get(year) .get(month) - .set(coordinate) + .set(coordinateSHA256) + } + + /** + * if not provided, assume current month and year + * @param {number} year + * @param {number} month + */ + async getMonthCoordinates(year = null, month = null) { + const now = Date.now() + const stringYear = year !== null ? year.toString() : now.getUTCFullYear().toString() + const stringMonth = month !== null ? month.toString() : now.getUTCMonth().toString() + + const data = await new Promise((res, rej) => { + getGunUser() + .get(Key.COORDINATE_INDEX) + .get(this.dateIndexName) + .get(stringYear) + .get(stringMonth) + .load() + }) + const coordinatesArray = Object + .values(data) + .filter(coordinateSHA256 => typeof coordinateSHA256 === 'string') + + return coordinatesArray + } + + /** + * if not provided, assume current month and year + * @param {number} year + * @param {number} month + * @returns {Promise} from newer to older + */ + async getMonthOrders(year = null, month = null) { + const now = Date.now() + const intYear = year !== null ? year : now.getUTCFullYear() + const intMonth = month !== null ? month : now.getUTCMonth() + + let coordinates = null + if (this.memIndex) { + //get coordinates from this.memDateIndex + } else { + coordinates = await this.getMonthCoordinates(intYear, intMonth) + } + const orders = await CommonUtils.asyncMap(coordinates, async coordinateSHA256 => { + const encryptedOrderString = await getGunUser() + .get(Key.COORDINATES) + .get(coordinateSHA256) + .then() + const mySecret = require('../gunDB/Mediator').getMySecret() + const decryptedString = await SEA.decrypt(encryptedOrderString, mySecret) + + /** + * @type {CoordinateOrder} + */ + const orderJSON = JSON.parse(decryptedString) + return orderJSON + }) + + const orderedOrders = orders.sort((a, b) => b.timestamp - a.timestamp) + return orderedOrders } } \ No newline at end of file From 2079b4f9846cadcdccb9e82f92fa4f8c15cc76fb Mon Sep 17 00:00:00 2001 From: hatim boufnichel Date: Sat, 2 Jan 2021 17:21:44 +0100 Subject: [PATCH 05/18] update struct --- services/schema/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/schema/index.js b/services/schema/index.js index 56f2c04d..8753a5db 100644 --- a/services/schema/index.js +++ b/services/schema/index.js @@ -11,8 +11,8 @@ const Key = require('../gunDB/contact-api/key') * @typedef {object} CoordinateOrder * @prop {string} fromLndPub * @prop {string} toLndPub - * @prop {string} fromGunPub - * @prop {string} toGunPub + * @prop {string=} fromGunPub can be optional, if the payment/invoice is not related to an order + * @prop {string=} toGunPub can be optional, if the payment/invoice is not related to an order * @prop {boolean} inbound * * @prop {string=} ownerGunPub Reserved for buddy system: @@ -55,10 +55,10 @@ const checkOrderInfo = (order = {}) => { if (typeof toLndPub !== 'string' || toLndPub === '') { return 'invalid or no "toLndPub" field provided to order coordinate' } - if (typeof fromGunPub !== 'string' || fromGunPub === '') { + if (fromGunPub && (typeof fromGunPub !== 'string' || fromGunPub === '')) { return 'invalid or no "fromGunPub" field provided to order coordinate' } - if (typeof toGunPub !== 'string' || toGunPub === '') { + if (toGunPub && (typeof toGunPub !== 'string' || toGunPub === '')) { return 'invalid or no "toGunPub" field provided to order coordinate' } if (typeof inbound !== 'boolean') { From bd897662656215904474630412431a9bc1c16f35 Mon Sep 17 00:00:00 2001 From: hatim boufnichel Date: Sat, 2 Jan 2021 20:53:54 +0100 Subject: [PATCH 06/18] wire with orders --- services/gunDB/contact-api/jobs/onOrders.js | 27 ++++++--- services/schema/index.js | 64 ++++++++++++++------- utils/lightningServices/errors.js | 52 ++++++++++------- 3 files changed, 94 insertions(+), 49 deletions(-) diff --git a/services/gunDB/contact-api/jobs/onOrders.js b/services/gunDB/contact-api/jobs/onOrders.js index 5bacd961..b21d6a67 100644 --- a/services/gunDB/contact-api/jobs/onOrders.js +++ b/services/gunDB/contact-api/jobs/onOrders.js @@ -12,8 +12,9 @@ const { Constants: { ErrorCode }, Schema } = Common - +const SchemaManager = require('../../../schema') const LightningServices = require('../../../../utils/lightningServices') +const LNDHealthManager = require('../../../../utils/lightningServices/errors') const Key = require('../key') const Utils = require('../utils') @@ -248,7 +249,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { }) /** - * @param {Common.Invoice} invoice + * @param {Common.Invoice & {r_hash:Buffer}} invoice */ const invoiceSubCb = invoice => { if (!invoice.settled) { @@ -260,14 +261,22 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { .get(maybePostId) .set(null) // each item in the set is a tip } - const coordinate = 'lnPub + invoiceIndex + payment hash(?)' //.... - const orderData = { - someInfo: 'info ' + const myLndPub = LNDHealthManager.lndPub + const myGunPub = getUser()._.sea.pub + if (!myLndPub) { + return //should never happen but just to be safe } - getUser() - .get('orders') - .get(coordinate) - .set(orderData) + SchemaManager.AddOrder({ + amount: parseInt(invoice.amt_paid, 10), + coordinateHash: invoice.r_hash.toString('hex'), + coordinateIndex: parseInt(invoice.add_index, 10), + toLndPub: myLndPub, + inbound: true, + type: 'other', //TODO better this + fromGunPub: order.from, + toGunPub: myGunPub, + invoiceMemo: invoice.memo + }) } stream.on('data', invoiceSubCb) diff --git a/services/schema/index.js b/services/schema/index.js index 8753a5db..3268705a 100644 --- a/services/schema/index.js +++ b/services/schema/index.js @@ -9,8 +9,8 @@ const Key = require('../gunDB/contact-api/key') * * This represents a settled order only, unsettled orders have no coordinate * @typedef {object} CoordinateOrder - * @prop {string} fromLndPub - * @prop {string} toLndPub + * @prop {string=} fromLndPub can be unknown when inbound + * @prop {string} toLndPub always known * @prop {string=} fromGunPub can be optional, if the payment/invoice is not related to an order * @prop {string=} toGunPub can be optional, if the payment/invoice is not related to an order * @prop {boolean} inbound @@ -26,6 +26,7 @@ const Key = require('../gunDB/contact-api/key') * @prop {OrderType} type * @prop {number} amount * @prop {string=} description + * @prop {string=} invoiceMemo * @prop {string=} metadata JSON encoded string to store extra data for special use cases * @prop {number=} timestamp timestamp will be added at processing time if empty * @@ -34,7 +35,7 @@ const Key = require('../gunDB/contact-api/key') /** * @param {CoordinateOrder} order */ -const checkOrderInfo = (order = {}) => { +const checkOrderInfo = order => { const { fromLndPub, toLndPub, @@ -47,23 +48,25 @@ const checkOrderInfo = (order = {}) => { coordinateIndex, coordinateHash, metadata, + invoiceMemo } = order - if (typeof fromLndPub !== 'string' || fromLndPub === '') { - return 'invalid or no "fromLndPub" field provided to order coordinate' + if (fromLndPub && (typeof fromLndPub !== 'string' || fromLndPub === '')) { + return 'invalid "fromLndPub" field provided to order coordinate' } if (typeof toLndPub !== 'string' || toLndPub === '') { return 'invalid or no "toLndPub" field provided to order coordinate' } if (fromGunPub && (typeof fromGunPub !== 'string' || fromGunPub === '')) { - return 'invalid or no "fromGunPub" field provided to order coordinate' + return 'invalid "fromGunPub" field provided to order coordinate' } if (toGunPub && (typeof toGunPub !== 'string' || toGunPub === '')) { - return 'invalid or no "toGunPub" field provided to order coordinate' + return 'invalid "toGunPub" field provided to order coordinate' } if (typeof inbound !== 'boolean') { return 'invalid or no "inbound" field provided to order coordinate' } + //@ts-expect-error if (typeof type !== 'string' || type === '') { return 'invalid or no "type" field provided to order coordinate' } @@ -81,6 +84,9 @@ const checkOrderInfo = (order = {}) => { if (description && (typeof description !== 'string' || description === '')) { return 'invalid "description" field provided to order coordinate' } + if (invoiceMemo && (typeof invoiceMemo !== 'string' || invoiceMemo === '')) { + return 'invalid "invoiceMemo" field provided to order coordinate' + } if (metadata && (typeof metadata !== 'string' || metadata === '')) { return 'invalid "metadata" field provided to order coordinate' } @@ -90,7 +96,7 @@ const checkOrderInfo = (order = {}) => { class SchemaManager { constructor({ memIndex = false }) {//config flag? this.memIndex = memIndex - this.orderCreateIndexCallbacks.push(this.dateIndexCreateCb) //create more Cbs and p + this.orderCreateIndexCallbacks.push(this.dateIndexCreateCb) //create more Cbs and put them here for more indexes callbacks } dateIndexName = 'dateIndex' @@ -161,7 +167,7 @@ class SchemaManager { ) ) } else { - res() + res(null) } }) }) @@ -179,7 +185,7 @@ class SchemaManager { if (this.memIndex) { //update date memIndex } - const date = new Date(orderInfo.timestamp) + const date = new Date(orderInfo.timestamp || 0) //use UTC for consistency? const year = date.getUTCFullYear().toString() const month = date.getUTCMonth().toString() @@ -194,21 +200,23 @@ class SchemaManager { /** * if not provided, assume current month and year - * @param {number} year - * @param {number} month + * @param {number|null} year + * @param {number|null} month */ async getMonthCoordinates(year = null, month = null) { const now = Date.now() + //@ts-expect-error const stringYear = year !== null ? year.toString() : now.getUTCFullYear().toString() + //@ts-expect-error const stringMonth = month !== null ? month.toString() : now.getUTCMonth().toString() - const data = await new Promise((res, rej) => { + const data = await new Promise(res => { getGunUser() .get(Key.COORDINATE_INDEX) .get(this.dateIndexName) .get(stringYear) .get(stringMonth) - .load() + .load(res) }) const coordinatesArray = Object .values(data) @@ -219,13 +227,15 @@ class SchemaManager { /** * if not provided, assume current month and year - * @param {number} year - * @param {number} month + * @param {number|null} year + * @param {number|null} month * @returns {Promise} from newer to older */ async getMonthOrders(year = null, month = null) { const now = Date.now() + //@ts-expect-error const intYear = year !== null ? year : now.getUTCFullYear() + //@ts-expect-error const intMonth = month !== null ? month : now.getUTCMonth() let coordinates = null @@ -234,11 +244,21 @@ class SchemaManager { } else { coordinates = await this.getMonthCoordinates(intYear, intMonth) } - const orders = await CommonUtils.asyncMap(coordinates, async coordinateSHA256 => { + /** + * @type {CoordinateOrder[]} + */ + const orders = [] + if (!coordinates) { + return orders + } + await CommonUtils.asyncForEach(coordinates, async coordinateSHA256 => { const encryptedOrderString = await getGunUser() .get(Key.COORDINATES) .get(coordinateSHA256) .then() + if (typeof encryptedOrderString !== 'string') { + return + } const mySecret = require('../gunDB/Mediator').getMySecret() const decryptedString = await SEA.decrypt(encryptedOrderString, mySecret) @@ -246,10 +266,14 @@ class SchemaManager { * @type {CoordinateOrder} */ const orderJSON = JSON.parse(decryptedString) - return orderJSON + orders.push(orderJSON) }) - + //@ts-expect-error const orderedOrders = orders.sort((a, b) => b.timestamp - a.timestamp) return orderedOrders } -} \ No newline at end of file +} + +const Manager = new SchemaManager() + +module.exports = Manager \ No newline at end of file diff --git a/utils/lightningServices/errors.js b/utils/lightningServices/errors.js index 9fdbd737..434b0f9b 100644 --- a/utils/lightningServices/errors.js +++ b/utils/lightningServices/errors.js @@ -20,17 +20,26 @@ class LNDErrorManager { */ _isCheckingHealth = false + /** + * @type {string|null} + */ + _lndPub = null + + get lndPub() { + return this._lndPub + } + /** * @type {HealthListener[]} */ _healthListeners = [] //rejects if(err && err.code !== 12) - getAvailableService(){ - + getAvailableService() { + //require('shock-common').Utils.makePromise((res, rej) => ...) - return new Promise((res,rej)=>{ - if(!this._isCheckingHealth){ + return new Promise((res, rej) => { + if (!this._isCheckingHealth) { this._isCheckingHealth = true this.getInfo() } @@ -39,7 +48,7 @@ class LNDErrorManager { * @param {LNDError} err * @param {object} response */ - const listener = (err,response)=>{ + const listener = (err, response) => { if (err) { if (err.code === 12) { res({ @@ -58,7 +67,7 @@ class LNDErrorManager { walletStatus: 'unknown', success: false }) - } else if(err.code === 4){ + } else if (err.code === 4) { rej({ service: 'unknown', message: @@ -77,7 +86,7 @@ class LNDErrorManager { }) } } - + res({ service: 'lightning', message: response, @@ -85,7 +94,7 @@ class LNDErrorManager { walletStatus: 'unlocked', success: true }) - + } this._healthListeners.push(listener) }) @@ -93,28 +102,31 @@ class LNDErrorManager { } //private - getInfo(){ + getInfo() { const { lightning } = LightningServices.services /** * * @param {LNDError} err - * @param {object} response + * @param {{identity_pubkey:string}} response */ const callback = (err, response) => { - this._healthListeners.forEach(l =>{ - l(err,response) + if (response && response.identity_pubkey) { + this._lndPub = response.identity_pubkey + } + this._healthListeners.forEach(l => { + l(err, response) }) this._healthListeners.length = 0 this._isCheckingHealth = false } const deadline = Date.now() + 10000 - lightning.getInfo({},{deadline}, callback) + lightning.getInfo({}, { deadline }, callback) } /** * @param {LNDError} e */ - handleError(e){ + handleError(e) { return this.sanitizeLNDError(e) } @@ -122,13 +134,13 @@ class LNDErrorManager { * @param {LNDError} e */ // eslint-disable-next-line - sanitizeLNDError(e){ + sanitizeLNDError(e) { let eMessage = '' - if(typeof e === 'string'){ + if (typeof e === 'string') { eMessage = e - }else if(e.details){ + } else if (e.details) { eMessage = e.details - } else if(e.message){ + } else if (e.message) { eMessage = e.message } if (eMessage.toLowerCase().includes('unknown')) { @@ -137,13 +149,13 @@ class LNDErrorManager { ? splittedMessage.slice(1).join('') : splittedMessage.join('') } - if(eMessage === ''){ + if (eMessage === '') { return 'unknown LND error' } return eMessage } - + } From 1cb7b137d6113f30d178ad8b28f4a4146cc32111 Mon Sep 17 00:00:00 2001 From: hatim boufnichel Date: Mon, 4 Jan 2021 18:56:49 +0100 Subject: [PATCH 07/18] on chain --- services/schema/index.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/services/schema/index.js b/services/schema/index.js index 3268705a..a8a52529 100644 --- a/services/schema/index.js +++ b/services/schema/index.js @@ -5,15 +5,18 @@ const SEA = require('../gunDB/Mediator').mySEA const Key = require('../gunDB/contact-api/key') /** * @typedef {import('../gunDB/contact-api/SimpleGUN').ISEA} ISEA - * @typedef { 'spontaneousPayment' | 'tip' | 'service' | 'product' | 'other' } OrderType + * @typedef { 'spontaneousPayment' | 'tip' | 'service' | 'product' | 'other'|'invoice'|'payment'|'chainTx' } OrderType * * This represents a settled order only, unsettled orders have no coordinate - * @typedef {object} CoordinateOrder + * @typedef {object} CoordinateOrder //everything is optional for different types * @prop {string=} fromLndPub can be unknown when inbound - * @prop {string} toLndPub always known + * @prop {string=} toLndPub always known * @prop {string=} fromGunPub can be optional, if the payment/invoice is not related to an order * @prop {string=} toGunPub can be optional, if the payment/invoice is not related to an order + * @prop {string=} fromBtcPub + * @prop {string=} toBtcPub * @prop {boolean} inbound + * NOTE: type specific checks are not made before creating the order node, filters must be done before rendering or processing * * @prop {string=} ownerGunPub Reserved for buddy system: * can be undefined, '', 'me', or node owner pub key to represent node owner, @@ -41,6 +44,8 @@ const checkOrderInfo = order => { toLndPub, fromGunPub, toGunPub, + fromBtcPub, + toBtcPub, inbound, type, amount, @@ -54,7 +59,7 @@ const checkOrderInfo = order => { if (fromLndPub && (typeof fromLndPub !== 'string' || fromLndPub === '')) { return 'invalid "fromLndPub" field provided to order coordinate' } - if (typeof toLndPub !== 'string' || toLndPub === '') { + if (toLndPub && (typeof toLndPub !== 'string' || toLndPub === '')) { return 'invalid or no "toLndPub" field provided to order coordinate' } if (fromGunPub && (typeof fromGunPub !== 'string' || fromGunPub === '')) { @@ -63,6 +68,12 @@ const checkOrderInfo = order => { if (toGunPub && (typeof toGunPub !== 'string' || toGunPub === '')) { return 'invalid "toGunPub" field provided to order coordinate' } + if (fromBtcPub && (typeof fromBtcPub !== 'string' || fromBtcPub === '')) { + return 'invalid "fromBtcPub" field provided to order coordinate' + } + if (toBtcPub && (typeof toBtcPub !== 'string' || toBtcPub === '')) { + return 'invalid "toBtcPub" field provided to order coordinate' + } if (typeof inbound !== 'boolean') { return 'invalid or no "inbound" field provided to order coordinate' } From ad893a3d518ce9b416a1baba4e10e2d032521e9f Mon Sep 17 00:00:00 2001 From: hatim boufnichel Date: Wed, 6 Jan 2021 20:00:43 +0100 Subject: [PATCH 08/18] on chain updates --- services/gunDB/contact-api/key.js | 2 + services/schema/index.js | 95 +++++++++++++++++++++++++++++++ src/sockets.js | 14 ++++- 3 files changed, 110 insertions(+), 1 deletion(-) diff --git a/services/gunDB/contact-api/key.js b/services/gunDB/contact-api/key.js index 06aaa1fe..4aaf0174 100644 --- a/services/gunDB/contact-api/key.js +++ b/services/gunDB/contact-api/key.js @@ -67,3 +67,5 @@ exports.POSTS_NEW = 'posts' exports.COORDINATES = 'coordinates' exports.COORDINATE_INDEX = 'coordinateIndex' + +exports.TMP_CHAIN_COORDINATE = 'tmpChainCoordinate' diff --git a/services/schema/index.js b/services/schema/index.js index a8a52529..55ed70d4 100644 --- a/services/schema/index.js +++ b/services/schema/index.js @@ -283,6 +283,101 @@ class SchemaManager { const orderedOrders = orders.sort((a, b) => b.timestamp - a.timestamp) return orderedOrders } + + /** + * + * @param {string} address + * @param {CoordinateOrder} orderInfo + */ + //eslint-disable-next-line class-methods-use-this + async AddTmpChainOrder(address, orderInfo) { + const checkErr = checkOrderInfo(orderInfo) + if (checkErr) { + throw new Error(checkErr) + } + + /** + * @type {CoordinateOrder} + */ + const filteredOrder = { + fromLndPub: orderInfo.fromLndPub, + toLndPub: orderInfo.toLndPub, + fromGunPub: orderInfo.fromGunPub, + toGunPub: orderInfo.toGunPub, + inbound: orderInfo.inbound, + ownerGunPub: orderInfo.ownerGunPub, + coordinateIndex: orderInfo.coordinateIndex, + coordinateHash: orderInfo.coordinateHash, + type: orderInfo.type, + amount: orderInfo.amount, + description: orderInfo.description, + metadata: orderInfo.metadata, + + timestamp: orderInfo.timestamp || Date.now(), + } + const orderString = JSON.stringify(filteredOrder) + const mySecret = require('../gunDB/Mediator').getMySecret() + const encryptedOrderString = await SEA.encrypt(orderString, mySecret) + + const addressSHA256 = Crypto.createHash('SHA256') + .update(address) + .digest('hex') + + await new Promise((res, rej) => { + getGunUser() + .get(Key.TMP_CHAIN_COORDINATE) + .get(addressSHA256) + .put(encryptedOrderString, ack => { + if (ack.err && typeof ack.err !== 'number') { + rej( + new Error( + `Error saving tmp chain coordinate order to user-graph: ${ack}` + ) + ) + } else { + res(null) + } + }) + }) + } + + /** + * + * @param {string} address + * @returns {Promise} + */ + //eslint-disable-next-line class-methods-use-this + async isTmpChainOrder(address) { + if (typeof address !== 'string' || address === '') { + return false + } + const addressSHA256 = Crypto.createHash('SHA256') + .update(address) + .digest('hex') + + const maybeData = await getGunUser() + .get(Key.TMP_CHAIN_COORDINATE) + .get(addressSHA256) + .then() + + if (typeof maybeData !== 'string' || maybeData === '') { + return false + } + const mySecret = require('../gunDB/Mediator').getMySecret() + const decryptedString = await SEA.decrypt(maybeData, mySecret) + if (typeof decryptedString !== 'string' || decryptedString === '') { + return false + } + + const tmpOrder = JSON.parse(decryptedString) + const checkErr = checkOrderInfo(tmpOrder) + if (checkErr) { + return false + } + + return tmpOrder + + } } const Manager = new SchemaManager() diff --git a/src/sockets.js b/src/sockets.js index 54537af8..44c8b523 100644 --- a/src/sockets.js +++ b/src/sockets.js @@ -17,6 +17,7 @@ const { } = require('../services/gunDB/Mediator') const { deepDecryptIfNeeded } = require('../services/gunDB/rpc') const GunEvents = require('../services/gunDB/contact-api/events') +const SchemaManager = require('../services/schema') /** * @typedef {import('../services/gunDB/Mediator').SimpleSocket} SimpleSocket * @typedef {import('../services/gunDB/contact-api/SimpleGUN').ValidDataValue} ValidDataValue @@ -198,7 +199,18 @@ module.exports = ( logger.warn('Subscribing to transactions socket...' + subID) stream.on('data', data => { logger.info('[SOCKET] New transaction data:', data) - emitEncryptedEvent({ eventName: 'transaction:new', data, socket }) + + Promise.all(data.dest_addresses.map(SchemaManager.isTmpChainOrder)).then( + responses => { + const hasOrder = responses.some(res => res !== false) + if (hasOrder && data.num_confirmations > 0) { + //buddy needs to manage this + } else { + //business as usual + emitEncryptedEvent({ eventName: 'transaction:new', data, socket }) + } + } + ) }) stream.on('end', () => { logger.info('New transactions stream ended, starting a new one...') From 2354e6dfa8691ae708963cd20cec2ccc94e002de Mon Sep 17 00:00:00 2001 From: hatim boufnichel Date: Fri, 8 Jan 2021 18:54:02 +0100 Subject: [PATCH 09/18] buddy --- services/buddySystem/index.js | 211 ++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 services/buddySystem/index.js diff --git a/services/buddySystem/index.js b/services/buddySystem/index.js new file mode 100644 index 00000000..b397b6e1 --- /dev/null +++ b/services/buddySystem/index.js @@ -0,0 +1,211 @@ +const Common = require('shock-common') +/** + * @typedef {object} Buddy + * @prop {string} gunPub + * @prop {number|null} balance + * @prop {boolean} tenant + * @prop {string|null} lndPub + * @prop {string} latestMessage + */ + +/** + * @typedef {object} InvoiceRequest + * @prop {'BUDDY_INVOICE_REQUEST'} type + * @prop {string} reqId + * @prop {number=} amount + * @prop {string=} memo + * + * @typedef {object} AddressRequest + * @prop {'BUDDY_ADDRESS_REQUEST'} type + * @prop {string} reqId + * @prop {boolean=} legacy + * + * @typedef {object} LightningPaymentRequest + * @prop {'BUDDY_LIGHTNING_PAYMENT_REQUEST'} type + * @prop {string} reqId + * @prop {string} invoice + * + * @typedef {object} OnChainPaymentRequest + * @prop {'BUDDY_ON_CHAIN_PAYMENT_REQUEST'} type + * @prop {string} reqId + * @prop {string} address + * @prop {number} amount + * + * @typedef {object} InvoiceResponse + * @prop {'BUDDY_INVOICE_RESPONSE'} type + * @prop {string} reqId + * @prop {string} invoice + * + * @typedef {object} AddressResponse + * @prop {'BUDDY_ADDRESS_RESPONSE'} type + * @prop {string} reqId + * @prop {string} address + * + * @typedef {object} InvoicePaidResponse + * @prop {'BUDDY_INVOICE_PAID_RESPONSE'} type + * @prop {string} reqId + * @prop {string} invoice + * @prop {number} amount + * + * @typedef {object} AddressPaidResponse + * @prop {'BUDDY_ADDRESS_PAID_RESPONSE'} type + * @prop {string} reqId + * @prop {string} address + * @prop {number} amount + */ + +/** + * @typedef { + * InvoiceRequest | + * AddressRequest | + * LightningPaymentRequest | + * OnChainPaymentRequest | + * InvoiceResponse| + * AddressResponse| + * InvoicePaidResponse| + * AddressPaidResponse + * } BuddyOperation + */ + +//uuid in the chat message is used to reference request and their relative responses +const invoiceRequest = 'BUDDY_INVOICE_REQUEST' //prefix+uuid+amount+memo +const addressRequest = 'BUDDY_ADDRESS_REQUEST' //prefix+uuid+legacy + +const lightningPaymentRequest = 'BUDDY_LIGHTNING_PAYMENT_REQUEST' //prefix+uuid+invoice +const onChainPaymentRequest = 'BUDDY_ON_CHAIN_PAYMENT_REQUEST' //prefix+uuid+addr+amount + +const invoiceResponse = 'BUDDY_INVOICE_RESPONSE' //prefix+uuid+invoice +const addressResponse = 'BUDDY_ADDRESS_RESPONSE' //prefix+uuid+address + +const invoicePaidResponse = 'BUDDY_INVOICE_PAID_RESPONSE' //prefix+uuid+invoice+amount +const addressPaidResponse = 'BUDDY_ADDRESS_PAID_RESPONSE' //prefix+uuid+address+amount + +/* might be useful later with other operations +const successfulResponse = 'BUDDY_SUCCESSFUL_RESPONSE' //prefix+uuid +const failureResponse = 'BUDDY_FAILURE_RESPONSE' //prefix+uuid+reason +*/ +class BuddyManager { + /** + * + * @param {string} gunPub + * @param {boolean} tenant + * @param {string|null} lndPub + */ + addBuddy(gunPub, tenant, lndPub = null) { + if (this._enabledBuddies[gunPub]) { + return + } + + this._enabledBuddies[gunPub] = { + gunPub, + balance: null, + tenant, + lndPub, + latestMessage: '' + } + } + + /** + * @param {Common.Schema.Chat[]} chats + */ + onChatsUpdate(chats) { + chats.forEach(chat => { + const { recipientPublicKey } = chat + if (!this._enabledBuddies[recipientPublicKey]) { + return + } + const buddy = this._enabledBuddies[recipientPublicKey] + if (buddy.balance === null) { + this._enabledBuddies[recipientPublicKey].balance = this.calculateBuddyBalance(messages) + } + const [latestMessage] = chat.messages + if (latestMessage !== buddy.latestMessage) { + + } + + }) + } + + /** + * @type {Record} + */ + _enabledBuddies = {} + + payForBuddy() { + //check balance + //handle payment + //send confirmation/error message + //update balance + } + + receiveForBuddy() { + //create invoice and send to buddy via message + //listen to invoice to be paid + //send confirmation/error message + //update balance + } + + isInvoiceForBuddy() { + + } + + + /** + * + * @param {Common.Schema.ChatMessage[]} messages + * @returns {number|null} + */ + calculateBuddyBalance(messages) { + + } + + /** + * + * @param {Buddy} buddy + * @param {Common.Schema.ChatMessage[]} messages + */ + handleNewMessagesWithBuddy(buddy, messages) { + let tmpIndex = messages.length + if (buddy.latestMessage !== '') { + tmpIndex = messages.indexOf(latestMessage) //should be pretty fast since the messages are in reverse order, so latest message known to manager should be one of the first ones + } + + for (; tmpIndex >= 0; tmpIndex--) { + const maybeRequest = this.processMessage(messages[tmpIndex]) + if (maybeRequest) { + //process request + } + } + + } + + /** + * + * @param {Common.Schema.ChatMessage} message + * @returns {BuddyOperation|null} + */ + processMessage(message) { + if (message.outgoing) {//we only care about incoming messages here + return null + } + if (message.body.startsWith(invoiceRequest)) { + + } + if (message.body.startsWith(addressRequest)) { + + } + if (message.body.startsWith(lightningPaymentRequest)) { + + } + if (message.body.startsWith(onChainPaymentRequest)) { + + } + } + + /** + * @param {BuddyOperation} buddyRequest + */ + processRequest(buddyRequest) { + + } +} \ No newline at end of file From 8b3f09473bb952f6623072569e7825ce44bb24a7 Mon Sep 17 00:00:00 2001 From: hatim boufnichel Date: Mon, 11 Jan 2021 20:11:50 +0100 Subject: [PATCH 10/18] separate schema and buddy --- services/buddySystem/index.js | 211 ---------------------------------- 1 file changed, 211 deletions(-) delete mode 100644 services/buddySystem/index.js diff --git a/services/buddySystem/index.js b/services/buddySystem/index.js deleted file mode 100644 index b397b6e1..00000000 --- a/services/buddySystem/index.js +++ /dev/null @@ -1,211 +0,0 @@ -const Common = require('shock-common') -/** - * @typedef {object} Buddy - * @prop {string} gunPub - * @prop {number|null} balance - * @prop {boolean} tenant - * @prop {string|null} lndPub - * @prop {string} latestMessage - */ - -/** - * @typedef {object} InvoiceRequest - * @prop {'BUDDY_INVOICE_REQUEST'} type - * @prop {string} reqId - * @prop {number=} amount - * @prop {string=} memo - * - * @typedef {object} AddressRequest - * @prop {'BUDDY_ADDRESS_REQUEST'} type - * @prop {string} reqId - * @prop {boolean=} legacy - * - * @typedef {object} LightningPaymentRequest - * @prop {'BUDDY_LIGHTNING_PAYMENT_REQUEST'} type - * @prop {string} reqId - * @prop {string} invoice - * - * @typedef {object} OnChainPaymentRequest - * @prop {'BUDDY_ON_CHAIN_PAYMENT_REQUEST'} type - * @prop {string} reqId - * @prop {string} address - * @prop {number} amount - * - * @typedef {object} InvoiceResponse - * @prop {'BUDDY_INVOICE_RESPONSE'} type - * @prop {string} reqId - * @prop {string} invoice - * - * @typedef {object} AddressResponse - * @prop {'BUDDY_ADDRESS_RESPONSE'} type - * @prop {string} reqId - * @prop {string} address - * - * @typedef {object} InvoicePaidResponse - * @prop {'BUDDY_INVOICE_PAID_RESPONSE'} type - * @prop {string} reqId - * @prop {string} invoice - * @prop {number} amount - * - * @typedef {object} AddressPaidResponse - * @prop {'BUDDY_ADDRESS_PAID_RESPONSE'} type - * @prop {string} reqId - * @prop {string} address - * @prop {number} amount - */ - -/** - * @typedef { - * InvoiceRequest | - * AddressRequest | - * LightningPaymentRequest | - * OnChainPaymentRequest | - * InvoiceResponse| - * AddressResponse| - * InvoicePaidResponse| - * AddressPaidResponse - * } BuddyOperation - */ - -//uuid in the chat message is used to reference request and their relative responses -const invoiceRequest = 'BUDDY_INVOICE_REQUEST' //prefix+uuid+amount+memo -const addressRequest = 'BUDDY_ADDRESS_REQUEST' //prefix+uuid+legacy - -const lightningPaymentRequest = 'BUDDY_LIGHTNING_PAYMENT_REQUEST' //prefix+uuid+invoice -const onChainPaymentRequest = 'BUDDY_ON_CHAIN_PAYMENT_REQUEST' //prefix+uuid+addr+amount - -const invoiceResponse = 'BUDDY_INVOICE_RESPONSE' //prefix+uuid+invoice -const addressResponse = 'BUDDY_ADDRESS_RESPONSE' //prefix+uuid+address - -const invoicePaidResponse = 'BUDDY_INVOICE_PAID_RESPONSE' //prefix+uuid+invoice+amount -const addressPaidResponse = 'BUDDY_ADDRESS_PAID_RESPONSE' //prefix+uuid+address+amount - -/* might be useful later with other operations -const successfulResponse = 'BUDDY_SUCCESSFUL_RESPONSE' //prefix+uuid -const failureResponse = 'BUDDY_FAILURE_RESPONSE' //prefix+uuid+reason -*/ -class BuddyManager { - /** - * - * @param {string} gunPub - * @param {boolean} tenant - * @param {string|null} lndPub - */ - addBuddy(gunPub, tenant, lndPub = null) { - if (this._enabledBuddies[gunPub]) { - return - } - - this._enabledBuddies[gunPub] = { - gunPub, - balance: null, - tenant, - lndPub, - latestMessage: '' - } - } - - /** - * @param {Common.Schema.Chat[]} chats - */ - onChatsUpdate(chats) { - chats.forEach(chat => { - const { recipientPublicKey } = chat - if (!this._enabledBuddies[recipientPublicKey]) { - return - } - const buddy = this._enabledBuddies[recipientPublicKey] - if (buddy.balance === null) { - this._enabledBuddies[recipientPublicKey].balance = this.calculateBuddyBalance(messages) - } - const [latestMessage] = chat.messages - if (latestMessage !== buddy.latestMessage) { - - } - - }) - } - - /** - * @type {Record} - */ - _enabledBuddies = {} - - payForBuddy() { - //check balance - //handle payment - //send confirmation/error message - //update balance - } - - receiveForBuddy() { - //create invoice and send to buddy via message - //listen to invoice to be paid - //send confirmation/error message - //update balance - } - - isInvoiceForBuddy() { - - } - - - /** - * - * @param {Common.Schema.ChatMessage[]} messages - * @returns {number|null} - */ - calculateBuddyBalance(messages) { - - } - - /** - * - * @param {Buddy} buddy - * @param {Common.Schema.ChatMessage[]} messages - */ - handleNewMessagesWithBuddy(buddy, messages) { - let tmpIndex = messages.length - if (buddy.latestMessage !== '') { - tmpIndex = messages.indexOf(latestMessage) //should be pretty fast since the messages are in reverse order, so latest message known to manager should be one of the first ones - } - - for (; tmpIndex >= 0; tmpIndex--) { - const maybeRequest = this.processMessage(messages[tmpIndex]) - if (maybeRequest) { - //process request - } - } - - } - - /** - * - * @param {Common.Schema.ChatMessage} message - * @returns {BuddyOperation|null} - */ - processMessage(message) { - if (message.outgoing) {//we only care about incoming messages here - return null - } - if (message.body.startsWith(invoiceRequest)) { - - } - if (message.body.startsWith(addressRequest)) { - - } - if (message.body.startsWith(lightningPaymentRequest)) { - - } - if (message.body.startsWith(onChainPaymentRequest)) { - - } - } - - /** - * @param {BuddyOperation} buddyRequest - */ - processRequest(buddyRequest) { - - } -} \ No newline at end of file From c877c806b4871d550404901699b73a88eb850d08 Mon Sep 17 00:00:00 2001 From: hatim boufnichel Date: Wed, 13 Jan 2021 19:38:02 +0100 Subject: [PATCH 11/18] fixies --- services/gunDB/contact-api/key.js | 2 + services/schema/index.js | 112 +++++++++++++++--------------- src/sockets.js | 11 +++ 3 files changed, 69 insertions(+), 56 deletions(-) diff --git a/services/gunDB/contact-api/key.js b/services/gunDB/contact-api/key.js index 4aaf0174..081ce2a3 100644 --- a/services/gunDB/contact-api/key.js +++ b/services/gunDB/contact-api/key.js @@ -69,3 +69,5 @@ exports.COORDINATES = 'coordinates' exports.COORDINATE_INDEX = 'coordinateIndex' exports.TMP_CHAIN_COORDINATE = 'tmpChainCoordinate' + +exports.DATE_COORDINATE_INDEX = 'dateCoordinateIndex' diff --git a/services/schema/index.js b/services/schema/index.js index 55ed70d4..d786ef7f 100644 --- a/services/schema/index.js +++ b/services/schema/index.js @@ -1,7 +1,6 @@ const Crypto = require('crypto') const { Utils: CommonUtils } = require('shock-common') const getGunUser = () => require('../gunDB/Mediator').getUser() -const SEA = require('../gunDB/Mediator').mySEA const Key = require('../gunDB/contact-api/key') /** * @typedef {import('../gunDB/contact-api/SimpleGUN').ISEA} ISEA @@ -104,10 +103,57 @@ const checkOrderInfo = order => { return null } +/** + * + * @param {CoordinateOrder} orderInfo + * @param {string} coordinateSHA256 + */ +const dateIndexCreateCb = (orderInfo, coordinateSHA256) => { + //if (this.memIndex) { need bind to use this here + //update date memIndex + //} + const date = new Date(orderInfo.timestamp || 0) + //use UTC for consistency? + const year = date.getUTCFullYear().toString() + const month = date.getUTCMonth().toString() + + getGunUser() + .get(Key.DATE_COORDINATE_INDEX) + .get(year) + .get(month) + .set(coordinateSHA256) +} + +/** + * if not provided, assume current month and year + * @param {number|null} year + * @param {number|null} month + */ +const getMonthCoordinates = async (year = null, month = null) => { + const now = Date.now() + //@ts-expect-error + const stringYear = year !== null ? year.toString() : now.getUTCFullYear().toString() + //@ts-expect-error + const stringMonth = month !== null ? month.toString() : now.getUTCMonth().toString() + + const data = await new Promise(res => { + getGunUser() + .get(Key.DATE_COORDINATE_INDEX) + .get(stringYear) + .get(stringMonth) + .load(res) + }) + const coordinatesArray = Object + .values(data) + .filter(coordinateSHA256 => typeof coordinateSHA256 === 'string') + + return coordinatesArray +} + class SchemaManager { - constructor({ memIndex = false }) {//config flag? - this.memIndex = memIndex - this.orderCreateIndexCallbacks.push(this.dateIndexCreateCb) //create more Cbs and put them here for more indexes callbacks + constructor(opts = { memIndex: false }) {//config flag? + this.memIndex = opts.memIndex + this.orderCreateIndexCallbacks.push(dateIndexCreateCb) //create more Cbs and put them here for more indexes callbacks } dateIndexName = 'dateIndex' @@ -133,7 +179,6 @@ class SchemaManager { * @param {CoordinateOrder} orderInfo */ async AddOrder(orderInfo) { - const checkErr = checkOrderInfo(orderInfo) if (checkErr) { throw new Error(checkErr) @@ -160,6 +205,7 @@ class SchemaManager { } const orderString = JSON.stringify(filteredOrder) const mySecret = require('../gunDB/Mediator').getMySecret() + const SEA = require('../gunDB/Mediator').mySEA const encryptedOrderString = await SEA.encrypt(orderString, mySecret) const coordinatePub = filteredOrder.inbound ? filteredOrder.toLndPub : filteredOrder.fromLndPub const coordinate = `${coordinatePub}__${filteredOrder.coordinateIndex}__${filteredOrder.coordinateHash}` @@ -187,54 +233,7 @@ class SchemaManager { this.orderCreateIndexCallbacks.forEach(cb => cb(filteredOrder, coordinateSHA256)) } - /** - * - * @param {CoordinateOrder} orderInfo - * @param {string} coordinateSHA256 - */ - dateIndexCreateCb(orderInfo, coordinateSHA256) { - if (this.memIndex) { - //update date memIndex - } - const date = new Date(orderInfo.timestamp || 0) - //use UTC for consistency? - const year = date.getUTCFullYear().toString() - const month = date.getUTCMonth().toString() - getGunUser() - .get(Key.COORDINATE_INDEX) - .get(this.dateIndexName) - .get(year) - .get(month) - .set(coordinateSHA256) - } - - /** - * if not provided, assume current month and year - * @param {number|null} year - * @param {number|null} month - */ - async getMonthCoordinates(year = null, month = null) { - const now = Date.now() - //@ts-expect-error - const stringYear = year !== null ? year.toString() : now.getUTCFullYear().toString() - //@ts-expect-error - const stringMonth = month !== null ? month.toString() : now.getUTCMonth().toString() - - const data = await new Promise(res => { - getGunUser() - .get(Key.COORDINATE_INDEX) - .get(this.dateIndexName) - .get(stringYear) - .get(stringMonth) - .load(res) - }) - const coordinatesArray = Object - .values(data) - .filter(coordinateSHA256 => typeof coordinateSHA256 === 'string') - - return coordinatesArray - } /** * if not provided, assume current month and year @@ -243,17 +242,15 @@ class SchemaManager { * @returns {Promise} from newer to older */ async getMonthOrders(year = null, month = null) { - const now = Date.now() - //@ts-expect-error + const now = new Date() const intYear = year !== null ? year : now.getUTCFullYear() - //@ts-expect-error const intMonth = month !== null ? month : now.getUTCMonth() let coordinates = null if (this.memIndex) { //get coordinates from this.memDateIndex } else { - coordinates = await this.getMonthCoordinates(intYear, intMonth) + coordinates = await getMonthCoordinates(intYear, intMonth) } /** * @type {CoordinateOrder[]} @@ -271,6 +268,7 @@ class SchemaManager { return } const mySecret = require('../gunDB/Mediator').getMySecret() + const SEA = require('../gunDB/Mediator').mySEA const decryptedString = await SEA.decrypt(encryptedOrderString, mySecret) /** @@ -317,6 +315,7 @@ class SchemaManager { } const orderString = JSON.stringify(filteredOrder) const mySecret = require('../gunDB/Mediator').getMySecret() + const SEA = require('../gunDB/Mediator').mySEA const encryptedOrderString = await SEA.encrypt(orderString, mySecret) const addressSHA256 = Crypto.createHash('SHA256') @@ -364,6 +363,7 @@ class SchemaManager { return false } const mySecret = require('../gunDB/Mediator').getMySecret() + const SEA = require('../gunDB/Mediator').mySEA const decryptedString = await SEA.decrypt(maybeData, mySecret) if (typeof decryptedString !== 'string' || decryptedString === '') { return false diff --git a/src/sockets.js b/src/sockets.js index 44c8b523..236f38fb 100644 --- a/src/sockets.js +++ b/src/sockets.js @@ -134,6 +134,17 @@ module.exports = ( stream.on('data', data => { logger.info('[SOCKET] New invoice data:', data) emitEncryptedEvent({ eventName: 'invoice:new', data, socket }) + if (!data.settled) { + return + } + SchemaManager.AddOrder({ + type: 'invoice', + amount: parseInt(data.amt_paid_sat, 10), + coordinateHash: data.r_hash.toString('hex'), + coordinateIndex: parseInt(data.add_index, 10), + inbound: true, + toLndPub: data.payment_addr + }) }) stream.on('end', () => { logger.info('New invoice stream ended, starting a new one...') From 239a5603f44694243ec2d36d465031e068a39d79 Mon Sep 17 00:00:00 2001 From: hatim boufnichel Date: Sun, 17 Jan 2021 17:07:42 +0100 Subject: [PATCH 12/18] invoice cb --- services/gunDB/contact-api/jobs/onOrders.js | 172 ++++++++++++-------- services/schema/index.js | 50 +++++- 2 files changed, 153 insertions(+), 69 deletions(-) diff --git a/services/gunDB/contact-api/jobs/onOrders.js b/services/gunDB/contact-api/jobs/onOrders.js index b21d6a67..4ab73302 100644 --- a/services/gunDB/contact-api/jobs/onOrders.js +++ b/services/gunDB/contact-api/jobs/onOrders.js @@ -14,7 +14,6 @@ const { } = Common const SchemaManager = require('../../../schema') const LightningServices = require('../../../../utils/lightningServices') -const LNDHealthManager = require('../../../../utils/lightningServices/errors') const Key = require('../key') const Utils = require('../utils') @@ -105,7 +104,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { return } - const listenerStartTime = performance.now() + //const listenerStartTime = performance.now() ordersProcessed.add(orderID) @@ -228,74 +227,113 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { const invoicePutEndTime = performance.now() - invoicePutStartTime - // invoices should be settled right away so we can rely on this single - // subscription instead of life-long all invoices subscription - /** - * @type {string|null} - */ - let maybePostId = null - if (order.targetType === 'post') { - const { postID } = order - maybePostId = postID || null - if (!Common.isPopulatedString(postID)) { - throw new TypeError(`postID not a a populated string`) - } - } - const { r_hash } = invoice - - // A post tip order lifecycle is short enough that we can do it like this. - const stream = LightningServices.invoices.subscribeSingleInvoice({ - r_hash - }) - - /** - * @param {Common.Invoice & {r_hash:Buffer}} invoice - */ - const invoiceSubCb = invoice => { - if (!invoice.settled) { - return - } - if (order.targetType === 'post' && typeof maybePostId === 'string') { - getUser() - .get('postToTipCount') - .get(maybePostId) - .set(null) // each item in the set is a tip - } - const myLndPub = LNDHealthManager.lndPub - const myGunPub = getUser()._.sea.pub - if (!myLndPub) { - return //should never happen but just to be safe - } - SchemaManager.AddOrder({ - amount: parseInt(invoice.amt_paid, 10), - coordinateHash: invoice.r_hash.toString('hex'), - coordinateIndex: parseInt(invoice.add_index, 10), - toLndPub: myLndPub, - inbound: true, - type: 'other', //TODO better this - fromGunPub: order.from, - toGunPub: myGunPub, - invoiceMemo: invoice.memo - }) - } - - stream.on('data', invoiceSubCb) - - stream.on('status', (/** @type {any} */ status) => { - logger.info(`${r_hash}, invoice status:`, status) - }) - stream.on('end', () => { - logger.warn(`${r_hash}, invoice stream ended`) - }) - stream.on('error', (/** @type {any} */ e) => { - logger.warn(`${r_hash}, error:`, e) - }) - logger.info(`[PERF] Added invoice to GunDB in ${invoicePutEndTime}ms`) - const listenerEndTime = performance.now() - listenerStartTime + /** + * + * @type {Common.Schema.InvoiceWhenListed & {r_hash:Buffer,payment_addr:string}} + */ + const paidInvoice = await new Promise(res => { + SchemaManager.addListenInvoice(invoice.r_hash, res) + }) + const hashString = paidInvoice.r_hash.toString('hex') + const { + amt_paid_sat: amt, + add_index: addIndex, + payment_addr: paymentAddr + } = paidInvoice + /**@type {'spontaneousPayment' | 'tip' | 'service' | 'product' | 'other'}*/ + //@ts-expect-error to fix + const orderType = order.targetType + //@ts-expect-error to fix + const { ackInfo } = order //a string representing what has been requested + switch (orderType) { + case 'tip': { + if (!Common.isPopulatedString(ackInfo)) { + throw new Error('ackInfo for postID not a populated string') + } else { + getUser() + .get('postToTipCount') + .get(ackInfo) + .set(null) // each item in the set is a tip + } + break + } + case 'spontaneousPayment': { + //no action required + break + } + case 'product': { + //assuming digital product that only requires to be unlocked + const ackData = { productFinalRef: '' } //find ref by decrypting it base on "ackInfo" provided information + const toSend = JSON.stringify(ackData) + const encrypted = await SEA.encrypt(toSend, secret) + const ordResponse = { + type: 'orderAck', + content: encrypted + } + await new Promise((res, rej) => { + getUser() + .get(Key.ORDER_TO_RESPONSE) + .get(orderID) + .put(ordResponse, ack => { + if (ack.err && typeof ack.err !== 'number') { + rej( + new Error( + `Error saving encrypted orderAck to order to response usergraph: ${ack}` + ) + ) + } else { + res() + } + }) + }) + break + } + case 'service': { + const ackData = { serviceFinalRef: '' } //find ref by decrypting it base on "ackInfo" provided information + const toSend = JSON.stringify(ackData) + const encrypted = await SEA.encrypt(toSend, secret) + const serviceResponse = { + type: 'orderAck', + content: encrypted + } + await new Promise((res, rej) => { + getUser() + .get(Key.ORDER_TO_RESPONSE) + .get(orderID) + .put(serviceResponse, ack => { + if (ack.err && typeof ack.err !== 'number') { + rej( + new Error( + `Error saving encrypted orderAck to order to response usergraph: ${ack}` + ) + ) + } else { + res() + } + }) + }) + break + } + case 'other': //not implemented yet but save them as a coordinate anyways + break + default: + return //exit because not implemented + } + const myGunPub = getUser()._.sea.pub + SchemaManager.AddOrder({ + type: orderType, + coordinateHash: hashString, + coordinateIndex: parseInt(addIndex, 10), + inbound: true, + amount: parseInt(amt, 10), - logger.info(`[PERF] Invoice generation completed in ${listenerEndTime}ms`) + toLndPub: paymentAddr, + fromGunPub: order.from, + toGunPub: myGunPub, + invoiceMemo: memo + }) } catch (err) { logger.error( `error inside onOrders, orderAddr: ${addr}, orderID: ${orderID}, order: ${JSON.stringify( diff --git a/services/schema/index.js b/services/schema/index.js index d786ef7f..64ea82f8 100644 --- a/services/schema/index.js +++ b/services/schema/index.js @@ -1,5 +1,5 @@ const Crypto = require('crypto') -const { Utils: CommonUtils } = require('shock-common') +const Common = require('shock-common') const getGunUser = () => require('../gunDB/Mediator').getUser() const Key = require('../gunDB/contact-api/key') /** @@ -259,7 +259,7 @@ class SchemaManager { if (!coordinates) { return orders } - await CommonUtils.asyncForEach(coordinates, async coordinateSHA256 => { + await Common.Utils.asyncForEach(coordinates, async coordinateSHA256 => { const encryptedOrderString = await getGunUser() .get(Key.COORDINATES) .get(coordinateSHA256) @@ -282,6 +282,52 @@ class SchemaManager { return orderedOrders } + /** + * @typedef {Common.Schema.InvoiceWhenListed & {r_hash:Buffer,payment_addr:string}} Invoice + */ + /** + * @type {Recordvoid>} + */ + _listeningInvoices = {} + + /** + * + * @param {Buffer} r_hash + * @param {(invoice:Invoice) =>void} done + */ + addListenInvoice(r_hash, done) { + const hashString = r_hash.toString("hex") + this._listeningInvoices[hashString] = done + } + + /** + * + * @param {Common.Schema.InvoiceWhenListed & {r_hash:Buffer,payment_addr:string}} data + */ + invoiceStreamDataCb(data) { + if (!data.settled) { + //invoice not paid yet + return + } + const hashString = data.r_hash.toString('hex') + const amt = parseInt(data.amt_paid_sat, 10) + if (this._listeningInvoices[hashString]) { + const done = this._listeningInvoices[hashString] + delete this._listeningInvoices[hashString] + done(data) + } else { + this.AddOrder({ + type: 'invoice', + coordinateHash: hashString, + coordinateIndex: parseInt(data.add_index, 10), + inbound: true, + amount: amt, + toLndPub: data.payment_addr, + invoiceMemo: data.memo + }) + } + } + /** * * @param {string} address From 4681f2279788380ac4bceed768829d5790dc6438 Mon Sep 17 00:00:00 2001 From: hatim boufnichel Date: Tue, 19 Jan 2021 18:42:02 +0100 Subject: [PATCH 13/18] order ack --- .env.example | 4 +- package.json | 5 +- services/gunDB/contact-api/actions.js | 117 +++++-- services/gunDB/contact-api/jobs/onOrders.js | 83 ++++- services/schema/index.js | 361 +++++++++++++++----- src/sockets.js | 3 +- utils/lightningServices/v2.js | 64 +++- yarn.lock | 10 +- 8 files changed, 517 insertions(+), 130 deletions(-) diff --git a/.env.example b/.env.example index 1e0d824d..83e2ce68 100644 --- a/.env.example +++ b/.env.example @@ -4,4 +4,6 @@ MS_TO_TOKEN_EXPIRATION=4500000 DISABLE_SHOCK_ENCRYPTION=false CACHE_HEADERS_MANDATORY=true SHOCK_CACHE=true -TRUSTED_KEYS=true \ No newline at end of file +TRUSTED_KEYS=true +TORRENT_SEED_URL=https://webtorrent.shock.network +TORRENT_SEED_TOKEN=jibberish \ No newline at end of file diff --git a/package.json b/package.json index f9d81a67..75f523a5 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "localtunnel": "^1.9.0", "lodash": "^4.17.20", "method-override": "^2.3.7", + "node-fetch": "^2.6.1", "node-persist": "^3.1.0", "promise": "^8.1.0", "ramda": "^0.27.1", @@ -50,7 +51,7 @@ "request-promise": "^4.2.6", "response-time": "^2.3.2", "shelljs": "^0.8.2", - "shock-common": "29.1.0", + "shock-common": "31.1.0", "socket.io": "2.1.1", "text-encoding": "^0.7.0", "tingodb": "^0.6.1", @@ -98,4 +99,4 @@ "pre-commit": "yarn lint && yarn typecheck && yarn lint-staged" } } -} +} \ No newline at end of file diff --git a/services/gunDB/contact-api/actions.js b/services/gunDB/contact-api/actions.js index ad3acd7c..7d81b5ac 100644 --- a/services/gunDB/contact-api/actions.js +++ b/services/gunDB/contact-api/actions.js @@ -21,6 +21,8 @@ const { const Getters = require('./getters') const Key = require('./key') const Utils = require('./utils') +const SchemaManager = require('../../schema') +const LNDHealthMananger = require('../../../utils/lightningServices/errors') /** * @typedef {import('./SimpleGUN').GUNNode} GUNNode @@ -923,7 +925,11 @@ const sendHRWithInitialMsg = async ( * @prop {Common.Schema.OrderTargetType} type * @prop {string=} postID */ - +/** + * @typedef {object} OrderRes + * @prop {PaymentV2} payment + * @prop {object=} orderAck + */ /** * Returns the preimage corresponding to the payment. * @param {string} to @@ -933,14 +939,14 @@ const sendHRWithInitialMsg = async ( * @param {SpontPaymentOptions} opts * @throws {Error} If no response in less than 20 seconds from the recipient, or * lightning cannot find a route for the payment. - * @returns {Promise} The payment's preimage. + * @returns {Promise} The payment's preimage. */ const sendSpontaneousPayment = async ( to, amount, memo, feeLimit, - opts = { type: 'user' } + opts = { type: 'spontaneousPayment' } ) => { try { const SEA = require('../Mediator').mySEA @@ -965,8 +971,8 @@ const sendSpontaneousPayment = async ( targetType: opts.type } - if (opts.type === 'post') { - order.postID = opts.postID + if (opts.type === 'tip') { + order.ackInfo = opts.postID } logger.info(JSON.stringify(order)) @@ -1073,17 +1079,74 @@ const sendSpontaneousPayment = async ( feeLimit, payment_request: orderResponse.response }) - - const coordinate = 'lnPub + invoiceIndex + payment hash(?)' //.... - const orderData = { - someInfo: 'info ' + const myLndPub = LNDHealthMananger.lndPub + if (opts.type !== 'contentReveal' && opts.type !== 'torrentSeed') { + SchemaManager.AddOrder({ + type: opts.type, + amount: parseInt(payment.value_sat, 10), + coordinateHash: payment.payment_hash, + coordinateIndex: parseInt(payment.payment_index, 10), + fromLndPub: myLndPub || undefined, + inbound: false, + fromGunPub: getUser()._.sea.pub, + toGunPub: to, + invoiceMemo: memo + }) + return { payment } } - getUser() - .get('orders') - .get(coordinate) - .set(orderData) + /** @type {import('shock-common').Schema.OrderResponse} */ + const encryptedOrderAckRes = await Utils.tryAndWait( + gun => + new Promise(res => { + gun + .user(to) + .get(Key.ORDER_TO_RESPONSE) + .get(orderID) + .on(orderResponse => { + if (Schema.isOrderResponse(orderResponse)) { + res(orderResponse) + } + }) + }), + v => !Schema.isOrderResponse(v) + ) - return payment + if (!Schema.isOrderResponse(encryptedOrderAckRes)) { + const e = TypeError( + `Expected OrderResponse got: ${typeof encryptedOrderAckRes}` + ) + logger.error(e) + throw e + } + + /** @type {import('shock-common').Schema.OrderResponse} */ + const orderAck = { + response: await SEA.decrypt(encryptedOrderAckRes.response, ourSecret), + type: encryptedOrderAckRes.type + } + + logger.info('decoded encryptedOrderAck: ' + JSON.stringify(orderAck)) + + if (orderAck.type === 'err') { + throw new Error(orderAck.response) + } + + if (orderAck.type !== 'orderAck') { + throw new Error(`expected orderAck response, got: ${orderAck.type}`) + } + SchemaManager.AddOrder({ + type: opts.type, + amount: parseInt(payment.value_sat, 10), + coordinateHash: payment.payment_hash, + coordinateIndex: parseInt(payment.payment_index, 10), + fromLndPub: myLndPub || undefined, + inbound: false, + fromGunPub: getUser()._.sea.pub, + toGunPub: to, + invoiceMemo: memo, + metadata: JSON.stringify(orderAck) + }) + return { payment, orderAck } } catch (e) { logger.error('Error inside sendPayment()') logger.error(e) @@ -1102,8 +1165,8 @@ const sendSpontaneousPayment = async ( * @returns {Promise} The payment's preimage. */ const sendPayment = async (to, amount, memo, feeLimit) => { - const payment = await sendSpontaneousPayment(to, amount, memo, feeLimit) - return payment.payment_preimage + const res = await sendSpontaneousPayment(to, amount, memo, feeLimit) + return res.payment.payment_preimage } /** @@ -1274,9 +1337,10 @@ const setLastSeenApp = () => * @param {string[]} tags * @param {string} title * @param {Common.Schema.ContentItem[]} content + * @param {ISEA} SEA * @returns {Promise<[string, Common.Schema.RawPost]>} */ -const createPostNew = async (tags, title, content) => { +const createPostNew = async (tags, title, content, SEA) => { /** @type {Common.Schema.RawPost} */ const newPost = { date: Date.now(), @@ -1285,11 +1349,19 @@ const createPostNew = async (tags, title, content) => { title, contentItems: {} } - - content.forEach(c => { + const mySecret = require('../Mediator').getMySecret() + await Common.Utils.asyncForEach(content, async c => { + const cBis = c + if ( + (cBis.type === 'image/embedded' || cBis.type === 'video/embedded') && + cBis.isPrivate + ) { + const encryptedMagnet = await SEA.encrypt(cBis.magnetURI, mySecret) + cBis.magnetURI = encryptedMagnet + } // @ts-expect-error const uuid = Gun.text.random() - newPost.contentItems[uuid] = c + newPost.contentItems[uuid] = cBis }) /** @type {string} */ @@ -1317,9 +1389,10 @@ const createPostNew = async (tags, title, content) => { * @param {string[]} tags * @param {string} title * @param {Common.Schema.ContentItem[]} content + * @param {ISEA} SEA * @returns {Promise} */ -const createPost = async (tags, title, content) => { +const createPost = async (tags, title, content, SEA) => { if (content.length === 0) { throw new Error(`A post must contain at least one paragraph/image/video`) } @@ -1396,7 +1469,7 @@ const createPost = async (tags, title, content) => { ) }) - const [postID, newPost] = await createPostNew(tags, title, content) + const [postID, newPost] = await createPostNew(tags, title, content, SEA) await Common.makePromise((res, rej) => { require('../Mediator') diff --git a/services/gunDB/contact-api/jobs/onOrders.js b/services/gunDB/contact-api/jobs/onOrders.js index 4ab73302..fc3485a7 100644 --- a/services/gunDB/contact-api/jobs/onOrders.js +++ b/services/gunDB/contact-api/jobs/onOrders.js @@ -8,6 +8,9 @@ const isFinite = require('lodash/isFinite') const isNumber = require('lodash/isNumber') const isNaN = require('lodash/isNaN') const Common = require('shock-common') +const crypto = require('crypto') +// @ts-expect-error TODO fix this +const fetch = require('node-fetch') const { Constants: { ErrorCode }, Schema @@ -242,30 +245,61 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { add_index: addIndex, payment_addr: paymentAddr } = paidInvoice - /**@type {'spontaneousPayment' | 'tip' | 'service' | 'product' | 'other'}*/ - //@ts-expect-error to fix const orderType = order.targetType - //@ts-expect-error to fix const { ackInfo } = order //a string representing what has been requested switch (orderType) { case 'tip': { - if (!Common.isPopulatedString(ackInfo)) { - throw new Error('ackInfo for postID not a populated string') - } else { - getUser() - .get('postToTipCount') - .get(ackInfo) - .set(null) // each item in the set is a tip + const postID = ackInfo + if (!Common.isPopulatedString(postID)) { + break //create the coordinate, but stop because of the invalid id } + getUser() + .get('postToTipCount') + .get(postID) + .set(null) // each item in the set is a tip break } case 'spontaneousPayment': { //no action required break } - case 'product': { + case 'contentReveal': { //assuming digital product that only requires to be unlocked - const ackData = { productFinalRef: '' } //find ref by decrypting it base on "ackInfo" provided information + const postID = ackInfo + if (!Common.isPopulatedString(postID)) { + break //create the coordinate, but stop because of the invalid id + } + const selectedPost = await new Promise(res => { + getUser() + .get(Key.POSTS_NEW) + .get(postID) + .load(res) + }) + if (!Common.Schema.isPost(selectedPost)) { + break //create the coordinate, but stop because of the invalid post + } + /** + * @type {Record} + */ + const contentsToSend = {} + const mySecret = require('../../Mediator').getMySecret() + await Common.Utils.asyncForEach( + Object.entries(selectedPost.contentItems), + async ([contentID, item]) => { + if ( + item.type !== 'image/embedded' && + item.type !== 'video/embedded' + ) { + return //only visual content can be private + } + if (!item.isPrivate) { + return + } + const decrypted = await SEA.decrypt(item.magnetURI, mySecret) + contentsToSend[contentID] = decrypted + } + ) + const ackData = { unlockedContents: contentsToSend } const toSend = JSON.stringify(ackData) const encrypted = await SEA.encrypt(toSend, secret) const ordResponse = { @@ -290,8 +324,29 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { }) break } - case 'service': { - const ackData = { serviceFinalRef: '' } //find ref by decrypting it base on "ackInfo" provided information + case 'torrentSeed': { + const seedUrl = process.env.TORRENT_SEED_URL + const seedToken = process.env.TORRENT_SEED_TOKEN + if (!seedUrl || !seedToken) { + break //service not available + } + const token = crypto.randomBytes(32).toString('hex') + const reqData = { + seed_token: seedToken, + wallet_token: token + } + const res = await fetch(`${seedUrl}/api/enroll_token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(reqData) + }) + if (res.ok) { + break //request didnt work, save coordinate anyway + } + + const ackData = { seedUrl, token } const toSend = JSON.stringify(ackData) const encrypted = await SEA.encrypt(toSend, secret) const serviceResponse = { diff --git a/services/schema/index.js b/services/schema/index.js index 64ea82f8..786b5ab0 100644 --- a/services/schema/index.js +++ b/services/schema/index.js @@ -1,10 +1,13 @@ const Crypto = require('crypto') +const logger = require('winston') const Common = require('shock-common') const getGunUser = () => require('../gunDB/Mediator').getUser() +const isAuthenticated = () => require('../gunDB/Mediator').isAuthenticated() const Key = require('../gunDB/contact-api/key') +const lndV2 = require('../../utils/lightningServices/v2') /** * @typedef {import('../gunDB/contact-api/SimpleGUN').ISEA} ISEA - * @typedef { 'spontaneousPayment' | 'tip' | 'service' | 'product' | 'other'|'invoice'|'payment'|'chainTx' } OrderType + * @typedef { 'spontaneousPayment' | 'tip' | 'torrentSeed' | 'contentReveal' | 'other'|'invoice'|'payment'|'chainTx' } OrderType * * This represents a settled order only, unsettled orders have no coordinate * @typedef {object} CoordinateOrder //everything is optional for different types @@ -150,6 +153,172 @@ const getMonthCoordinates = async (year = null, month = null) => { return coordinatesArray } +/** + * + * @param {string|undefined} address + * @param {CoordinateOrder} orderInfo + */ +const AddTmpChainOrder = async (address, orderInfo) => { + if (!address) { + throw new Error("invalid address passed to AddTmpChainOrder") + } + if (!orderInfo.toBtcPub) { + throw new Error("invalid toBtcPub passed to AddTmpChainOrder") + } + const checkErr = checkOrderInfo(orderInfo) + if (checkErr) { + throw new Error(checkErr) + } + + /** + * @type {CoordinateOrder} + */ + const filteredOrder = { + fromLndPub: orderInfo.fromLndPub, + toLndPub: orderInfo.toLndPub, + fromGunPub: orderInfo.fromGunPub, + toGunPub: orderInfo.toGunPub, + inbound: orderInfo.inbound, + ownerGunPub: orderInfo.ownerGunPub, + coordinateIndex: orderInfo.coordinateIndex, + coordinateHash: orderInfo.coordinateHash, + type: orderInfo.type, + amount: orderInfo.amount, + description: orderInfo.description, + metadata: orderInfo.metadata, + + timestamp: orderInfo.timestamp || Date.now(), + } + const orderString = JSON.stringify(filteredOrder) + const mySecret = require('../gunDB/Mediator').getMySecret() + const SEA = require('../gunDB/Mediator').mySEA + const encryptedOrderString = await SEA.encrypt(orderString, mySecret) + + const addressSHA256 = Crypto.createHash('SHA256') + .update(address) + .digest('hex') + + await new Promise((res, rej) => { + getGunUser() + .get(Key.TMP_CHAIN_COORDINATE) + .get(addressSHA256) + .put(encryptedOrderString, ack => { + if (ack.err && typeof ack.err !== 'number') { + rej( + new Error( + `Error saving tmp chain coordinate order to user-graph: ${ack}` + ) + ) + } else { + res(null) + } + }) + }) +} + +/** + * + * @param {string} address + * @returns {Promise} + */ +const isTmpChainOrder = async (address) => { + if (typeof address !== 'string' || address === '') { + return false + } + const addressSHA256 = Crypto.createHash('SHA256') + .update(address) + .digest('hex') + + const maybeData = await getGunUser() + .get(Key.TMP_CHAIN_COORDINATE) + .get(addressSHA256) + .then() + + if (typeof maybeData !== 'string' || maybeData === '') { + return false + } + const mySecret = require('../gunDB/Mediator').getMySecret() + const SEA = require('../gunDB/Mediator').mySEA + const decryptedString = await SEA.decrypt(maybeData, mySecret) + if (typeof decryptedString !== 'string' || decryptedString === '') { + return false + } + + const tmpOrder = JSON.parse(decryptedString) + const checkErr = checkOrderInfo(tmpOrder) + if (checkErr) { + return false + } + + return tmpOrder + +} + +/** + * @param {string} address + */ +const clearTmpChainOrder = async (address) => { + if (typeof address !== 'string' || address === '') { + return + } + const addressSHA256 = Crypto.createHash('SHA256') + .update(address) + .digest('hex') + + await new Promise((res, rej) => { + getGunUser() + .get(Key.TMP_CHAIN_COORDINATE) + .get(addressSHA256) + .put(null, ack => { + if (ack.err && typeof ack.err !== 'number') { + rej( + new Error( + `Error nulling tmp chain coordinate order to user-graph: ${ack}` + ) + ) + } else { + res(null) + } + }) + }) +} + +/** + * @param {Common.Schema.ChainTransaction} tx + * @param {CoordinateOrder|false| undefined} order + */ +const handleUnconfirmedTx = (tx, order) => { + const { tx_hash } = tx + const amountInt = parseInt(tx.amount, 10) + if (order) { + /*if an order already exists, update the order data + if an unconfirmed transaction has a tmp order already + it means the address was generated by shockAPI, or the tx was sent by shockAPI*/ + const orderUpdate = order + orderUpdate.amount = Math.abs(amountInt) + orderUpdate.inbound = amountInt > 0 + /*tmp coordinate does not have a coordinate hash until the transaction is created, + before it will contain 'unknown' */ + orderUpdate.coordinateHash = tx_hash + /*update the order data, + provides a notification when the TX enters the mempool */ + AddTmpChainOrder(orderUpdate.toBtcPub, orderUpdate) + } else { + /*if an order does not exist, create the tmp order, + and use tx_hash as key. + this means the address was NOT generated by shockAPI, or the tx was NOT sent by shockAPI */ + AddTmpChainOrder(tx_hash, { + type: 'chainTx', + amount: Math.abs(amountInt), + coordinateHash: tx_hash, + coordinateIndex: 0, //coordinate index is 0 until the tx is confirmed and the block is known + inbound: amountInt > 0, + toBtcPub: 'unknown' + }) + } + +} + class SchemaManager { constructor(opts = { memIndex: false }) {//config flag? this.memIndex = opts.memIndex @@ -328,104 +497,128 @@ class SchemaManager { } } + + + + + + /** - * - * @param {string} address - * @param {CoordinateOrder} orderInfo + * @type {Record} + * lnd fires a confirmed transaction event TWICE, let's make sure it is only managed ONCE */ - //eslint-disable-next-line class-methods-use-this - async AddTmpChainOrder(address, orderInfo) { - const checkErr = checkOrderInfo(orderInfo) - if (checkErr) { - throw new Error(checkErr) + _confirmedTransactions = {} + + /** + * @param {Common.Schema.ChainTransaction} data + */ + async transactionStreamDataCb(data) { + const { num_confirmations } = data + const responses = await Promise.all(data.dest_addresses.map(isTmpChainOrder)) + const hasOrder = responses.find(res => res !== false) + + if (num_confirmations === 0) { + handleUnconfirmedTx(data, hasOrder) + } else { + this.handleConfirmedTx(data, hasOrder) } - - /** - * @type {CoordinateOrder} - */ - const filteredOrder = { - fromLndPub: orderInfo.fromLndPub, - toLndPub: orderInfo.toLndPub, - fromGunPub: orderInfo.fromGunPub, - toGunPub: orderInfo.toGunPub, - inbound: orderInfo.inbound, - ownerGunPub: orderInfo.ownerGunPub, - coordinateIndex: orderInfo.coordinateIndex, - coordinateHash: orderInfo.coordinateHash, - type: orderInfo.type, - amount: orderInfo.amount, - description: orderInfo.description, - metadata: orderInfo.metadata, - - timestamp: orderInfo.timestamp || Date.now(), - } - const orderString = JSON.stringify(filteredOrder) - const mySecret = require('../gunDB/Mediator').getMySecret() - const SEA = require('../gunDB/Mediator').mySEA - const encryptedOrderString = await SEA.encrypt(orderString, mySecret) - - const addressSHA256 = Crypto.createHash('SHA256') - .update(address) - .digest('hex') - - await new Promise((res, rej) => { - getGunUser() - .get(Key.TMP_CHAIN_COORDINATE) - .get(addressSHA256) - .put(encryptedOrderString, ack => { - if (ack.err && typeof ack.err !== 'number') { - rej( - new Error( - `Error saving tmp chain coordinate order to user-graph: ${ack}` - ) - ) - } else { - res(null) - } - }) - }) } + + /** * - * @param {string} address - * @returns {Promise} + * @param {Common.Schema.ChainTransaction} tx + * @param {CoordinateOrder|false| undefined} order */ - //eslint-disable-next-line class-methods-use-this - async isTmpChainOrder(address) { - if (typeof address !== 'string' || address === '') { - return false + handleConfirmedTx(tx, order) { + const { tx_hash } = tx + if (this._confirmedTransactions[tx_hash]) { + //this tx confirmation was already handled + return } - const addressSHA256 = Crypto.createHash('SHA256') - .update(address) - .digest('hex') - - const maybeData = await getGunUser() - .get(Key.TMP_CHAIN_COORDINATE) - .get(addressSHA256) - .then() - - if (typeof maybeData !== 'string' || maybeData === '') { - return false + if (!order) { + /*confirmed transaction MUST have a tmp order, + if not, means something gone wrong */ + logger.error('found a confirmed transaction that does not have a tmp order!!') + return } - const mySecret = require('../gunDB/Mediator').getMySecret() - const SEA = require('../gunDB/Mediator').mySEA - const decryptedString = await SEA.decrypt(maybeData, mySecret) - if (typeof decryptedString !== 'string' || decryptedString === '') { - return false + if (!order.toBtcPub) { + /*confirmed transaction tmp order MUST have a non null toBtcPub */ + logger.error('found a confirmed transaction that does not have toBtcPub in the order!!') + return } - - const tmpOrder = JSON.parse(decryptedString) - const checkErr = checkOrderInfo(tmpOrder) - if (checkErr) { - return false + const finalOrder = order + finalOrder.coordinateIndex = tx.block_height + this.AddOrder(finalOrder) + if (order.toBtcPub === 'unknown') { + clearTmpChainOrder(tx_hash) + } else { + clearTmpChainOrder(order.toBtcPub) } - - return tmpOrder - + this._confirmedTransactions[tx_hash] = true } } const Manager = new SchemaManager() +/*invoice stream, +this is the only place where it's needed, +everything is a coordinate now*/ +let InvoiceShouldRetry = true +setInterval(() => { + if (!InvoiceShouldRetry) { + return + } + if (!isAuthenticated()) { + return + } + InvoiceShouldRetry = false + + lndV2.subscribeInvoices( + invoice => { + if (!isAuthenticated) { + logger.error("got an invoice while not authenticated, will ignore it and cancel the stream") + return true + } + Manager.invoiceStreamDataCb(invoice) + return false + }, + error => { + logger.error(`Error in invoices sub, will retry in two second, reason: ${error.reason}`) + InvoiceShouldRetry = true + } + ) + +}, 2000) + +/*transactions stream, +this is the only place where it's needed, +everything is a coordinate now*/ +let TransactionShouldRetry = true +setInterval(() => { + if (!TransactionShouldRetry) { + return + } + if (!isAuthenticated()) { + return + } + TransactionShouldRetry = false + + lndV2.subscribeTransactions( + transaction => { + if (!isAuthenticated) { + logger.error("got a transaction while not authenticated, will ignore it and cancel the stream") + return true + } + Manager.transactionStreamDataCb(transaction) + return false + }, + error => { + logger.error(`Error in transaction sub, will retry in two second, reason: ${error.reason}`) + TransactionShouldRetry = true + } + ) + +}, 2000) module.exports = Manager \ No newline at end of file diff --git a/src/sockets.js b/src/sockets.js index 236f38fb..353435f2 100644 --- a/src/sockets.js +++ b/src/sockets.js @@ -298,13 +298,14 @@ module.exports = ( const subID = Math.floor(Math.random() * 1000).toString() const isNotifications = isNotificationsSocket ? 'notifications' : '' logger.info('[LND] New LND Socket created:' + isNotifications + subID) + /* not used by wallet anymore const cancelInvoiceStream = onNewInvoice(socket, subID) const cancelTransactionStream = onNewTransaction(socket, subID) socket.on('disconnect', () => { logger.info('LND socket disconnected:' + isNotifications + subID) cancelInvoiceStream() cancelTransactionStream() - }) + })*/ } }) diff --git a/utils/lightningServices/v2.js b/utils/lightningServices/v2.js index b933c63b..e54e3ee4 100644 --- a/utils/lightningServices/v2.js +++ b/utils/lightningServices/v2.js @@ -571,6 +571,66 @@ const addInvoice = (value, memo = '', confidential = true, expiry = 180) => ) }) +/** + * @typedef {object} lndErr + * @prop {string} reason + * @prop {number} code + * + */ +/** + * @param {(invoice:Common.Schema.InvoiceWhenListed & {r_hash:Buffer,payment_addr:string}) => (boolean | undefined)} dataCb + * @param {(error:lndErr) => void} errorCb + */ +const subscribeInvoices = (dataCb, errorCb) => { + const { lightning } = lightningServices.getServices() + const stream = lightning.subscribeInvoices({}) + stream.on('data', invoice => { + const cancelStream = dataCb(invoice) + if (cancelStream) { + //@ts-expect-error + stream.cancel() + } + }) + stream.on('error', error => { + errorCb(error) + try { + //@ts-expect-error + stream.cancel() + } catch { + logger.info( + '[subscribeInvoices] tried to cancel an already canceled stream' + ) + } + }) +} + +/** + * @param {(tx:Common.Schema.ChainTransaction) => (boolean | undefined)} dataCb + * @param {(error:lndErr) => void} errorCb + */ +const subscribeTransactions = (dataCb, errorCb) => { + const { lightning } = lightningServices.getServices() + const stream = lightning.subscribeTransactions({}) + stream.on('data', transaction => { + const cancelStream = dataCb(transaction) + if (cancelStream) { + //@ts-expect-error + stream.cancel() + } + }) + stream.on('error', error => { + errorCb(error) + try { + //@ts-expect-error + stream.cancel() + } catch { + logger.info( + '[subscribeTransactions] tried to cancel an already canceled stream' + ) + } + }) +} + module.exports = { sendPaymentV2Keysend, sendPaymentV2Invoice, @@ -582,5 +642,7 @@ module.exports = { getChanInfo, listPeers, pendingChannels, - addInvoice + addInvoice, + subscribeInvoices, + subscribeTransactions } diff --git a/yarn.lock b/yarn.lock index daa7f4e4..beb3a3bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4966,7 +4966,7 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -node-fetch@^2.3.0: +node-fetch@^2.3.0, node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== @@ -6255,10 +6255,10 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== -shock-common@29.1.0: - version "29.1.0" - resolved "https://registry.yarnpkg.com/shock-common/-/shock-common-29.1.0.tgz#3b6d8613fb7c73b8b76c98293a14ec168a9dc888" - integrity sha512-O2tK+TShF3ioAdP4K33MB5QUDTmMqzz+pZe/HnSbi9q1DyX/zQ2Uluzol1NDE/6Z2SSnVFA7/2vJKGaCEdMKoQ== +shock-common@31.1.0: + version "31.1.0" + resolved "https://registry.yarnpkg.com/shock-common/-/shock-common-31.1.0.tgz#9c8f25d0d405a9a9c52849c2d96452c5ddd17267" + integrity sha512-1490v3gTY5ZNEB/Lelfix+6bI4mfFE8hVrtN4ijz0aj/Cl1ZP5ATKdYO+hffReI+4yDaPSAAWd/HYk9b497Kxw== dependencies: immer "^6.0.6" lodash "^4.17.19" From 9f4a0b05d21c093451b259a64da6eb677c8f71e9 Mon Sep 17 00:00:00 2001 From: hatim boufnichel Date: Thu, 21 Jan 2021 17:30:12 +0100 Subject: [PATCH 14/18] working order ack --- services/gunDB/contact-api/actions.js | 38 ++- services/gunDB/contact-api/jobs/onOrders.js | 349 +++++++++++--------- services/schema/index.js | 1 + src/routes.js | 25 +- 4 files changed, 242 insertions(+), 171 deletions(-) diff --git a/services/gunDB/contact-api/actions.js b/services/gunDB/contact-api/actions.js index 7d81b5ac..6f234e10 100644 --- a/services/gunDB/contact-api/actions.js +++ b/services/gunDB/contact-api/actions.js @@ -923,7 +923,7 @@ const sendHRWithInitialMsg = async ( /** * @typedef {object} SpontPaymentOptions * @prop {Common.Schema.OrderTargetType} type - * @prop {string=} postID + * @prop {string=} ackInfo */ /** * @typedef {object} OrderRes @@ -968,11 +968,8 @@ const sendSpontaneousPayment = async ( from: getUser()._.sea.pub, memo: memo || 'no memo', timestamp: Date.now(), - targetType: opts.type - } - - if (opts.type === 'tip') { - order.ackInfo = opts.postID + targetType: opts.type, + ackInfo: opts.ackInfo } logger.info(JSON.stringify(order)) @@ -1009,7 +1006,7 @@ const sendSpontaneousPayment = async ( ) ) } else { - res(ord._.get) + setTimeout(() => res(ord._.get), 0) } }) }) @@ -1020,7 +1017,8 @@ const sendSpontaneousPayment = async ( )}` throw new Error(msg) } - + console.log('ORDER ID') + console.log(orderID) /** @type {import('shock-common').Schema.OrderResponse} */ const encryptedOrderRes = await Utils.tryAndWait( gun => @@ -1030,12 +1028,13 @@ const sendSpontaneousPayment = async ( .get(Key.ORDER_TO_RESPONSE) .get(orderID) .on(orderResponse => { + console.log(orderResponse) if (Schema.isOrderResponse(orderResponse)) { res(orderResponse) } }) }), - v => !Schema.isOrderResponse(v) + v => Schema.isOrderResponse(v) ) if (!Schema.isOrderResponse(encryptedOrderRes)) { @@ -1046,10 +1045,12 @@ const sendSpontaneousPayment = async ( throw e } - /** @type {import('shock-common').Schema.OrderResponse} */ + /** @type {import('shock-common').Schema.OrderResponse &{ackNode:string}} */ const orderResponse = { response: await SEA.decrypt(encryptedOrderRes.response, ourSecret), - type: encryptedOrderRes.type + type: encryptedOrderRes.type, + //@ts-expect-error + ackNode: encryptedOrderRes.ackNode } logger.info('decoded orderResponse: ' + JSON.stringify(orderResponse)) @@ -1080,7 +1081,10 @@ const sendSpontaneousPayment = async ( payment_request: orderResponse.response }) const myLndPub = LNDHealthMananger.lndPub - if (opts.type !== 'contentReveal' && opts.type !== 'torrentSeed') { + if ( + (opts.type !== 'contentReveal' && opts.type !== 'torrentSeed') || + !orderResponse.ackNode + ) { SchemaManager.AddOrder({ type: opts.type, amount: parseInt(payment.value_sat, 10), @@ -1094,6 +1098,8 @@ const sendSpontaneousPayment = async ( }) return { payment } } + console.log('ACK NODE') + console.log(orderResponse.ackNode) /** @type {import('shock-common').Schema.OrderResponse} */ const encryptedOrderAckRes = await Utils.tryAndWait( gun => @@ -1101,8 +1107,11 @@ const sendSpontaneousPayment = async ( gun .user(to) .get(Key.ORDER_TO_RESPONSE) - .get(orderID) + .get(orderResponse.ackNode) .on(orderResponse => { + console.log(orderResponse) + console.log(Schema.isOrderResponse(orderResponse)) + if (Schema.isOrderResponse(orderResponse)) { res(orderResponse) } @@ -1113,7 +1122,7 @@ const sendSpontaneousPayment = async ( if (!Schema.isOrderResponse(encryptedOrderAckRes)) { const e = TypeError( - `Expected OrderResponse got: ${typeof encryptedOrderAckRes}` + `Expected encryptedOrderAckRes got: ${typeof encryptedOrderAckRes}` ) logger.error(e) throw e @@ -1148,6 +1157,7 @@ const sendSpontaneousPayment = async ( }) return { payment, orderAck } } catch (e) { + console.log(e) logger.error('Error inside sendPayment()') logger.error(e) throw e diff --git a/services/gunDB/contact-api/jobs/onOrders.js b/services/gunDB/contact-api/jobs/onOrders.js index fc3485a7..63b9914e 100644 --- a/services/gunDB/contact-api/jobs/onOrders.js +++ b/services/gunDB/contact-api/jobs/onOrders.js @@ -2,6 +2,7 @@ * @format */ // @ts-check +const Gun = require('gun') const { performance } = require('perf_hooks') const logger = require('winston') const isFinite = require('lodash/isFinite') @@ -201,11 +202,15 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { logger.info( `onOrders() -> Will now place the encrypted invoice in order to response usergraph: ${addr}` ) + // @ts-expect-error + const ackNode = Gun.text.random() /** @type {import('shock-common').Schema.OrderResponse} */ const orderResponse = { response: encInvoice, - type: 'invoice' + type: 'invoice', + //@ts-expect-error + ackNode } const invoicePutStartTime = performance.now() @@ -231,164 +236,212 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { const invoicePutEndTime = performance.now() - invoicePutStartTime logger.info(`[PERF] Added invoice to GunDB in ${invoicePutEndTime}ms`) - /** * - * @type {Common.Schema.InvoiceWhenListed & {r_hash:Buffer,payment_addr:string}} + * @param {Common.Schema.InvoiceWhenListed & {r_hash:Buffer,payment_addr:string}} paidInvoice */ - const paidInvoice = await new Promise(res => { - SchemaManager.addListenInvoice(invoice.r_hash, res) - }) - const hashString = paidInvoice.r_hash.toString('hex') - const { - amt_paid_sat: amt, - add_index: addIndex, - payment_addr: paymentAddr - } = paidInvoice - const orderType = order.targetType - const { ackInfo } = order //a string representing what has been requested - switch (orderType) { - case 'tip': { - const postID = ackInfo - if (!Common.isPopulatedString(postID)) { - break //create the coordinate, but stop because of the invalid id - } - getUser() - .get('postToTipCount') - .get(postID) - .set(null) // each item in the set is a tip - break - } - case 'spontaneousPayment': { - //no action required - break - } - case 'contentReveal': { - //assuming digital product that only requires to be unlocked - const postID = ackInfo - if (!Common.isPopulatedString(postID)) { - break //create the coordinate, but stop because of the invalid id - } - const selectedPost = await new Promise(res => { - getUser() - .get(Key.POSTS_NEW) - .get(postID) - .load(res) - }) - if (!Common.Schema.isPost(selectedPost)) { - break //create the coordinate, but stop because of the invalid post - } - /** - * @type {Record} - */ - const contentsToSend = {} - const mySecret = require('../../Mediator').getMySecret() - await Common.Utils.asyncForEach( - Object.entries(selectedPost.contentItems), - async ([contentID, item]) => { - if ( - item.type !== 'image/embedded' && - item.type !== 'video/embedded' - ) { - return //only visual content can be private - } - if (!item.isPrivate) { - return - } - const decrypted = await SEA.decrypt(item.magnetURI, mySecret) - contentsToSend[contentID] = decrypted + const invoicePaidCb = async paidInvoice => { + console.log('INVOICE PAID') + const hashString = paidInvoice.r_hash.toString('hex') + const { + amt_paid_sat: amt, + add_index: addIndex, + payment_addr: paymentAddr + } = paidInvoice + const orderType = order.targetType + const { ackInfo } = order //a string representing what has been requested + switch (orderType) { + case 'tip': { + const postID = ackInfo + if (!Common.isPopulatedString(postID)) { + break //create the coordinate, but stop because of the invalid id } - ) - const ackData = { unlockedContents: contentsToSend } - const toSend = JSON.stringify(ackData) - const encrypted = await SEA.encrypt(toSend, secret) - const ordResponse = { - type: 'orderAck', - content: encrypted - } - await new Promise((res, rej) => { getUser() - .get(Key.ORDER_TO_RESPONSE) - .get(orderID) - .put(ordResponse, ack => { - if (ack.err && typeof ack.err !== 'number') { - rej( - new Error( - `Error saving encrypted orderAck to order to response usergraph: ${ack}` - ) - ) - } else { - res() + .get('postToTipCount') + .get(postID) + .set(null) // each item in the set is a tip + break + } + case 'spontaneousPayment': { + //no action required + break + } + case 'contentReveal': { + console.log('cONTENT REVEAL') + //assuming digital product that only requires to be unlocked + const postID = ackInfo + console.log('ACK INFO') + console.log(ackInfo) + if (!Common.isPopulatedString(postID)) { + break //create the coordinate, but stop because of the invalid id + } + console.log('IS STRING') + const selectedPost = await new Promise(res => { + getUser() + .get(Key.POSTS_NEW) + .get(postID) + .load(res) + }) + console.log('LOAD ok') + console.log(selectedPost) + if ( + !selectedPost || + !selectedPost.status || + selectedPost.status !== 'publish' + ) { + break //create the coordinate, but stop because of the invalid post + } + console.log('IS POST') + /** + * @type {Record} + */ + const contentsToSend = {} + const mySecret = require('../../Mediator').getMySecret() + console.log('SECRET OK') + await Common.Utils.asyncForEach( + Object.entries(selectedPost.contentItems), + async ([contentID, item]) => { + if ( + item.type !== 'image/embedded' && + item.type !== 'video/embedded' + ) { + return //only visual content can be private } - }) - }) - break - } - case 'torrentSeed': { - const seedUrl = process.env.TORRENT_SEED_URL - const seedToken = process.env.TORRENT_SEED_TOKEN - if (!seedUrl || !seedToken) { - break //service not available - } - const token = crypto.randomBytes(32).toString('hex') - const reqData = { - seed_token: seedToken, - wallet_token: token - } - const res = await fetch(`${seedUrl}/api/enroll_token`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(reqData) - }) - if (res.ok) { - break //request didnt work, save coordinate anyway - } + if (!item.isPrivate) { + return + } + const decrypted = await SEA.decrypt(item.magnetURI, mySecret) + contentsToSend[contentID] = decrypted + } + ) + const ackData = { unlockedContents: contentsToSend } + const toSend = JSON.stringify(ackData) + const encrypted = await SEA.encrypt(toSend, secret) + const ordResponse = { + type: 'orderAck', + response: encrypted + } + console.log('RES READY') - const ackData = { seedUrl, token } - const toSend = JSON.stringify(ackData) - const encrypted = await SEA.encrypt(toSend, secret) - const serviceResponse = { - type: 'orderAck', - content: encrypted - } - await new Promise((res, rej) => { - getUser() - .get(Key.ORDER_TO_RESPONSE) - .get(orderID) - .put(serviceResponse, ack => { - if (ack.err && typeof ack.err !== 'number') { - rej( - new Error( - `Error saving encrypted orderAck to order to response usergraph: ${ack}` + await new Promise((res, rej) => { + getUser() + .get(Key.ORDER_TO_RESPONSE) + .get(ackNode) + .put(ordResponse, ack => { + if (ack.err && typeof ack.err !== 'number') { + rej( + new Error( + `Error saving encrypted orderAck to order to response usergraph: ${ack}` + ) ) - ) - } else { - res() - } - }) - }) - break + } else { + res() + } + }) + }) + console.log('RES SENT') + break + } + case 'torrentSeed': { + console.log('TORRENT') + const seedUrl = process.env.TORRENT_SEED_URL + const seedToken = process.env.TORRENT_SEED_TOKEN + if (!seedUrl || !seedToken) { + break //service not available + } + console.log('SEED URL OK') + const token = crypto.randomBytes(32).toString('hex') + const reqData = { + seed_token: seedToken, + wallet_token: token + } + console.log(seedUrl) + console.log(seedToken) + const res = await fetch(`${seedUrl}/api/enroll_token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(reqData) + }) + if (res.status !== 200) { + break //request didnt work, save coordinate anyway + } + console.log('RES SEED OK') + const ackData = { seedUrl, token } + const toSend = JSON.stringify(ackData) + const encrypted = await SEA.encrypt(toSend, secret) + const serviceResponse = { + type: 'orderAck', + response: encrypted + } + console.log('RES SEED SENT') + await new Promise((res, rej) => { + getUser() + .get(Key.ORDER_TO_RESPONSE) + .get(ackNode) + .put(serviceResponse, ack => { + if (ack.err && typeof ack.err !== 'number') { + rej( + new Error( + `Error saving encrypted orderAck to order to response usergraph: ${ack}` + ) + ) + } else { + res() + } + }) + }) + break + } + case 'other': //not implemented yet but save them as a coordinate anyways + break + default: + return //exit because not implemented } - case 'other': //not implemented yet but save them as a coordinate anyways - break - default: - return //exit because not implemented + const myGunPub = getUser()._.sea.pub + SchemaManager.AddOrder({ + type: orderType, + coordinateHash: hashString, + coordinateIndex: parseInt(addIndex, 10), + inbound: true, + amount: parseInt(amt, 10), + + toLndPub: paymentAddr, + fromGunPub: order.from, + toGunPub: myGunPub, + invoiceMemo: memo + }) } - const myGunPub = getUser()._.sea.pub - SchemaManager.AddOrder({ - type: orderType, - coordinateHash: hashString, - coordinateIndex: parseInt(addIndex, 10), - inbound: true, - amount: parseInt(amt, 10), + console.log('WAITING INVOICE TO BE PAID') + new Promise(res => SchemaManager.addListenInvoice(invoice.r_hash, res)) + .then(invoicePaidCb) + .catch(err => { + logger.error( + `error inside onOrders, orderAddr: ${addr}, orderID: ${orderID}, order: ${JSON.stringify( + order + )}` + ) + logger.error(err) - toLndPub: paymentAddr, - fromGunPub: order.from, - toGunPub: myGunPub, - invoiceMemo: memo - }) + /** @type {import('shock-common').Schema.OrderResponse} */ + const orderResponse = { + response: err.message, + type: 'err' + } + + getUser() + .get(Key.ORDER_TO_RESPONSE) + .get(orderID) + // @ts-expect-error + .put(orderResponse, ack => { + if (ack.err && typeof ack.err !== 'number') { + logger.error( + `Error saving encrypted invoice to order to response usergraph: ${ack}` + ) + } + }) + }) } catch (err) { logger.error( `error inside onOrders, orderAddr: ${addr}, orderID: ${orderID}, order: ${JSON.stringify( diff --git a/services/schema/index.js b/services/schema/index.js index 786b5ab0..ea7b78bd 100644 --- a/services/schema/index.js +++ b/services/schema/index.js @@ -387,6 +387,7 @@ class SchemaManager { .get(coordinateSHA256) .put(encryptedOrderString, ack => { if (ack.err && typeof ack.err !== 'number') { + console.log(ack) rej( new Error( `Error saving coordinate order to user-graph: ${ack}` diff --git a/src/routes.js b/src/routes.js index f61a4857..3801a4f7 100644 --- a/src/routes.js +++ b/src/routes.js @@ -1192,12 +1192,17 @@ module.exports = async ( app.post('/api/lnd/unifiedTrx', async (req, res) => { try { - const { type, amt, to, memo, feeLimit, postID } = req.body - - if (type !== 'spont' && type !== 'post') { + const { type, amt, to, memo, feeLimit, ackInfo } = req.body + if ( + type !== 'spontaneousPayment' && + type !== 'tip' && + type !== 'torrentSeed' && + type !== 'contentReveal' && + type !== 'other' + ) { return res.status(415).json({ field: 'type', - errorMessage: `Only 'spont' and 'post' payments supported via this endpoint for now.` + errorMessage: `Only 'spontaneousPayment'| 'tip' | 'torrentSeed' | 'contentReveal' | 'other' payments supported via this endpoint for now.` }) } @@ -1231,17 +1236,17 @@ module.exports = async ( }) } - if (type === 'post' && typeof postID !== 'string') { + if (type === 'tip' && typeof ackInfo !== 'string') { return res.status(400).json({ - field: 'postID', - errorMessage: `Send postID` + field: 'ackInfo', + errorMessage: `Send ackInfo` }) } return res.status(200).json( await GunActions.sendSpontaneousPayment(to, amt, memo, feeLimit, { type, - postID + ackInfo }) ) } catch (e) { @@ -2164,10 +2169,12 @@ module.exports = async ( app.post(`/api/gun/wall/`, async (req, res) => { try { const { tags, title, contentItems } = req.body + const SEA = require('../services/gunDB/Mediator').mySEA return res .status(200) - .json(await GunActions.createPostNew(tags, title, contentItems)) + .json(await GunActions.createPostNew(tags, title, contentItems, SEA)) } catch (e) { + console.log(e) return res.status(500).json({ errorMessage: (typeof e === 'string' ? e : e.message) || 'Unknown error.' From 6d8c82b693d64294de3ad0f2e6e06efa30fc2c12 Mon Sep 17 00:00:00 2001 From: hatim boufnichel Date: Sun, 24 Jan 2021 18:27:21 +0100 Subject: [PATCH 15/18] clean and manage errs --- services/gunDB/contact-api/jobs/onOrders.js | 27 +++- services/schema/index.js | 164 +++++++++----------- 2 files changed, 95 insertions(+), 96 deletions(-) diff --git a/services/gunDB/contact-api/jobs/onOrders.js b/services/gunDB/contact-api/jobs/onOrders.js index 63b9914e..8da7fda7 100644 --- a/services/gunDB/contact-api/jobs/onOrders.js +++ b/services/gunDB/contact-api/jobs/onOrders.js @@ -242,6 +242,8 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { */ const invoicePaidCb = async paidInvoice => { console.log('INVOICE PAID') + let breakError = null + let orderMetadata //eslint-disable-line init-declarations const hashString = paidInvoice.r_hash.toString('hex') const { amt_paid_sat: amt, @@ -254,6 +256,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { case 'tip': { const postID = ackInfo if (!Common.isPopulatedString(postID)) { + breakError = 'invalid ackInfo provided for postID' break //create the coordinate, but stop because of the invalid id } getUser() @@ -273,6 +276,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { console.log('ACK INFO') console.log(ackInfo) if (!Common.isPopulatedString(postID)) { + breakError = 'invalid ackInfo provided for postID' break //create the coordinate, but stop because of the invalid id } console.log('IS STRING') @@ -289,6 +293,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { !selectedPost.status || selectedPost.status !== 'publish' ) { + breakError = 'ackInfo provided does not correspond to a valid post' break //create the coordinate, but stop because of the invalid post } console.log('IS POST') @@ -298,6 +303,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { const contentsToSend = {} const mySecret = require('../../Mediator').getMySecret() console.log('SECRET OK') + let privateFound = false await Common.Utils.asyncForEach( Object.entries(selectedPost.contentItems), async ([contentID, item]) => { @@ -310,10 +316,16 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { if (!item.isPrivate) { return } + privateFound = true const decrypted = await SEA.decrypt(item.magnetURI, mySecret) contentsToSend[contentID] = decrypted } ) + if (!privateFound) { + breakError = + 'post provided from ackInfo does not contain private content' + break //no private content in this post + } const ackData = { unlockedContents: contentsToSend } const toSend = JSON.stringify(ackData) const encrypted = await SEA.encrypt(toSend, secret) @@ -339,7 +351,8 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { } }) }) - console.log('RES SENT') + console.log('RES SENT CONTENT') + orderMetadata = JSON.stringify(ordResponse) break } case 'torrentSeed': { @@ -347,6 +360,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { const seedUrl = process.env.TORRENT_SEED_URL const seedToken = process.env.TORRENT_SEED_TOKEN if (!seedUrl || !seedToken) { + breakError = 'torrentSeed service not available' break //service not available } console.log('SEED URL OK') @@ -365,6 +379,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { body: JSON.stringify(reqData) }) if (res.status !== 200) { + breakError = 'torrentSeed service currently not available' break //request didnt work, save coordinate anyway } console.log('RES SEED OK') @@ -392,6 +407,8 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { } }) }) + console.log('RES SENT SEED') + orderMetadata = JSON.stringify(serviceResponse) break } case 'other': //not implemented yet but save them as a coordinate anyways @@ -399,6 +416,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { default: return //exit because not implemented } + const metadata = breakError ? JSON.stringify(breakError) : orderMetadata const myGunPub = getUser()._.sea.pub SchemaManager.AddOrder({ type: orderType, @@ -410,8 +428,13 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { toLndPub: paymentAddr, fromGunPub: order.from, toGunPub: myGunPub, - invoiceMemo: memo + invoiceMemo: memo, + + metadata }) + if (breakError) { + throw new Error(breakError) + } } console.log('WAITING INVOICE TO BE PAID') new Promise(res => SchemaManager.addListenInvoice(invoice.r_hash, res)) diff --git a/services/schema/index.js b/services/schema/index.js index ea7b78bd..76ad3fc0 100644 --- a/services/schema/index.js +++ b/services/schema/index.js @@ -106,52 +106,50 @@ const checkOrderInfo = order => { return null } -/** +/* * * @param {CoordinateOrder} orderInfo * @param {string} coordinateSHA256 - */ + *//* const dateIndexCreateCb = (orderInfo, coordinateSHA256) => { - //if (this.memIndex) { need bind to use this here - //update date memIndex - //} - const date = new Date(orderInfo.timestamp || 0) - //use UTC for consistency? - const year = date.getUTCFullYear().toString() - const month = date.getUTCMonth().toString() +//if (this.memIndex) { need bind to use this here +//update date memIndex +//} +const date = new Date(orderInfo.timestamp || 0) +//use UTC for consistency? +const year = date.getUTCFullYear().toString() +const month = date.getUTCMonth().toString() - getGunUser() - .get(Key.DATE_COORDINATE_INDEX) - .get(year) - .get(month) - .set(coordinateSHA256) -} +getGunUser() +.get(Key.DATE_COORDINATE_INDEX) +.get(year) +.get(month) +.set(coordinateSHA256) +}*/ -/** +/* * if not provided, assume current month and year * @param {number|null} year * @param {number|null} month - */ + *//* const getMonthCoordinates = async (year = null, month = null) => { - const now = Date.now() - //@ts-expect-error - const stringYear = year !== null ? year.toString() : now.getUTCFullYear().toString() - //@ts-expect-error - const stringMonth = month !== null ? month.toString() : now.getUTCMonth().toString() +const now = Date.now() +const stringYear = year !== null ? year.toString() : now.getUTCFullYear().toString() +const stringMonth = month !== null ? month.toString() : now.getUTCMonth().toString() - const data = await new Promise(res => { - getGunUser() - .get(Key.DATE_COORDINATE_INDEX) - .get(stringYear) - .get(stringMonth) - .load(res) - }) - const coordinatesArray = Object - .values(data) - .filter(coordinateSHA256 => typeof coordinateSHA256 === 'string') +const data = await new Promise(res => { + getGunUser() + .get(Key.DATE_COORDINATE_INDEX) + .get(stringYear) + .get(stringMonth) + .load(res) +}) +const coordinatesArray = Object + .values(data) + .filter(coordinateSHA256 => typeof coordinateSHA256 === 'string') - return coordinatesArray -} +return coordinatesArray +}*/ /** * @@ -320,25 +318,11 @@ const handleUnconfirmedTx = (tx, order) => { } class SchemaManager { - constructor(opts = { memIndex: false }) {//config flag? - this.memIndex = opts.memIndex - this.orderCreateIndexCallbacks.push(dateIndexCreateCb) //create more Cbs and put them here for more indexes callbacks - } - - dateIndexName = 'dateIndex' - - memIndex = false //save the index data in memory for faster access - - // MEM INDEX, will be used only if memIndex === true - memDateIndex = {} //not implemented yet - - memGunPubIndex = {} //not implemented yet - - memLndPubIndex = {} //not implemented yet - - memTypeIndex = {} //not implemented yet - // + //constructor() { + // this.orderCreateIndexCallbacks.push(dateIndexCreateCb) //create more Cbs and put them here for more indexes callbacks + //} + //dateIndexName = 'dateIndex' /** * @type {((order : CoordinateOrder,coordinateSHA256 : string)=>void)[]} */ @@ -347,6 +331,7 @@ class SchemaManager { /** * @param {CoordinateOrder} orderInfo */ + // eslint-disable-next-line class-methods-use-this async AddOrder(orderInfo) { const checkErr = checkOrderInfo(orderInfo) if (checkErr) { @@ -369,7 +354,6 @@ class SchemaManager { amount: orderInfo.amount, description: orderInfo.description, metadata: orderInfo.metadata, - timestamp: orderInfo.timestamp || Date.now(), } const orderString = JSON.stringify(filteredOrder) @@ -400,57 +384,49 @@ class SchemaManager { }) //update all indexes with - this.orderCreateIndexCallbacks.forEach(cb => cb(filteredOrder, coordinateSHA256)) + //this.orderCreateIndexCallbacks.forEach(cb => cb(filteredOrder, coordinateSHA256)) } - /** + /* * if not provided, assume current month and year * @param {number|null} year * @param {number|null} month * @returns {Promise} from newer to older - */ - async getMonthOrders(year = null, month = null) { - const now = new Date() - const intYear = year !== null ? year : now.getUTCFullYear() - const intMonth = month !== null ? month : now.getUTCMonth() + *//* +async getMonthOrders(year = null, month = null) { +const now = new Date() +const intYear = year !== null ? year : now.getUTCFullYear() +const intMonth = month !== null ? month : now.getUTCMonth() - let coordinates = null - if (this.memIndex) { - //get coordinates from this.memDateIndex - } else { - coordinates = await getMonthCoordinates(intYear, intMonth) - } - /** - * @type {CoordinateOrder[]} - */ - const orders = [] - if (!coordinates) { - return orders - } - await Common.Utils.asyncForEach(coordinates, async coordinateSHA256 => { - const encryptedOrderString = await getGunUser() - .get(Key.COORDINATES) - .get(coordinateSHA256) - .then() - if (typeof encryptedOrderString !== 'string') { - return - } - const mySecret = require('../gunDB/Mediator').getMySecret() - const SEA = require('../gunDB/Mediator').mySEA - const decryptedString = await SEA.decrypt(encryptedOrderString, mySecret) - - /** - * @type {CoordinateOrder} - */ - const orderJSON = JSON.parse(decryptedString) - orders.push(orderJSON) - }) - //@ts-expect-error - const orderedOrders = orders.sort((a, b) => b.timestamp - a.timestamp) - return orderedOrders +let coordinates = null +if (this.memIndex) { + //get coordinates from this.memDateIndex +} else { + coordinates = await getMonthCoordinates(intYear, intMonth) +} +const orders = [] +if (!coordinates) { + return orders +} +await Common.Utils.asyncForEach(coordinates, async coordinateSHA256 => { + const encryptedOrderString = await getGunUser() + .get(Key.COORDINATES) + .get(coordinateSHA256) + .then() + if (typeof encryptedOrderString !== 'string') { + return } + const mySecret = require('../gunDB/Mediator').getMySecret() + const SEA = require('../gunDB/Mediator').mySEA + const decryptedString = await SEA.decrypt(encryptedOrderString, mySecret) + const orderJSON = JSON.parse(decryptedString) + orders.push(orderJSON) +}) +const orderedOrders = orders.sort((a, b) => b.timestamp - a.timestamp) +return orderedOrders +}*/ /** * @typedef {Common.Schema.InvoiceWhenListed & {r_hash:Buffer,payment_addr:string}} Invoice From b0c5f40595559b3ff8590c2391a6e20ff6721661 Mon Sep 17 00:00:00 2001 From: hatim boufnichel Date: Fri, 29 Jan 2021 15:46:47 +0100 Subject: [PATCH 16/18] fixies --- services/gunDB/contact-api/actions.js | 27 +++++++------ services/gunDB/contact-api/jobs/onOrders.js | 43 +++++++++++++-------- 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/services/gunDB/contact-api/actions.js b/services/gunDB/contact-api/actions.js index 6f234e10..c03e0307 100644 --- a/services/gunDB/contact-api/actions.js +++ b/services/gunDB/contact-api/actions.js @@ -1112,15 +1112,18 @@ const sendSpontaneousPayment = async ( console.log(orderResponse) console.log(Schema.isOrderResponse(orderResponse)) - if (Schema.isOrderResponse(orderResponse)) { + //@ts-expect-error + if (orderResponse && orderResponse.type === 'orderAck') { + //@ts-expect-error res(orderResponse) } }) }), - v => !Schema.isOrderResponse(v) + //@ts-expect-error + v => !v || !v.type ) - if (!Schema.isOrderResponse(encryptedOrderAckRes)) { + if (!encryptedOrderAckRes || !encryptedOrderAckRes.type) { const e = TypeError( `Expected encryptedOrderAckRes got: ${typeof encryptedOrderAckRes}` ) @@ -1361,17 +1364,17 @@ const createPostNew = async (tags, title, content, SEA) => { } const mySecret = require('../Mediator').getMySecret() await Common.Utils.asyncForEach(content, async c => { - const cBis = c - if ( - (cBis.type === 'image/embedded' || cBis.type === 'video/embedded') && - cBis.isPrivate - ) { - const encryptedMagnet = await SEA.encrypt(cBis.magnetURI, mySecret) - cBis.magnetURI = encryptedMagnet - } // @ts-expect-error const uuid = Gun.text.random() - newPost.contentItems[uuid] = cBis + if ( + (c.type === 'image/embedded' || c.type === 'video/embedded') && + c.isPrivate + ) { + const encryptedMagnet = await SEA.encrypt(c.magnetURI, mySecret) + newPost.contentItems[uuid] = { ...c, magnetURI: encryptedMagnet } + } else { + newPost.contentItems[uuid] = c + } }) /** @type {string} */ diff --git a/services/gunDB/contact-api/jobs/onOrders.js b/services/gunDB/contact-api/jobs/onOrders.js index 8da7fda7..cede83c6 100644 --- a/services/gunDB/contact-api/jobs/onOrders.js +++ b/services/gunDB/contact-api/jobs/onOrders.js @@ -357,6 +357,11 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { } case 'torrentSeed': { console.log('TORRENT') + const numberOfTokens = Number(ackInfo) + if (isNaN(numberOfTokens)) { + breakError = 'ackInfo provided is not a valid number' + break + } const seedUrl = process.env.TORRENT_SEED_URL const seedToken = process.env.TORRENT_SEED_TOKEN if (!seedUrl || !seedToken) { @@ -364,26 +369,30 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { break //service not available } console.log('SEED URL OK') - const token = crypto.randomBytes(32).toString('hex') - const reqData = { - seed_token: seedToken, - wallet_token: token + const tokens = Array(numberOfTokens) + for (let i = 0; i < numberOfTokens; i++) { + tokens[i] = crypto.randomBytes(32).toString('hex') } - console.log(seedUrl) - console.log(seedToken) - const res = await fetch(`${seedUrl}/api/enroll_token`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(reqData) - }) - if (res.status !== 200) { - breakError = 'torrentSeed service currently not available' - break //request didnt work, save coordinate anyway + /**@param {string} token */ + const enrollToken = async token => { + const reqData = { + seed_token: seedToken, + wallet_token: token + } + const res = await fetch(`${seedUrl}/api/enroll_token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(reqData) + }) + if (res.status !== 200) { + throw new Error('torrentSeed service currently not available') + } } + await Promise.all(tokens.map(enrollToken)) console.log('RES SEED OK') - const ackData = { seedUrl, token } + const ackData = { seedUrl, tokens } const toSend = JSON.stringify(ackData) const encrypted = await SEA.encrypt(toSend, secret) const serviceResponse = { From 54bb3e6ee18eb07e2019c65eca641134ddda3a2b Mon Sep 17 00:00:00 2001 From: emad-salah Date: Sun, 7 Feb 2021 17:45:56 +0100 Subject: [PATCH 17/18] PWA fixes --- src/cors.js | 3 ++- src/routes.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cors.js b/src/cors.js index e3bb309e..7a0bd24b 100644 --- a/src/cors.js +++ b/src/cors.js @@ -1,8 +1,9 @@ const setAccessControlHeaders = (req, res) => { res.header("Access-Control-Allow-Origin", "*"); + res.header("Access-Control-Allow-Methods", "OPTIONS,POST,GET,PUT,DELETE") res.header( "Access-Control-Allow-Headers", - "Origin, X-Requested-With, Content-Type, Accept, Authorization" + "Origin, X-Requested-With, Content-Type, Accept, Authorization, public-key-for-decryption" ); }; diff --git a/src/routes.js b/src/routes.js index f61a4857..adca79bf 100644 --- a/src/routes.js +++ b/src/routes.js @@ -1124,7 +1124,7 @@ module.exports = async ( app.get('/api/lnd/listchannels', async (_, res) => { try { return res.json({ - channels: await LV2.listChannels() + channels: await LV2.listChannels({ active_only: false }) }) } catch (e) { console.log(e) From 138311693584c937fdcaa75cbcc35ed301e45fe5 Mon Sep 17 00:00:00 2001 From: hatim boufnichel Date: Sat, 27 Feb 2021 20:58:45 +0100 Subject: [PATCH 18/18] fixed coordinates, orderAck, lnd streams --- services/coordinates.js | 63 --------------------- services/gunDB/contact-api/actions.js | 16 +++--- services/gunDB/contact-api/jobs/onOrders.js | 49 +++++++++------- utils/lightningServices/v2.js | 52 +---------------- 4 files changed, 37 insertions(+), 143 deletions(-) delete mode 100644 services/coordinates.js diff --git a/services/coordinates.js b/services/coordinates.js deleted file mode 100644 index d56981bb..00000000 --- a/services/coordinates.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @format - */ - -const Common = require('shock-common') -const mapValues = require('lodash/mapValues') -const pickBy = require('lodash/pickBy') -const Bluebird = require('bluebird') -const Logger = require('winston') -const Key = require('../services/gunDB/contact-api/key') - -const { getUser, getMySecret, mySEA } = require('./gunDB/Mediator') - -/** - * @param {string} coordID - * @param {Common.Coordinate} data - * @returns {Promise} - */ -module.exports.writeCoordinate = async (coordID, data) => { - if (coordID !== data.id) { - throw new Error('CoordID must be equal to data.id') - } - - try { - /** - * Because there are optional properties, typescript can also allow them - * to be specified but with a value of `undefined`. Filter out these. - * @type {Record} - */ - const sanitizedData = pickBy(data, v => typeof v !== 'undefined') - - const encData = await Bluebird.props( - mapValues(sanitizedData, v => { - return mySEA.encrypt(v, getMySecret()) - }) - ) - - getUser() - .get(Key.COORDINATES) - .get(coordID) - .put(encData, ack => { - if (ack.err && typeof ack.err !== 'number') { - Logger.info( - `Error writting corrdinate, coordinate id: ${coordID}, data: ${JSON.stringify( - data, - null, - 2 - )}` - ) - Logger.error(ack.err) - } - }) - } catch (e) { - Logger.info( - `Error writing coordinate, coordinate id: ${coordID}, data: ${JSON.stringify( - data, - null, - 2 - )}` - ) - Logger.error(e.message) - } -} diff --git a/services/gunDB/contact-api/actions.js b/services/gunDB/contact-api/actions.js index 05bf741d..9c2a3ca3 100644 --- a/services/gunDB/contact-api/actions.js +++ b/services/gunDB/contact-api/actions.js @@ -11,8 +11,7 @@ const { ErrorCode } = Constants const { sendPaymentV2Invoice, - decodePayReq, - myLNDPub + decodePayReq } = require('../../../utils/lightningServices/v2') /** @@ -261,7 +260,7 @@ const acceptRequest = async ( newlyCreatedOutgoingFeedID, ourSecret ) - + //why await if you dont need the response? await /** @type {Promise} */ (new Promise((res, rej) => { gun .get(Key.HANDSHAKE_NODES) @@ -359,7 +358,7 @@ const generateHandshakeAddress = async () => { } }) })) - + //why await if you dont need the response? await /** @type {Promise} */ (new Promise((res, rej) => { gun .get(Key.HANDSHAKE_NODES) @@ -644,7 +643,7 @@ const sendHandshakeRequest = async (recipientPublicKey, gun, user, SEA) => { handshakeAddress: await SEA.encrypt(currentHandshakeAddress, mySecret), timestamp } - + //why await if you dont need the response? await /** @type {Promise} */ (new Promise((res, rej) => { //@ts-ignore user.get(Key.STORED_REQS).set(storedReq, ack => { @@ -1406,10 +1405,9 @@ const createPostNew = async (tags, title, content) => { * @param {string[]} tags * @param {string} title * @param {Common.Schema.ContentItem[]} content - * @param {ISEA} SEA * @returns {Promise} */ -const createPost = async (tags, title, content, SEA) => { +const createPost = async (tags, title, content) => { if (content.length === 0) { throw new Error(`A post must contain at least one paragraph/image/video`) } @@ -1486,7 +1484,7 @@ const createPost = async (tags, title, content, SEA) => { ) })) - const [postID, newPost] = await createPostNew(tags, title, content, SEA) + const [postID, newPost] = await createPostNew(tags, title, content) await Common.makePromise((res, rej) => { require('../Mediator') @@ -1595,7 +1593,7 @@ const follow = async (publicKey, isPrivate) => { status: 'ok', user: publicKey } - + //why await if you dont need the response? await /** @type {Promise} */ (new Promise((res, rej) => { require('../Mediator') .getUser() diff --git a/services/gunDB/contact-api/jobs/onOrders.js b/services/gunDB/contact-api/jobs/onOrders.js index 54c56d9d..2d14c12f 100644 --- a/services/gunDB/contact-api/jobs/onOrders.js +++ b/services/gunDB/contact-api/jobs/onOrders.js @@ -2,15 +2,12 @@ * @format */ // @ts-check -const Gun = require('gun') -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 crypto = require('crypto') -// @ts-expect-error TODO fix this const fetch = require('node-fetch') const { Constants: { ErrorCode }, @@ -18,11 +15,6 @@ const { } = Common const SchemaManager = require('../../../schema') const LightningServices = require('../../../../utils/lightningServices') -const { - addInvoice, - myLNDPub -} = require('../../../../utils/lightningServices/v2') -const { writeCoordinate } = require('../../../coordinates') const Key = require('../key') const Utils = require('../utils') const { gunUUID } = require('../../../../utils') @@ -65,6 +57,28 @@ const ordersProcessed = new Set() let currentOrderAddr = '' +/** + * @param {InvoiceRequest} invoiceReq + * @returns {Promise} + */ +const _addInvoice = invoiceReq => + new Promise((resolve, rej) => { + const { + services: { lightning } + } = LightningServices + + lightning.addInvoice(invoiceReq, ( + /** @type {any} */ error, + /** @type {InvoiceResponse} */ response + ) => { + if (error) { + rej(error) + } else { + resolve(response) + } + }) + }) + /** * @param {string} addr * @param {ISEA} SEA @@ -150,12 +164,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { `onOrders() -> Will now create an invoice : ${JSON.stringify(invoiceReq)}` ) - const invoice = await addInvoice( - invoiceReq.value, - invoiceReq.memo, - true, - invoiceReq.expiry - ) + const invoice = await _addInvoice(invoiceReq) logger.info( 'onOrders() -> Successfully created the invoice, will now encrypt it' @@ -166,14 +175,12 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { logger.info( `onOrders() -> Will now place the encrypted invoice in order to response usergraph: ${addr}` ) - // @ts-expect-error - const ackNode = Gun.text.random() + const ackNode = gunUUID() /** @type {import('shock-common').Schema.OrderResponse} */ const orderResponse = { response: encInvoice, type: 'invoice', - //@ts-expect-error ackNode } @@ -221,8 +228,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { } getUser() .get('postToTipCount') - // CAST: Checked above. - .get(/** @type {string} */ (order.ackInfo)) + .get(postID) .set(null) // each item in the set is a tip break } @@ -308,7 +314,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { ) ) } else { - res() + res(null) } }) }) @@ -340,6 +346,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { seed_token: seedToken, wallet_token: token } + //@ts-expect-error const res = await fetch(`${seedUrl}/api/enroll_token`, { method: 'POST', headers: { @@ -373,7 +380,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { ) ) } else { - res() + res(null) } }) }) diff --git a/utils/lightningServices/v2.js b/utils/lightningServices/v2.js index 6f2cccbb..a32572cf 100644 --- a/utils/lightningServices/v2.js +++ b/utils/lightningServices/v2.js @@ -6,8 +6,6 @@ const logger = require('winston') const Common = require('shock-common') const Ramda = require('ramda') -const { writeCoordinate } = require('../../services/coordinates') - const lightningServices = require('./lightning-services') /** * @typedef {import('./types').PaymentV2} PaymentV2 @@ -237,28 +235,12 @@ const decodePayReq = payReq => ) }) -/** - * @returns {Promise} - */ -const myLNDPub = () => - Common.makePromise((res, rej) => { - const { lightning } = lightningServices.getServices() - - lightning.getInfo({}, (err, data) => { - if (err) { - rej(new Error(err.message)) - } else { - res(data.identity_pubkey) - } - }) - }) - /** * aklssjdklasd * @param {SendPaymentV2Request} sendPaymentRequest * @returns {Promise} */ -const sendPaymentV2 = async sendPaymentRequest => { +const sendPaymentV2 = sendPaymentRequest => { const { services: { router } } = lightningServices @@ -269,10 +251,7 @@ const sendPaymentV2 = async sendPaymentRequest => { ) } - /** - * @type {import("./types").PaymentV2} - */ - const paymentV2 = await Common.makePromise((res, rej) => { + return Common.makePromise((res, rej) => { const stream = router.sendPaymentV2(sendPaymentRequest) stream.on( @@ -311,33 +290,6 @@ const sendPaymentV2 = async sendPaymentRequest => { } ) }) - - /** @type {Common.Coordinate} */ - const coord = { - amount: Number(paymentV2.value_sat), - id: paymentV2.payment_hash, - inbound: false, - timestamp: Date.now(), - toLndPub: await myLNDPub(), - fromLndPub: undefined, - invoiceMemo: undefined, - type: 'payment' - } - - if (sendPaymentRequest.payment_request) { - const invoice = await decodePayReq(sendPaymentRequest.payment_request) - - coord.invoiceMemo = invoice.description - coord.toLndPub = invoice.destination - } - - if (sendPaymentRequest.dest) { - coord.toLndPub = sendPaymentRequest.dest.toString('base64') - } - - await writeCoordinate(paymentV2.payment_hash, coord) - - return paymentV2 } /**