diff --git a/.env.example b/.env.example index 3c94261c..ce097770 100644 --- a/.env.example +++ b/.env.example @@ -7,4 +7,4 @@ SHOCK_CACHE=true TRUSTED_KEYS=true LOCAL_TUNNEL_SERVER=https://tunnel.rip TORRENT_SEED_URL=https://webtorrent.shock.network -TORRENT_SEED_TOKEN=jibberish +TORRENT_SEED_TOKEN=jibberish \ No newline at end of file diff --git a/package.json b/package.json index 608bb8d1..7955cca1 100644 --- a/package.json +++ b/package.json @@ -105,4 +105,4 @@ "pre-commit": "yarn lint && yarn typecheck && yarn lint-staged" } } -} +} \ No newline at end of file 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 e1fd403b..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') /** @@ -22,7 +21,8 @@ const { const Getters = require('./getters') const Key = require('./key') const Utils = require('./utils') -const { writeCoordinate } = require('../../coordinates') +const SchemaManager = require('../../schema') +const LNDHealthMananger = require('../../../utils/lightningServices/errors') /** * @typedef {import('./SimpleGUN').GUNNode} GUNNode @@ -260,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) @@ -358,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) @@ -643,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 => { @@ -923,10 +923,13 @@ const sendHRWithInitialMsg = async ( /** * @typedef {object} SpontPaymentOptions * @prop {Common.Schema.OrderTargetType} type - * @prop {string=} postID * @prop {string=} ackInfo */ - +/** + * @typedef {object} OrderRes + * @prop {PaymentV2} payment + * @prop {object=} orderAck + */ /** * Returns the preimage corresponding to the payment. * @param {string} to @@ -936,7 +939,7 @@ 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, @@ -965,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)) @@ -1006,7 +1006,7 @@ const sendSpontaneousPayment = async ( ) ) } else { - res(ord._.get) + setTimeout(() => res(ord._.get), 0) } }) }) @@ -1017,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 => @@ -1027,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)) { @@ -1043,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)) @@ -1076,35 +1080,87 @@ const sendSpontaneousPayment = async ( feeLimit, payment_request: orderResponse.response }) + const myLndPub = LNDHealthMananger.lndPub + if ( + (opts.type !== 'contentReveal' && opts.type !== 'torrentSeed') || + !orderResponse.ackNode + ) { + 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 } + } + console.log('ACK NODE') + console.log(orderResponse.ackNode) + /** @type {import('shock-common').Schema.OrderResponse} */ + const encryptedOrderAckRes = await Utils.tryAndWait( + gun => + new Promise(res => { + gun + .user(to) + .get(Key.ORDER_TO_RESPONSE) + .get(orderResponse.ackNode) + .on(orderResponse => { + console.log(orderResponse) + console.log(Schema.isOrderResponse(orderResponse)) - await writeCoordinate(payment.payment_hash, { - id: payment.payment_hash, - type: (() => { - if (opts.type === 'tip') { - return 'tip' - } else if (opts.type === 'spontaneousPayment') { - return 'spontaneousPayment' - } else if (opts.type === 'contentReveal') { - return 'other' // TODO - } else if (opts.type === 'other') { - return 'other' // TODO - } else if (opts.type === 'torrentSeed') { - return 'other' // TODO - } - // ensures we handle all possible types - /** @type {never} */ - const assertNever = opts.type + //@ts-expect-error + if (orderResponse && orderResponse.type === 'orderAck') { + //@ts-expect-error + res(orderResponse) + } + }) + }), + //@ts-expect-error + v => !v || !v.type + ) - return assertNever && opts.type // please TS - })(), - amount: Number(payment.value_sat), + if (!encryptedOrderAckRes || !encryptedOrderAckRes.type) { + const e = TypeError( + `Expected encryptedOrderAckRes 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, - timestamp: Date.now(), - toLndPub: await myLNDPub() + fromGunPub: getUser()._.sea.pub, + toGunPub: to, + invoiceMemo: memo, + metadata: JSON.stringify(orderAck) }) - - return payment + return { payment, orderAck } } catch (e) { + console.log(e) logger.error('Error inside sendPayment()') logger.error(e) throw e @@ -1122,8 +1178,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 } /** @@ -1307,12 +1363,6 @@ const createPostNew = async (tags, title, content) => { contentItems: {} } - content.forEach(c => { - // @ts-expect-error - const uuid = Gun.text.random() - newPost.contentItems[uuid] = c - }) - const mySecret = require('../Mediator').getMySecret() await Common.Utils.asyncForEach(content, async c => { @@ -1543,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 ad757ccc..2d14c12f 100644 --- a/services/gunDB/contact-api/jobs/onOrders.js +++ b/services/gunDB/contact-api/jobs/onOrders.js @@ -7,20 +7,14 @@ const isFinite = require('lodash/isFinite') const isNumber = require('lodash/isNumber') const isNaN = require('lodash/isNaN') const Common = require('shock-common') +const crypto = require('crypto') +const fetch = require('node-fetch') const { Constants: { ErrorCode }, Schema } = Common -const { assertNever } = require('assert-never') -const crypto = require('crypto') -const fetch = require('node-fetch') - +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') @@ -63,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 @@ -89,6 +105,8 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { return } + //const listenerStartTime = performance.now() + ordersProcessed.add(orderID) logger.info( @@ -146,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' @@ -162,11 +175,13 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { logger.info( `onOrders() -> Will now place the encrypted invoice in order to response usergraph: ${addr}` ) + const ackNode = gunUUID() /** @type {import('shock-common').Schema.OrderResponse} */ const orderResponse = { response: encInvoice, - type: 'invoice' + type: 'invoice', + ackNode } await /** @type {Promise} */ (new Promise((res, rej) => { @@ -187,86 +202,74 @@ 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 - if (order.targetType === 'tip') { - const { ackInfo } = order - if (!Common.isPopulatedString(ackInfo)) { - throw new TypeError(`ackInfo(postID) not a a populated string`) - } - } - - // A post tip order lifecycle is short enough that we can do it like this. - const stream = LightningServices.invoices.subscribeSingleInvoice({ - r_hash: invoice.r_hash - }) - - /** @type {Common.Coordinate} */ - const coord = { - amount, - id: invoice.r_hash.toString(), - inbound: true, - timestamp: Date.now(), - type: 'invoice', - invoiceMemo: memo, - fromGunPub: order.from, - toGunPub: getUser()._.sea.pub, - toLndPub: await myLNDPub() - } - - if (order.targetType === 'tip') { - coord.type = 'tip' - } else { - coord.type = 'spontaneousPayment' - } - + //logger.info(`[PERF] Added invoice to GunDB in ${invoicePutEndTime}ms`) /** - * @param {Common.InvoiceWhenListed} invoice + * + * @param {Common.Schema.InvoiceWhenListed & {r_hash:Buffer,payment_addr:string}} paidInvoice */ - const onData = async invoice => { - if (invoice.settled) { - writeCoordinate(invoice.r_hash.toString(), coord) - - if (order.targetType === 'tip') { + 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, + 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)) { + breakError = 'invalid ackInfo provided for postID' + break //create the coordinate, but stop because of the invalid id + } getUser() .get('postToTipCount') - // CAST: Checked above. - .get(/** @type {string} */ (order.ackInfo)) + .get(postID) .set(null) // each item in the set is a tip - } else if (order.targetType === 'contentReveal') { - // ----------------------------------------- - logger.debug('Content Reveal') - + break + } + case 'spontaneousPayment': { + //no action required + break + } + case 'contentReveal': { + console.log('cONTENT REVEAL') //assuming digital product that only requires to be unlocked - const postID = order.ackInfo - + const postID = ackInfo + console.log('ACK INFO') + console.log(ackInfo) if (!Common.isPopulatedString(postID)) { - logger.error(`Invalid post ID`) - logger.error(postID) - return + breakError = 'invalid ackInfo provided for postID' + break //create the coordinate, but stop because of the invalid id } - - // TODO: do this reactively + console.log('IS STRING') const selectedPost = await new Promise(res => { getUser() .get(Key.POSTS_NEW) .get(postID) .load(res) }) - - logger.debug(selectedPost) - - if (Common.isPost(selectedPost)) { - logger.error('Post id provided does not correspond to a valid post') - return + console.log('LOAD ok') + console.log(selectedPost) + if ( + !selectedPost || + !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') /** * @type {Record} */ const contentsToSend = {} const mySecret = require('../../Mediator').getMySecret() - logger.debug('SECRET OK') + console.log('SECRET OK') let privateFound = false await Common.Utils.asyncForEach( Object.entries(selectedPost.contentItems), @@ -286,8 +289,9 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { } ) if (!privateFound) { - logger.error(`Post provided does not contain private content`) - return + 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) @@ -296,15 +300,12 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { type: 'orderAck', response: encrypted } - logger.debug('RES READY') + console.log('RES READY') - const uuid = gunUUID() - orderResponse.ackNode = uuid - - await /** @type {Promise} */ (new Promise((res, rej) => { + await new Promise((res, rej) => { getUser() .get(Key.ORDER_TO_RESPONSE) - .get(uuid) + .get(ackNode) .put(ordResponse, ack => { if (ack.err && typeof ack.err !== 'number') { rej( @@ -313,29 +314,28 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { ) ) } else { - res() + res(null) } }) - })) - logger.debug('RES SENT CONTENT') - - // ---------------------------------------------------------------------------------- - } else if (order.targetType === 'spontaneousPayment') { - // no action required - } else if (order.targetType === 'torrentSeed') { - logger.debug('TORRENT') - const numberOfTokens = Number(order.ackInfo) + }) + console.log('RES SENT CONTENT') + orderMetadata = JSON.stringify(ordResponse) + break + } + case 'torrentSeed': { + console.log('TORRENT') + const numberOfTokens = Number(ackInfo) if (isNaN(numberOfTokens)) { - logger.error('ackInfo provided is not a valid number') - return + 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) { - logger.error('torrentSeed service not available') - return + breakError = 'torrentSeed service not available' + break //service not available } - logger.debug('SEED URL OK') + console.log('SEED URL OK') const tokens = Array(numberOfTokens) for (let i = 0; i < numberOfTokens; i++) { tokens[i] = crypto.randomBytes(32).toString('hex') @@ -346,7 +346,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { seed_token: seedToken, wallet_token: token } - // @ts-expect-error TODO + //@ts-expect-error const res = await fetch(`${seedUrl}/api/enroll_token`, { method: 'POST', headers: { @@ -359,7 +359,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { } } await Promise.all(tokens.map(enrollToken)) - logger.debug('RES SEED OK') + console.log('RES SEED OK') const ackData = { seedUrl, tokens } const toSend = JSON.stringify(ackData) const encrypted = await SEA.encrypt(toSend, secret) @@ -368,14 +368,10 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { response: encrypted } console.log('RES SEED SENT') - - const uuid = gunUUID() - orderResponse.ackNode = uuid - - await /** @type {Promise} */ (new Promise((res, rej) => { + await new Promise((res, rej) => { getUser() .get(Key.ORDER_TO_RESPONSE) - .get(uuid) + .get(ackNode) .put(serviceResponse, ack => { if (ack.err && typeof ack.err !== 'number') { rej( @@ -384,32 +380,68 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => { ) ) } else { - res() + res(null) } }) - })) - logger.debug('RES SENT SEED') - } else if (order.targetType === 'other') { - // TODO - } else { - assertNever(order.targetType) + }) + console.log('RES SENT SEED') + orderMetadata = JSON.stringify(serviceResponse) + break } + case 'other': //not implemented yet but save them as a coordinate anyways + break + default: + return //exit because not implemented + } + const metadata = breakError ? JSON.stringify(breakError) : orderMetadata + const myGunPub = getUser()._.sea.pub + SchemaManager.AddOrder({ + type: orderType, + coordinateHash: hashString, + coordinateIndex: parseInt(addIndex, 10), + inbound: true, + amount: parseInt(amt, 10), - stream.off() + toLndPub: paymentAddr, + fromGunPub: order.from, + toGunPub: myGunPub, + 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)) + .then(invoicePaidCb) + .catch(err => { + logger.error( + `error inside onOrders, orderAddr: ${addr}, orderID: ${orderID}, order: ${JSON.stringify( + order + )}` + ) + logger.error(err) - stream.on('data', onData) + /** @type {import('shock-common').Schema.OrderResponse} */ + const orderResponse = { + response: err.message, + type: 'err' + } - stream.on('status', (/** @type {any} */ status) => { - logger.info(`Post tip, post: ${order.ackInfo}, invoice status:`, status) - }) - stream.on('end', () => { - logger.warn(`Post tip, post: ${order.ackInfo}, invoice stream ended`) - }) - stream.on('error', (/** @type {any} */ e) => { - logger.warn(`Post tip, post: ${order.ackInfo}, error:`, e) - }) + 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/gunDB/contact-api/key.js b/services/gunDB/contact-api/key.js index 210fae06..081ce2a3 100644 --- a/services/gunDB/contact-api/key.js +++ b/services/gunDB/contact-api/key.js @@ -63,4 +63,11 @@ exports.PROFILE_BINARY = 'profileBinary' exports.POSTS_NEW = 'posts' +// For Coordinates 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 new file mode 100644 index 00000000..76ad3fc0 --- /dev/null +++ b/services/schema/index.js @@ -0,0 +1,601 @@ +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' | '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 + * @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 {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, + * 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=} 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 + * + */ + +/** + * @param {CoordinateOrder} order + */ +const checkOrderInfo = order => { + const { + fromLndPub, + toLndPub, + fromGunPub, + toGunPub, + fromBtcPub, + toBtcPub, + inbound, + type, + amount, + description, + coordinateIndex, + coordinateHash, + metadata, + invoiceMemo + } = order + + if (fromLndPub && (typeof fromLndPub !== 'string' || fromLndPub === '')) { + return 'invalid "fromLndPub" field provided to order coordinate' + } + if (toLndPub && (typeof toLndPub !== 'string' || toLndPub === '')) { + return 'invalid or no "toLndPub" field provided to order coordinate' + } + if (fromGunPub && (typeof fromGunPub !== 'string' || fromGunPub === '')) { + return 'invalid "fromGunPub" field provided to order coordinate' + } + 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' + } + //@ts-expect-error + 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 (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' + } + 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() +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') + +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() { + // this.orderCreateIndexCallbacks.push(dateIndexCreateCb) //create more Cbs and put them here for more indexes callbacks + //} + + //dateIndexName = 'dateIndex' + /** + * @type {((order : CoordinateOrder,coordinateSHA256 : string)=>void)[]} + */ + orderCreateIndexCallbacks = [] + + /** + * @param {CoordinateOrder} orderInfo + */ + // eslint-disable-next-line class-methods-use-this + async AddOrder(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 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}` + const coordinateSHA256 = Crypto.createHash('SHA256') + .update(coordinate) + .digest('hex') + await new Promise((res, rej) => { + getGunUser() + .get(Key.COORDINATES) + .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}` + ) + ) + } else { + res(null) + } + }) + }) + + //update all indexes with + //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() + +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 + */ + /** + * @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 + }) + } + } + + + + + + + + /** + * @type {Record} + * lnd fires a confirmed transaction event TWICE, let's make sure it is only managed ONCE + */ + _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) + } + } + + + + /** + * + * @param {Common.Schema.ChainTransaction} tx + * @param {CoordinateOrder|false| undefined} order + */ + handleConfirmedTx(tx, order) { + const { tx_hash } = tx + if (this._confirmedTransactions[tx_hash]) { + //this tx confirmation was already handled + return + } + 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 + } + 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 finalOrder = order + finalOrder.coordinateIndex = tx.block_height + this.AddOrder(finalOrder) + if (order.toBtcPub === 'unknown') { + clearTmpChainOrder(tx_hash) + } else { + clearTmpChainOrder(order.toBtcPub) + } + 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/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 a79b5eb7..c52b7757 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) @@ -1192,11 +1192,8 @@ module.exports = async ( app.post('/api/lnd/unifiedTrx', async (req, res) => { try { - const { type, amt, to, memo, feeLimit, postID, ackInfo } = req.body - + const { type, amt, to, memo, feeLimit, ackInfo } = req.body if ( - type !== 'spont' && - type !== 'post' && type !== 'spontaneousPayment' && type !== 'tip' && type !== 'torrentSeed' && @@ -1209,21 +1206,6 @@ module.exports = async ( }) } - const typesThatShouldContainAckInfo = [ - 'tip', - 'torrentSeed', - 'contentReveal' - ] - - const shouldContainAckInfo = typesThatShouldContainAckInfo.includes(type) - - if (shouldContainAckInfo && !Common.isPopulatedString(ackInfo)) { - return res.status(400).json({ - field: 'ackInfo', - errorMessage: `Transactions of type ${typesThatShouldContainAckInfo} should contain an ackInfo field.` - }) - } - const amount = Number(amt) if (!isARealUsableNumber(amount)) { @@ -1254,17 +1236,16 @@ 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 }) ) @@ -2188,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.' diff --git a/src/sockets.js b/src/sockets.js index f1a90196..7404745e 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 @@ -73,6 +74,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...') @@ -138,7 +150,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...') @@ -215,13 +238,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/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 } - + } diff --git a/utils/lightningServices/v2.js b/utils/lightningServices/v2.js index 45a03a52..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 } /** @@ -619,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, @@ -631,5 +643,6 @@ module.exports = { listPeers, pendingChannels, addInvoice, - myLNDPub + subscribeInvoices, + subscribeTransactions }