From 219de3b67fc66ededd94c4fddad38e9a50522dcc Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Mon, 13 Jan 2020 15:57:55 -0400 Subject: [PATCH 01/24] remove unnecessary check --- services/gunDB/contact-api/actions.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/services/gunDB/contact-api/actions.js b/services/gunDB/contact-api/actions.js index 9fe5300c..160f6934 100644 --- a/services/gunDB/contact-api/actions.js +++ b/services/gunDB/contact-api/actions.js @@ -408,18 +408,8 @@ const sendHandshakeRequest = async (recipientPublicKey, gun, user, SEA) => { console.log('sendHR() -> before mySecret') const mySecret = await SEA.secret(user._.sea.epub, user._.sea) - if (typeof mySecret !== 'string') { - throw new TypeError( - "sendHandshakeRequest() -> typeof mySecret !== 'string'" - ) - } console.log('sendHR() -> before ourSecret') const ourSecret = await SEA.secret(recipientEpub, user._.sea) - if (typeof ourSecret !== 'string') { - throw new TypeError( - "sendHandshakeRequest() -> typeof ourSecret !== 'string'" - ) - } // check if successful handshake is present From 40f1e939d75ab20c24f505d816c40dfe722512b1 Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Mon, 13 Jan 2020 15:59:18 -0400 Subject: [PATCH 02/24] order schema --- services/gunDB/contact-api/schema.js | 41 ++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/services/gunDB/contact-api/schema.js b/services/gunDB/contact-api/schema.js index 929534c9..9a7d57bf 100644 --- a/services/gunDB/contact-api/schema.js +++ b/services/gunDB/contact-api/schema.js @@ -1,5 +1,5 @@ /** - * @prettier + * @format */ /** * @typedef {object} HandshakeRequest @@ -316,7 +316,6 @@ exports.isPartialOutgoing = item => { } /** - * * @param {any} item * @returns {item is Outgoing} */ @@ -337,3 +336,41 @@ exports.isOutgoing = item => { return typeof obj.with === 'string' && messagesAreMessages } + +/** + * @typedef {object} Order + * @prop {string} from Public key of sender. + * @prop {string} amount Encrypted + * @prop {string} memo Encrypted + * @prop {number} timestamp + */ + +/** + * @param {any} item + * @returns {item is Order} + */ +exports.isOrder = item => { + if (typeof item !== 'object') { + return false + } + + if (item === null) { + return false + } + + const obj = /** @type {Order} */ (item) + + if (typeof obj.amount !== 'string') { + return false + } + + if (typeof obj.from !== 'string') { + return false + } + + if (typeof obj.memo !== 'string') { + return false + } + + return typeof obj.timestamp === 'number' +} From 500e7eb2bf1e9d91f2867840dfc0851ceda9d32d Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Mon, 13 Jan 2020 15:59:24 -0400 Subject: [PATCH 03/24] order error --- services/gunDB/contact-api/errorCode.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/gunDB/contact-api/errorCode.js b/services/gunDB/contact-api/errorCode.js index eddc4f84..c3785d4a 100644 --- a/services/gunDB/contact-api/errorCode.js +++ b/services/gunDB/contact-api/errorCode.js @@ -41,3 +41,5 @@ exports.ALREADY_REQUESTED_HANDSHAKE = 'ALREADY_REQUESTED_HANDSHAKE' exports.STALE_HANDSHAKE_ADDRESS = 'STALE_HANDSHAKE_ADDRESS' exports.TIMEOUT_ERR = 'TIMEOUT_ERR' + +exports.ORDER_NOT_ANSWERED_IN_TIME = 'ORDER_NOT_ANSWERED_IN_TIME' From f638003543a7537127240cbcb4390134dc4f9014 Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Mon, 13 Jan 2020 15:59:38 -0400 Subject: [PATCH 04/24] order keys --- services/gunDB/contact-api/key.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/gunDB/contact-api/key.js b/services/gunDB/contact-api/key.js index 1aa3f8a0..52d7de8b 100644 --- a/services/gunDB/contact-api/key.js +++ b/services/gunDB/contact-api/key.js @@ -24,3 +24,9 @@ exports.DISPLAY_NAME = 'displayName' * Maps user to the last request sent to them. */ exports.USER_TO_LAST_REQUEST_SENT = 'USER_TO_LAST_REQUEST_SENT' + +exports.CURRENT_ORDER_ADDRESS = 'currentOrderAddress' + +exports.ORDER_NODES = 'orderNodes' + +exports.ORDER_TO_RESPONSE = 'orderToResponse' From 3b7b5d345c4d035c2d458c4b0b37e6600484030c Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Mon, 13 Jan 2020 16:01:37 -0400 Subject: [PATCH 05/24] order getters --- services/gunDB/contact-api/getters.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 services/gunDB/contact-api/getters.js diff --git a/services/gunDB/contact-api/getters.js b/services/gunDB/contact-api/getters.js new file mode 100644 index 00000000..7687fdbb --- /dev/null +++ b/services/gunDB/contact-api/getters.js @@ -0,0 +1,15 @@ +const Key = require('./key') +/** + * @param {string} pub + * @param {import('./SimpleGUN').GUNNode} gun + * @returns {Promise} + */ +exports.currentOrderAddress = async (pub, gun) => { + const currAddr = await gun.user(pub).get(Key.CURRENT_ORDER_ADDRESS).then() + + if (typeof currAddr !== 'string') { + throw new TypeError('Expected user.currentOrderAddress to be an string') + } + + return currAddr +} \ No newline at end of file From 93960e93172eff9177e41cb1435911be41420a7f Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Mon, 13 Jan 2020 16:03:36 -0400 Subject: [PATCH 06/24] onOrders job --- services/gunDB/contact-api/jobs/index.js | 18 ++++ .../{jobs.js => jobs/onAcceptedRequests.js} | 29 ++----- services/gunDB/contact-api/jobs/onOrders.js | 82 +++++++++++++++++++ 3 files changed, 109 insertions(+), 20 deletions(-) create mode 100644 services/gunDB/contact-api/jobs/index.js rename services/gunDB/contact-api/{jobs.js => jobs/onAcceptedRequests.js} (80%) create mode 100644 services/gunDB/contact-api/jobs/onOrders.js diff --git a/services/gunDB/contact-api/jobs/index.js b/services/gunDB/contact-api/jobs/index.js new file mode 100644 index 00000000..55c993e1 --- /dev/null +++ b/services/gunDB/contact-api/jobs/index.js @@ -0,0 +1,18 @@ +/** + * @format + * Jobs are subscriptions to events that perform actions (write to GUN) on + * response to certain ways events can happen. These tasks need to be fired up + * at app launch otherwise certain features won't work as intended. Tasks should + * ideally be idempotent, that is, if they were to be fired up after a certain + * amount of time after app launch, everything should work as intended. For this + * to work, special care has to be put into how these respond to events. These + * tasks accept factories that are homonymous to the events on this same module. + */ + +const onAcceptedRequests = require('./onAcceptedRequests') +const onOrders = require('./onOrders') + +module.exports = { + onAcceptedRequests, + onOrders +} diff --git a/services/gunDB/contact-api/jobs.js b/services/gunDB/contact-api/jobs/onAcceptedRequests.js similarity index 80% rename from services/gunDB/contact-api/jobs.js rename to services/gunDB/contact-api/jobs/onAcceptedRequests.js index a6ec48dd..34888ca9 100644 --- a/services/gunDB/contact-api/jobs.js +++ b/services/gunDB/contact-api/jobs/onAcceptedRequests.js @@ -1,24 +1,15 @@ /** - * @prettier - * Taks are subscriptions to events that perform actions (write to GUN) on - * response to certain ways events can happen. These tasks need to be fired up - * at app launch otherwise certain features won't work as intended. Tasks should - * ideally be idempotent, that is, if they were to be fired up after a certain - * amount of time after app launch, everything should work as intended. For this - * to work, special care has to be put into how these respond to events. These - * tasks could be hardcoded inside events but then they wouldn't be easily - * auto-testable. These tasks accept factories that are homonymous to the events - * on the same + * @format */ -const ErrorCode = require('./errorCode') -const Key = require('./key') -const Schema = require('./schema') -const Utils = require('./utils') +const ErrorCode = require('../errorCode') +const Key = require('../key') +const Schema = require('../schema') +const Utils = require('../utils') /** - * @typedef {import('./SimpleGUN').GUNNode} GUNNode - * @typedef {import('./SimpleGUN').ISEA} ISEA - * @typedef {import('./SimpleGUN').UserGUNNode} UserGUNNode + * @typedef {import('../SimpleGUN').GUNNode} GUNNode + * @typedef {import('../SimpleGUN').ISEA} ISEA + * @typedef {import('../SimpleGUN').UserGUNNode} UserGUNNode */ /** @@ -148,6 +139,4 @@ const onAcceptedRequests = async (user, SEA) => { }) } -module.exports = { - onAcceptedRequests -} +module.exports = onAcceptedRequests diff --git a/services/gunDB/contact-api/jobs/onOrders.js b/services/gunDB/contact-api/jobs/onOrders.js new file mode 100644 index 00000000..95043e34 --- /dev/null +++ b/services/gunDB/contact-api/jobs/onOrders.js @@ -0,0 +1,82 @@ +/** + * @format + */ +const ErrorCode = require('../errorCode') +const Key = require('../key') +const Schema = require('../schema') +const Utils = require('../utils') + +/** + * @typedef {import('../SimpleGUN').GUNNode} GUNNode + * @typedef {import('../SimpleGUN').ListenerData} ListenerData + * @typedef {import('../SimpleGUN').ISEA} ISEA + * @typedef {import('../SimpleGUN').UserGUNNode} UserGUNNode + */ + +let currentOrderAddr = '' + +/** + * @param {string} addr + * @param {UserGUNNode} user + * @param {ISEA} SEA + * @returns {(order: ListenerData, orderID: string) => void} + */ +const listenerForAddr = (addr, user, SEA) => async (order, orderID) => { + if (addr !== currentOrderAddr) { + return + } + + if (!Schema.isOrder(order)) { + throw new Error(`Expected an order instead got: ${JSON.stringify(order)}`) + } + + const senderEpub = await Utils.pubToEpub(order.from) + const secret = await SEA.secret(senderEpub, user._.sea) + + const amount = Number(await SEA.decrypt(order.amount, secret)) + const memo = await SEA.decrypt(order.memo, secret) + + // create invoice here.. + + const invoice = `INVOICE_${amount}_${memo}` + + const encInvoice = await SEA.encrypt(invoice, secret) + + user + .get(Key.ORDER_TO_RESPONSE) + .get(orderID) + .put(encInvoice, ack => { + if (ack.err) { + console.error(`error saving order response: ${ack.err}`) + } + }) +} + +/** + * @param {UserGUNNode} user + * @param {GUNNode} gun + * @param {ISEA} SEA + * @throws {Error} NOT_AUTH + * @returns {void} + */ +const onOrders = (user, gun, SEA) => { + if (!user.is) { + throw new Error(ErrorCode.NOT_AUTH) + } + + user.get(Key.CURRENT_ORDER_ADDRESS).on(addr => { + if (typeof addr !== 'string') { + throw new TypeError('Expected current order address to be an string') + } + + currentOrderAddr = addr + + gun + .get(Key.ORDER_NODES) + .get(addr) + .map() + .on(listenerForAddr(currentOrderAddr, user, SEA)) + }) +} + +module.exports = onOrders From 17ed0f6a9dadc93f3fcc0389cf3d6c4aadbb634b Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Mon, 13 Jan 2020 16:06:28 -0400 Subject: [PATCH 07/24] reject correctly --- services/gunDB/contact-api/actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/gunDB/contact-api/actions.js b/services/gunDB/contact-api/actions.js index 160f6934..059d9496 100644 --- a/services/gunDB/contact-api/actions.js +++ b/services/gunDB/contact-api/actions.js @@ -650,7 +650,7 @@ const sendMessage = async (recipientPublicKey, body, user, SEA) => { .get(Key.MESSAGES) .set(newMessage, ack => { if (ack.err) { - rej(ack.err) + rej(new Error(ack.err)) } else { res() } From 107b3cde4c8a3d393e078adfdfd6b9afad732977 Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Mon, 13 Jan 2020 16:10:27 -0400 Subject: [PATCH 08/24] basic delete message --- services/gunDB/contact-api/actions.js | 59 +++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/services/gunDB/contact-api/actions.js b/services/gunDB/contact-api/actions.js index 059d9496..5df71f80 100644 --- a/services/gunDB/contact-api/actions.js +++ b/services/gunDB/contact-api/actions.js @@ -658,6 +658,64 @@ const sendMessage = async (recipientPublicKey, body, user, SEA) => { }) } +/** + * @param {string} recipientPub + * @param {string} msgID + * @param {UserGUNNode} user + * @param {ISEA} SEA + * @returns {Promise} + */ +const deleteMessage = async (recipientPub, msgID, user, SEA) => { + if (!user.is) { + throw new Error(ErrorCode.NOT_AUTH) + } + + if (typeof recipientPub !== 'string') { + throw new TypeError( + `expected recipientPublicKey to be an string, but instead got: ${typeof recipientPub}` + ) + } + + if (recipientPub.length === 0) { + throw new TypeError( + 'expected recipientPublicKey to be an string of length greater than zero' + ) + } + + if (typeof msgID !== 'string') { + throw new TypeError( + `expected msgID to be an string, instead got: ${typeof msgID}` + ) + } + + if (msgID.length === 0) { + throw new TypeError( + 'expected msgID to be an string of length greater than zero' + ) + } + + const outgoingID = await Utils.recipientToOutgoingID(recipientPub, user, SEA) + + if (outgoingID === null) { + throw new Error(`Could not fetch an outgoing id for user: ${recipientPub}`) + } + + return new Promise((res, rej) => { + user + .get(Key.OUTGOINGS) + .get(outgoingID) + .get(Key.MESSAGES) + .get(msgID) + .put(null, ack => { + if (ack.err) { + rej(new Error(ack.err)) + } else { + res() + } + }) + }) +} + /** * @param {string|null} avatar * @param {UserGUNNode} user @@ -781,6 +839,7 @@ module.exports = { blacklist, generateHandshakeAddress, sendHandshakeRequest, + deleteMessage, sendMessage, sendHRWithInitialMsg, setAvatar, From ffe28744d95393b57e227bc80a2933a250a49e07 Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Mon, 13 Jan 2020 16:11:16 -0400 Subject: [PATCH 09/24] send payment action --- services/gunDB/contact-api/actions.js | 89 ++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/services/gunDB/contact-api/actions.js b/services/gunDB/contact-api/actions.js index 5df71f80..c663f353 100644 --- a/services/gunDB/contact-api/actions.js +++ b/services/gunDB/contact-api/actions.js @@ -3,6 +3,7 @@ */ const uuidv1 = require('uuid/v1') const ErrorCode = require('./errorCode') +const Getters = require('./getters') const Key = require('./key') const Utils = require('./utils') const { isHandshakeRequest } = require('./schema') @@ -15,6 +16,8 @@ const { isHandshakeRequest } = require('./schema') * @typedef {import('./schema').Message} Message * @typedef {import('./schema').Outgoing} Outgoing * @typedef {import('./schema').PartialOutgoing} PartialOutgoing + * @typedef {import('./schema').Order} Order + * @typedef {import('./SimpleGUN').Ack} Ack */ /** @@ -831,6 +834,89 @@ const sendHRWithInitialMsg = async ( await sendMessage(recipientPublicKey, initialMsg, user, SEA) } +/** + * @param {string} to + * @param {number} amount + * @param {string} memo + * @param {GUNNode} gun + * @param {UserGUNNode} user + * @param {ISEA} SEA + * @throws {Error} If no response in less than 20 seconds from the recipient, or + * lightning cannot find a route for the payment. + * @returns {Promise} + */ +const sendPayment = async (to, amount, memo, gun, user, SEA) => { + const recipientEpub = await Utils.pubToEpub(to) + const ourSecret = await SEA.secret(recipientEpub, user._.sea) + + if (amount < 1) { + throw new RangeError('Amount must be at least 1 sat.') + } + + /** @type {Order} */ + const order = { + amount: await SEA.encrypt(amount.toString(), ourSecret), + from: user._.sea.pub, + memo: await SEA.encrypt(memo, ourSecret), + timestamp: Date.now() + } + + const currOrderAddress = await Getters.currentOrderAddress(to, gun) + + order.timestamp = Date.now() + + /** @type {string} */ + const orderID = await new Promise((res, rej) => { + const ord = gun + .get(Key.ORDER_NODES) + .get(currOrderAddress) + .set(order, ack => { + if (ack.err) { + rej( + new Error( + `Error writing order to order node: ${currOrderAddress} for pub: ${to}: ${ack.err}` + ) + ) + } else { + res(ord._.get) + } + }) + }) + + const bob = gun.user(to) + + /** @type {string} */ + const invoice = await Promise.race([ + new Promise((res, rej) => { + bob + .get(Key.ORDER_TO_RESPONSE) + .get(orderID) + .on(data => { + if (typeof data !== 'string') { + rej( + `Expected order response from pub ${to} to be an string, instead got: ${typeof data}` + ) + } else { + res(data) + } + }) + }), + new Promise((_, rej) => { + setTimeout(() => { + rej(new Error(ErrorCode.ORDER_NOT_ANSWERED_IN_TIME)) + }, 20000) + }) + ]) + + if (Math.random() > 0.5) { + return Promise.resolve() + } + + return Promise.reject( + new Error('Lightning could not find a route to pay invoice: ' + invoice) + ) +} + module.exports = { INITIAL_MSG, __createOutgoingFeed, @@ -843,5 +929,6 @@ module.exports = { sendMessage, sendHRWithInitialMsg, setAvatar, - setDisplayName + setDisplayName, + sendPayment } From 3cd17357febd29793195a0e1c0b0b0db27c82bef Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Mon, 13 Jan 2020 16:16:02 -0400 Subject: [PATCH 10/24] spin up onOrders job on auth --- services/gunDB/Mediator/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/gunDB/Mediator/index.js b/services/gunDB/Mediator/index.js index be27bf87..35a2debf 100644 --- a/services/gunDB/Mediator/index.js +++ b/services/gunDB/Mediator/index.js @@ -154,6 +154,7 @@ const authenticate = async (alias, pass) => { } // move this to a subscription; implement off() ? todo API.Jobs.onAcceptedRequests(user, mySEA) + API.Jobs.onOrders(user, gun, mySEA) return user._.sea.pub } @@ -177,6 +178,7 @@ const authenticate = async (alias, pass) => { throw new Error(ack.err) } else if (typeof ack.sea === 'object') { API.Jobs.onAcceptedRequests(user, mySEA) + API.Jobs.onOrders(user, gun, mySEA) const mySec = await mySEA.secret(user._.sea.epub, user._.sea) if (typeof mySec !== 'string') { From 9a10bf324f0420d4089b6e6028ecf7387c69c8d5 Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Mon, 13 Jan 2020 16:19:57 -0400 Subject: [PATCH 11/24] send payment action constant --- services/gunDB/action-constants.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/gunDB/action-constants.js b/services/gunDB/action-constants.js index ac17c7a1..440e7bcf 100644 --- a/services/gunDB/action-constants.js +++ b/services/gunDB/action-constants.js @@ -5,6 +5,7 @@ const Actions = { SEND_HANDSHAKE_REQUEST: "SEND_HANDSHAKE_REQUEST", SEND_HANDSHAKE_REQUEST_WITH_INITIAL_MSG: "SEND_HANDSHAKE_REQUEST_WITH_INITIAL_MSG", SEND_MESSAGE: "SEND_MESSAGE", + SEND_PAYMENT: "SEND_PAYMENT", SET_AVATAR: "SET_AVATAR", SET_DISPLAY_NAME: "SET_DISPLAY_NAME" }; From 4947a4dbb6db6ad65cb3a91c1f28571b80e7c36a Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Mon, 13 Jan 2020 16:26:00 -0400 Subject: [PATCH 12/24] wire sendpayment to socket --- services/gunDB/Mediator/index.js | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/services/gunDB/Mediator/index.js b/services/gunDB/Mediator/index.js index 35a2debf..c7b520f6 100644 --- a/services/gunDB/Mediator/index.js +++ b/services/gunDB/Mediator/index.js @@ -296,6 +296,7 @@ class Mediator { this.sendHRWithInitialMsg ) socket.on(Action.SEND_MESSAGE, this.sendMessage) + socket.on(Action.SEND_PAYMENT, this.sendPayment) socket.on(Action.SET_AVATAR, this.setAvatar) socket.on(Action.SET_DISPLAY_NAME, this.setDisplayName) @@ -563,6 +564,39 @@ class Mediator { } } + /** + * @param {Readonly<{ uuid: string, recipientPub: string, amount: number, memo: string, token: string }>} reqBody + */ + sendPayment = async reqBody => { + try { + const { recipientPub, amount, memo, token } = reqBody + + await throwOnInvalidToken(token) + + await API.Actions.sendPayment( + recipientPub, + amount, + memo, + gun, + user, + mySEA + ) + + this.socket.emit(Action.SEND_PAYMENT, { + ok: true, + msg: null, + origBody: reqBody + }) + } catch (err) { + console.log(err) + this.socket.emit(Action.SEND_PAYMENT, { + ok: false, + msg: err.message, + origBody: reqBody + }) + } + } + /** * @param {Readonly<{ avatar: string|null , token: string }>} body */ From 20b80bea1323ff3c1417300d2a81c70137d1ee1c Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Mon, 13 Jan 2020 18:57:50 -0400 Subject: [PATCH 13/24] encrypted strings must be populated --- services/gunDB/contact-api/actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/gunDB/contact-api/actions.js b/services/gunDB/contact-api/actions.js index c663f353..73db8fbc 100644 --- a/services/gunDB/contact-api/actions.js +++ b/services/gunDB/contact-api/actions.js @@ -857,7 +857,7 @@ const sendPayment = async (to, amount, memo, gun, user, SEA) => { const order = { amount: await SEA.encrypt(amount.toString(), ourSecret), from: user._.sea.pub, - memo: await SEA.encrypt(memo, ourSecret), + memo: await SEA.encrypt(memo || 'no memo', ourSecret), timestamp: Date.now() } From 80c2874bbb18d8a000de88c7039b063b238f7675 Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Mon, 13 Jan 2020 19:18:23 -0400 Subject: [PATCH 14/24] gen order address & at register --- services/gunDB/Mediator/index.js | 1 + services/gunDB/contact-api/actions.js | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/services/gunDB/Mediator/index.js b/services/gunDB/Mediator/index.js index c7b520f6..79b671cf 100644 --- a/services/gunDB/Mediator/index.js +++ b/services/gunDB/Mediator/index.js @@ -951,6 +951,7 @@ const register = async (alias, pass) => { return authenticate(alias, pass).then(async pub => { await API.Actions.setDisplayName('anon' + pub.slice(0, 8), user) await API.Actions.generateHandshakeAddress(user) + await API.Actions.generateOrderAddress(user) return pub }) } diff --git a/services/gunDB/contact-api/actions.js b/services/gunDB/contact-api/actions.js index 73db8fbc..fe881211 100644 --- a/services/gunDB/contact-api/actions.js +++ b/services/gunDB/contact-api/actions.js @@ -917,6 +917,27 @@ const sendPayment = async (to, amount, memo, gun, user, SEA) => { ) } +/** + * @param {UserGUNNode} user + * @returns {Promise} + */ +const generateOrderAddress = user => + new Promise((res, rej) => { + if (!user.is) { + throw new Error(ErrorCode.NOT_AUTH) + } + + const address = uuidv1() + + user.get(Key.CURRENT_ORDER_ADDRESS).put(address, ack => { + if (ack.err) { + rej(new Error(ack.err)) + } else { + res() + } + }) + }) + module.exports = { INITIAL_MSG, __createOutgoingFeed, @@ -930,5 +951,6 @@ module.exports = { sendHRWithInitialMsg, setAvatar, setDisplayName, - sendPayment + sendPayment, + generateOrderAddress } From 3e2cea2b86fa68b3bfef9e1cfe3a726a5c8a6f6b Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Mon, 13 Jan 2020 19:19:25 -0400 Subject: [PATCH 15/24] decode invoice --- services/gunDB/contact-api/actions.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/gunDB/contact-api/actions.js b/services/gunDB/contact-api/actions.js index fe881211..eb052000 100644 --- a/services/gunDB/contact-api/actions.js +++ b/services/gunDB/contact-api/actions.js @@ -912,8 +912,10 @@ const sendPayment = async (to, amount, memo, gun, user, SEA) => { return Promise.resolve() } + const decInvoice = await SEA.decrypt(invoice, ourSecret) + return Promise.reject( - new Error('Lightning could not find a route to pay invoice: ' + invoice) + new Error('Lightning could not find a route to pay invoice: ' + decInvoice) ) } From 6dcc0d2d04add6d590d2e749be0d72ed8b4af8ae Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Mon, 13 Jan 2020 21:28:32 -0400 Subject: [PATCH 16/24] generate invoice for order --- services/gunDB/contact-api/jobs/onOrders.js | 30 +++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/services/gunDB/contact-api/jobs/onOrders.js b/services/gunDB/contact-api/jobs/onOrders.js index 95043e34..fcd13376 100644 --- a/services/gunDB/contact-api/jobs/onOrders.js +++ b/services/gunDB/contact-api/jobs/onOrders.js @@ -1,6 +1,9 @@ /** * @format */ + +const LightningServices = require('../../../../utils/lightningServices') + const ErrorCode = require('../errorCode') const Key = require('../key') const Schema = require('../schema') @@ -36,9 +39,32 @@ const listenerForAddr = (addr, user, SEA) => async (order, orderID) => { const amount = Number(await SEA.decrypt(order.amount, secret)) const memo = await SEA.decrypt(order.memo, secret) - // create invoice here.. + /** + * @type {[any , string]} + */ + const [err, invoice] = await new Promise(resolve => { + const { + services: { lightning } + } = LightningServices - const invoice = `INVOICE_${amount}_${memo}` + lightning.addInvoice( + { + expiry: 36000, + memo, + value: amount + }, + ( + /** @type {any} */ error, + /** @type {{ payment_request: string }} */ response + ) => { + resolve([error, response.payment_request]) + } + ) + }) + + if (err) { + console.log(new Error(err)) + } const encInvoice = await SEA.encrypt(invoice, secret) From 88abc9a2a9c9aa4368ad5829eb73a94744cdf289 Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Mon, 13 Jan 2020 21:28:40 -0400 Subject: [PATCH 17/24] disable rule --- .eslintrc.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.eslintrc.json b/.eslintrc.json index 8bd8ef46..bfcdeedb 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -62,7 +62,10 @@ "no-shadow": "off", // We're usually throwing objects throughout the API to allow for more detailed error messages - "no-throw-literal": "off" + "no-throw-literal": "off", + + // lightning has sync methods and this rule bans them + "no-sync": "off" }, "parser": "babel-eslint", "env": { From 0734cb6cb924748cdde834aa955b400217277f7d Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Mon, 13 Jan 2020 21:28:50 -0400 Subject: [PATCH 18/24] send payment when order invoice is received --- services/gunDB/contact-api/actions.js | 30 ++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/services/gunDB/contact-api/actions.js b/services/gunDB/contact-api/actions.js index eb052000..b9f4da32 100644 --- a/services/gunDB/contact-api/actions.js +++ b/services/gunDB/contact-api/actions.js @@ -2,6 +2,9 @@ * @format */ const uuidv1 = require('uuid/v1') + +const LightningServices = require('../../../utils/lightningServices') + const ErrorCode = require('./errorCode') const Getters = require('./getters') const Key = require('./key') @@ -908,15 +911,28 @@ const sendPayment = async (to, amount, memo, gun, user, SEA) => { }) ]) - if (Math.random() > 0.5) { - return Promise.resolve() - } - const decInvoice = await SEA.decrypt(invoice, ourSecret) - return Promise.reject( - new Error('Lightning could not find a route to pay invoice: ' + decInvoice) - ) + const { + services: { lightning } + } = LightningServices + + await new Promise((rej, resolve) => { + lightning.sendPaymentSync( + { + payment_request: decInvoice + }, + (/** @type {any} */ err, /** @type {any} */ res) => { + if (err) { + console.log(err) + rej(err) + } else { + console.log(res) + resolve() + } + } + ) + }) } /** From 4e24f81f8af0ca364384befdbb0f4234b69b09e8 Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Mon, 13 Jan 2020 21:55:27 -0400 Subject: [PATCH 19/24] better error handling --- services/gunDB/contact-api/getters.js | 1 + services/gunDB/contact-api/jobs/onOrders.js | 86 +++++++++++---------- 2 files changed, 48 insertions(+), 39 deletions(-) diff --git a/services/gunDB/contact-api/getters.js b/services/gunDB/contact-api/getters.js index 7687fdbb..f439512a 100644 --- a/services/gunDB/contact-api/getters.js +++ b/services/gunDB/contact-api/getters.js @@ -1,4 +1,5 @@ const Key = require('./key') + /** * @param {string} pub * @param {import('./SimpleGUN').GUNNode} gun diff --git a/services/gunDB/contact-api/jobs/onOrders.js b/services/gunDB/contact-api/jobs/onOrders.js index fcd13376..3db6e350 100644 --- a/services/gunDB/contact-api/jobs/onOrders.js +++ b/services/gunDB/contact-api/jobs/onOrders.js @@ -25,57 +25,65 @@ let currentOrderAddr = '' * @returns {(order: ListenerData, orderID: string) => void} */ const listenerForAddr = (addr, user, SEA) => async (order, orderID) => { - if (addr !== currentOrderAddr) { - return - } + try { + if (addr !== currentOrderAddr) { + return + } - if (!Schema.isOrder(order)) { - throw new Error(`Expected an order instead got: ${JSON.stringify(order)}`) - } + if (!Schema.isOrder(order)) { + throw new Error(`Expected an order instead got: ${JSON.stringify(order)}`) + } - const senderEpub = await Utils.pubToEpub(order.from) - const secret = await SEA.secret(senderEpub, user._.sea) + const orderToResponse = user.get(Key.ORDER_TO_RESPONSE) - const amount = Number(await SEA.decrypt(order.amount, secret)) - const memo = await SEA.decrypt(order.memo, secret) + if (await orderToResponse.get(orderID).then()) { + return + } - /** - * @type {[any , string]} - */ - const [err, invoice] = await new Promise(resolve => { - const { - services: { lightning } - } = LightningServices + const senderEpub = await Utils.pubToEpub(order.from) + const secret = await SEA.secret(senderEpub, user._.sea) - lightning.addInvoice( - { - expiry: 36000, - memo, - value: amount - }, - ( - /** @type {any} */ error, - /** @type {{ payment_request: string }} */ response - ) => { - resolve([error, response.payment_request]) - } - ) - }) + const amount = Number(await SEA.decrypt(order.amount, secret)) + const memo = await SEA.decrypt(order.memo, secret) - if (err) { - console.log(new Error(err)) - } + /** + * @type {string} + */ + const invoice = await new Promise((resolve, rej) => { + const { + services: { lightning } + } = LightningServices - const encInvoice = await SEA.encrypt(invoice, secret) + lightning.addInvoice( + { + expiry: 36000, + memo, + value: amount + }, + ( + /** @type {any} */ error, + /** @type {{ payment_request: string }} */ response + ) => { + if (error) { + rej(error) + } else { + resolve(response.payment_request) + } + } + ) + }) - user - .get(Key.ORDER_TO_RESPONSE) - .get(orderID) - .put(encInvoice, ack => { + const encInvoice = await SEA.encrypt(invoice, secret) + + orderToResponse.get(orderID).put(encInvoice, ack => { if (ack.err) { console.error(`error saving order response: ${ack.err}`) } }) + } catch (err) { + console.error('error inside onOrders:') + console.error(err) + } } /** From a72f6dd09181ecdd77cc9697d59c999a4f32a85c Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Mon, 13 Jan 2020 22:05:53 -0400 Subject: [PATCH 20/24] check auth --- services/gunDB/contact-api/actions.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/gunDB/contact-api/actions.js b/services/gunDB/contact-api/actions.js index b9f4da32..9a994731 100644 --- a/services/gunDB/contact-api/actions.js +++ b/services/gunDB/contact-api/actions.js @@ -849,6 +849,10 @@ const sendHRWithInitialMsg = async ( * @returns {Promise} */ const sendPayment = async (to, amount, memo, gun, user, SEA) => { + if (!user.is) { + throw new Error(ErrorCode.NOT_AUTH) + } + const recipientEpub = await Utils.pubToEpub(to) const ourSecret = await SEA.secret(recipientEpub, user._.sea) From c3fff0a432278dc6e6bbbb71085308a6cd369f3f Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Tue, 14 Jan 2020 18:08:18 -0400 Subject: [PATCH 21/24] timeout for getter --- services/gunDB/contact-api/actions.js | 2 +- services/gunDB/contact-api/getters.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/services/gunDB/contact-api/actions.js b/services/gunDB/contact-api/actions.js index 9a994731..e81d5ff5 100644 --- a/services/gunDB/contact-api/actions.js +++ b/services/gunDB/contact-api/actions.js @@ -868,7 +868,7 @@ const sendPayment = async (to, amount, memo, gun, user, SEA) => { timestamp: Date.now() } - const currOrderAddress = await Getters.currentOrderAddress(to, gun) + const currOrderAddress = await Getters.currentOrderAddress(to) order.timestamp = Date.now() diff --git a/services/gunDB/contact-api/getters.js b/services/gunDB/contact-api/getters.js index f439512a..fa704e37 100644 --- a/services/gunDB/contact-api/getters.js +++ b/services/gunDB/contact-api/getters.js @@ -1,12 +1,12 @@ const Key = require('./key') +const Utils = require('./utils') /** * @param {string} pub - * @param {import('./SimpleGUN').GUNNode} gun * @returns {Promise} */ -exports.currentOrderAddress = async (pub, gun) => { - const currAddr = await gun.user(pub).get(Key.CURRENT_ORDER_ADDRESS).then() +exports.currentOrderAddress = async (pub) => { + const currAddr = await Utils.tryAndWait((gun) => gun.user(pub).get(Key.CURRENT_ORDER_ADDRESS).then()) if (typeof currAddr !== 'string') { throw new TypeError('Expected user.currentOrderAddress to be an string') From 8ce9e65a8dd76c6b11464ec013901ce12c4314d5 Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Tue, 14 Jan 2020 18:26:14 -0400 Subject: [PATCH 22/24] better handle of sendpaymentsync response --- services/gunDB/contact-api/actions.js | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/services/gunDB/contact-api/actions.js b/services/gunDB/contact-api/actions.js index e81d5ff5..a9c29cfb 100644 --- a/services/gunDB/contact-api/actions.js +++ b/services/gunDB/contact-api/actions.js @@ -921,18 +921,31 @@ const sendPayment = async (to, amount, memo, gun, user, SEA) => { services: { lightning } } = LightningServices + /** + * Partial + * https://api.lightning.community/#grpc-response-sendresponse-2 + * @typedef {object} SendResponse + * @prop {string|null} payment_error + * @prop {any[]|null} payment_route + */ + await new Promise((rej, resolve) => { lightning.sendPaymentSync( { payment_request: decInvoice }, - (/** @type {any} */ err, /** @type {any} */ res) => { - if (err) { - console.log(err) - rej(err) - } else { - console.log(res) + (_, /** @type {SendResponse} */ res) => { + if (res.payment_error) { + rej(new Error(res.payment_error)) + } else if (res.payment_route) { resolve() + } else { + rej( + new Error( + 'Unexpected response from sendPaymentSync() -> ' + + JSON.stringify(res) + ) + ) } } ) From 47f37b06d8f600f0a76cc7b5db9e76cf7b1e4fb2 Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Tue, 14 Jan 2020 18:33:20 -0400 Subject: [PATCH 23/24] stringent sendpaymentsync handling --- services/gunDB/contact-api/actions.js | 36 +++++++++++++++++++-------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/services/gunDB/contact-api/actions.js b/services/gunDB/contact-api/actions.js index a9c29cfb..ad9bf1d3 100644 --- a/services/gunDB/contact-api/actions.js +++ b/services/gunDB/contact-api/actions.js @@ -934,18 +934,32 @@ const sendPayment = async (to, amount, memo, gun, user, SEA) => { { payment_request: decInvoice }, - (_, /** @type {SendResponse} */ res) => { - if (res.payment_error) { - rej(new Error(res.payment_error)) - } else if (res.payment_route) { - resolve() - } else { - rej( - new Error( - 'Unexpected response from sendPaymentSync() -> ' + - JSON.stringify(res) + (/*** @type {any} */ err, /** @type {SendResponse} */ res) => { + console.log(`sendPaymentSync err: ${JSON.stringify(err)}`) + console.log(`sendPaymentSync res: ${JSON.stringify(res)}`) + + if (err || typeof err === 'number') { + rej(new Error(`sendPaymentSync error: ${JSON.stringify(err)}`)) + } else if (res) { + if (res.payment_error) { + rej( + new Error( + `sendPaymentSync error response: ${JSON.stringify(res)}` + ) ) - ) + } else if (!res.payment_route) { + rej( + new Error( + `sendPaymentSync no payment route response: ${JSON.stringify( + res + )}` + ) + ) + } else { + resolve() + } + } else { + rej(new Error('no error or response received from sendPaymentSync')) } } ) From e8ad0b854c99b5ac6c013e67b52f7142304b9465 Mon Sep 17 00:00:00 2001 From: Daniel Lugo Date: Tue, 14 Jan 2020 18:42:10 -0400 Subject: [PATCH 24/24] exact error handling --- services/gunDB/contact-api/actions.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/services/gunDB/contact-api/actions.js b/services/gunDB/contact-api/actions.js index ad9bf1d3..b6b7de2b 100644 --- a/services/gunDB/contact-api/actions.js +++ b/services/gunDB/contact-api/actions.js @@ -921,6 +921,11 @@ const sendPayment = async (to, amount, memo, gun, user, SEA) => { services: { lightning } } = LightningServices + /** + * @typedef {object} SendErr + * @prop {string} details + */ + /** * Partial * https://api.lightning.community/#grpc-response-sendresponse-2 @@ -929,17 +934,14 @@ const sendPayment = async (to, amount, memo, gun, user, SEA) => { * @prop {any[]|null} payment_route */ - await new Promise((rej, resolve) => { + await new Promise((resolve, rej) => { lightning.sendPaymentSync( { payment_request: decInvoice }, - (/*** @type {any} */ err, /** @type {SendResponse} */ res) => { - console.log(`sendPaymentSync err: ${JSON.stringify(err)}`) - console.log(`sendPaymentSync res: ${JSON.stringify(res)}`) - - if (err || typeof err === 'number') { - rej(new Error(`sendPaymentSync error: ${JSON.stringify(err)}`)) + (/** @type {SendErr=} */ err, /** @type {SendResponse} */ res) => { + if (err) { + rej(new Error(err.details)) } else if (res) { if (res.payment_error) { rej(