Merge branch 'master' into feature/end-to-end-encryption
This commit is contained in:
commit
d621e659be
14 changed files with 694 additions and 40 deletions
|
|
@ -62,7 +62,10 @@
|
||||||
|
|
||||||
"no-shadow": "off",
|
"no-shadow": "off",
|
||||||
// We're usually throwing objects throughout the API to allow for more detailed error messages
|
// 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",
|
"parser": "babel-eslint",
|
||||||
"env": {
|
"env": {
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,7 @@ const authenticate = async (alias, pass) => {
|
||||||
}
|
}
|
||||||
// move this to a subscription; implement off() ? todo
|
// move this to a subscription; implement off() ? todo
|
||||||
API.Jobs.onAcceptedRequests(user, mySEA)
|
API.Jobs.onAcceptedRequests(user, mySEA)
|
||||||
|
API.Jobs.onOrders(user, gun, mySEA)
|
||||||
return user._.sea.pub
|
return user._.sea.pub
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,6 +180,7 @@ const authenticate = async (alias, pass) => {
|
||||||
throw new Error(ack.err)
|
throw new Error(ack.err)
|
||||||
} else if (typeof ack.sea === 'object') {
|
} else if (typeof ack.sea === 'object') {
|
||||||
API.Jobs.onAcceptedRequests(user, mySEA)
|
API.Jobs.onAcceptedRequests(user, mySEA)
|
||||||
|
API.Jobs.onOrders(user, gun, mySEA)
|
||||||
|
|
||||||
const mySec = await mySEA.secret(user._.sea.epub, user._.sea)
|
const mySec = await mySEA.secret(user._.sea.epub, user._.sea)
|
||||||
if (typeof mySec !== 'string') {
|
if (typeof mySec !== 'string') {
|
||||||
|
|
@ -301,6 +303,8 @@ class Mediator {
|
||||||
this.socket.on(Action.SEND_MESSAGE, this.sendMessage)
|
this.socket.on(Action.SEND_MESSAGE, this.sendMessage)
|
||||||
this.socket.on(Action.SET_AVATAR, this.setAvatar)
|
this.socket.on(Action.SET_AVATAR, this.setAvatar)
|
||||||
this.socket.on(Action.SET_DISPLAY_NAME, this.setDisplayName)
|
this.socket.on(Action.SET_DISPLAY_NAME, this.setDisplayName)
|
||||||
|
this.socket.on(Action.SEND_PAYMENT, this.sendPayment)
|
||||||
|
this.socket.on(Action.SET_BIO, this.setBio)
|
||||||
|
|
||||||
this.socket.on(Event.ON_AVATAR, this.onAvatar)
|
this.socket.on(Event.ON_AVATAR, this.onAvatar)
|
||||||
this.socket.on(Event.ON_BLACKLIST, this.onBlacklist)
|
this.socket.on(Event.ON_BLACKLIST, this.onBlacklist)
|
||||||
|
|
@ -309,6 +313,8 @@ class Mediator {
|
||||||
this.socket.on(Event.ON_HANDSHAKE_ADDRESS, this.onHandshakeAddress)
|
this.socket.on(Event.ON_HANDSHAKE_ADDRESS, this.onHandshakeAddress)
|
||||||
this.socket.on(Event.ON_RECEIVED_REQUESTS, this.onReceivedRequests)
|
this.socket.on(Event.ON_RECEIVED_REQUESTS, this.onReceivedRequests)
|
||||||
this.socket.on(Event.ON_SENT_REQUESTS, this.onSentRequests)
|
this.socket.on(Event.ON_SENT_REQUESTS, this.onSentRequests)
|
||||||
|
this.socket.on(Event.ON_BIO, this.onBio)
|
||||||
|
this.socket.on(Event.ON_SEED_BACKUP, this.onSeedBackup)
|
||||||
|
|
||||||
this.socket.on(IS_GUN_AUTH, this.isGunAuth)
|
this.socket.on(IS_GUN_AUTH, this.isGunAuth)
|
||||||
}
|
}
|
||||||
|
|
@ -640,6 +646,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
|
* @param {Readonly<{ avatar: string|null , token: string }>} body
|
||||||
*/
|
*/
|
||||||
|
|
@ -934,6 +973,88 @@ class Mediator {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Readonly<{ token: string }>} body
|
||||||
|
*/
|
||||||
|
onBio = async body => {
|
||||||
|
try {
|
||||||
|
const { token } = body
|
||||||
|
|
||||||
|
await throwOnInvalidToken(token)
|
||||||
|
|
||||||
|
API.Events.onBio(bio => {
|
||||||
|
this.socket.emit(Event.ON_BIO, {
|
||||||
|
msg: bio,
|
||||||
|
ok: true,
|
||||||
|
origBody: body
|
||||||
|
})
|
||||||
|
}, user)
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
this.socket.emit(Event.ON_BIO, {
|
||||||
|
ok: false,
|
||||||
|
msg: err.message,
|
||||||
|
origBody: body
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Readonly<{ bio: string|null , token: string }>} body
|
||||||
|
*/
|
||||||
|
setBio = async body => {
|
||||||
|
try {
|
||||||
|
const { bio, token } = body
|
||||||
|
|
||||||
|
await throwOnInvalidToken(token)
|
||||||
|
|
||||||
|
await API.Actions.setBio(bio, user)
|
||||||
|
|
||||||
|
this.socket.emit(Action.SET_BIO, {
|
||||||
|
ok: true,
|
||||||
|
msg: null,
|
||||||
|
origBody: body
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
this.socket.emit(Action.SET_BIO, {
|
||||||
|
ok: false,
|
||||||
|
msg: err.message,
|
||||||
|
origBody: body
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Readonly<{ token: string }>} body
|
||||||
|
*/
|
||||||
|
onSeedBackup = async body => {
|
||||||
|
try {
|
||||||
|
const { token } = body
|
||||||
|
|
||||||
|
await throwOnInvalidToken(token)
|
||||||
|
|
||||||
|
await API.Events.onSeedBackup(
|
||||||
|
seedBackup => {
|
||||||
|
this.socket.emit(Event.ON_SEED_BACKUP, {
|
||||||
|
ok: true,
|
||||||
|
msg: seedBackup,
|
||||||
|
origBody: body
|
||||||
|
})
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
mySEA
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
this.socket.emit(Event.ON_SEED_BACKUP, {
|
||||||
|
ok: false,
|
||||||
|
msg: err.message,
|
||||||
|
origBody: body
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -996,6 +1117,7 @@ const register = async (alias, pass) => {
|
||||||
return authenticate(alias, pass).then(async pub => {
|
return authenticate(alias, pass).then(async pub => {
|
||||||
await API.Actions.setDisplayName('anon' + pub.slice(0, 8), user)
|
await API.Actions.setDisplayName('anon' + pub.slice(0, 8), user)
|
||||||
await API.Actions.generateHandshakeAddress(user)
|
await API.Actions.generateHandshakeAddress(user)
|
||||||
|
await API.Actions.generateOrderAddress(user)
|
||||||
return pub
|
return pub
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -1033,5 +1155,6 @@ module.exports = {
|
||||||
register,
|
register,
|
||||||
instantiateGun,
|
instantiateGun,
|
||||||
getGun,
|
getGun,
|
||||||
getUser
|
getUser,
|
||||||
|
mySEA
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,10 @@ const Actions = {
|
||||||
SEND_HANDSHAKE_REQUEST: "SEND_HANDSHAKE_REQUEST",
|
SEND_HANDSHAKE_REQUEST: "SEND_HANDSHAKE_REQUEST",
|
||||||
SEND_HANDSHAKE_REQUEST_WITH_INITIAL_MSG: "SEND_HANDSHAKE_REQUEST_WITH_INITIAL_MSG",
|
SEND_HANDSHAKE_REQUEST_WITH_INITIAL_MSG: "SEND_HANDSHAKE_REQUEST_WITH_INITIAL_MSG",
|
||||||
SEND_MESSAGE: "SEND_MESSAGE",
|
SEND_MESSAGE: "SEND_MESSAGE",
|
||||||
|
SEND_PAYMENT: "SEND_PAYMENT",
|
||||||
SET_AVATAR: "SET_AVATAR",
|
SET_AVATAR: "SET_AVATAR",
|
||||||
SET_DISPLAY_NAME: "SET_DISPLAY_NAME"
|
SET_DISPLAY_NAME: "SET_DISPLAY_NAME",
|
||||||
|
SET_BIO: "SET_BIO"
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = Actions;
|
module.exports = Actions;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,11 @@
|
||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
const uuidv1 = require('uuid/v1')
|
const uuidv1 = require('uuid/v1')
|
||||||
|
|
||||||
|
const LightningServices = require('../../../utils/lightningServices')
|
||||||
|
|
||||||
const ErrorCode = require('./errorCode')
|
const ErrorCode = require('./errorCode')
|
||||||
|
const Getters = require('./getters')
|
||||||
const Key = require('./key')
|
const Key = require('./key')
|
||||||
const Utils = require('./utils')
|
const Utils = require('./utils')
|
||||||
const { isHandshakeRequest } = require('./schema')
|
const { isHandshakeRequest } = require('./schema')
|
||||||
|
|
@ -15,6 +19,8 @@ const { isHandshakeRequest } = require('./schema')
|
||||||
* @typedef {import('./schema').Message} Message
|
* @typedef {import('./schema').Message} Message
|
||||||
* @typedef {import('./schema').Outgoing} Outgoing
|
* @typedef {import('./schema').Outgoing} Outgoing
|
||||||
* @typedef {import('./schema').PartialOutgoing} PartialOutgoing
|
* @typedef {import('./schema').PartialOutgoing} PartialOutgoing
|
||||||
|
* @typedef {import('./schema').Order} Order
|
||||||
|
* @typedef {import('./SimpleGUN').Ack} Ack
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -408,18 +414,8 @@ const sendHandshakeRequest = async (recipientPublicKey, gun, user, SEA) => {
|
||||||
console.log('sendHR() -> before mySecret')
|
console.log('sendHR() -> before mySecret')
|
||||||
|
|
||||||
const mySecret = await SEA.secret(user._.sea.epub, user._.sea)
|
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')
|
console.log('sendHR() -> before ourSecret')
|
||||||
const ourSecret = await SEA.secret(recipientEpub, user._.sea)
|
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
|
// check if successful handshake is present
|
||||||
|
|
||||||
|
|
@ -660,7 +656,65 @@ const sendMessage = async (recipientPublicKey, body, user, SEA) => {
|
||||||
.get(Key.MESSAGES)
|
.get(Key.MESSAGES)
|
||||||
.set(newMessage, ack => {
|
.set(newMessage, ack => {
|
||||||
if (ack.err) {
|
if (ack.err) {
|
||||||
rej(ack.err)
|
rej(new Error(ack.err))
|
||||||
|
} else {
|
||||||
|
res()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} recipientPub
|
||||||
|
* @param {string} msgID
|
||||||
|
* @param {UserGUNNode} user
|
||||||
|
* @param {ISEA} SEA
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
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 {
|
} else {
|
||||||
res()
|
res()
|
||||||
}
|
}
|
||||||
|
|
@ -783,6 +837,220 @@ const sendHRWithInitialMsg = async (
|
||||||
await sendMessage(recipientPublicKey, initialMsg, user, SEA)
|
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<void>}
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
|
||||||
|
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 || 'no memo', ourSecret),
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
const currOrderAddress = await Getters.currentOrderAddress(to)
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
const decInvoice = await SEA.decrypt(invoice, ourSecret)
|
||||||
|
|
||||||
|
const {
|
||||||
|
services: { lightning }
|
||||||
|
} = LightningServices
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} SendErr
|
||||||
|
* @prop {string} details
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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((resolve, rej) => {
|
||||||
|
lightning.sendPaymentSync(
|
||||||
|
{
|
||||||
|
payment_request: decInvoice
|
||||||
|
},
|
||||||
|
(/** @type {SendErr=} */ err, /** @type {SendResponse} */ res) => {
|
||||||
|
if (err) {
|
||||||
|
rej(new Error(err.details))
|
||||||
|
} 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'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {UserGUNNode} user
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string|null} bio
|
||||||
|
* @param {UserGUNNode} user
|
||||||
|
* @throws {TypeError} Rejects if avatar is not an string or an empty string.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
const setBio = (bio, user) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
if (!user.is) {
|
||||||
|
throw new Error(ErrorCode.NOT_AUTH)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof bio === 'string' && bio.length === 0) {
|
||||||
|
throw new TypeError(
|
||||||
|
"'bio' must be an string and have length greater than one or be null"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof bio !== 'string' && bio !== null) {
|
||||||
|
throw new TypeError(
|
||||||
|
"'bio' must be an string and have length greater than one or be null"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
user.get(Key.BIO).put(bio, ack => {
|
||||||
|
if (ack.err) {
|
||||||
|
reject(new Error(ack.err))
|
||||||
|
} else {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} mnemonicPhrase
|
||||||
|
* @param {UserGUNNode} user
|
||||||
|
* @param {ISEA} SEA
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
const saveSeedBackup = async (mnemonicPhrase, user, SEA) => {
|
||||||
|
if (
|
||||||
|
!Array.isArray(mnemonicPhrase) ||
|
||||||
|
mnemonicPhrase.some(word => typeof word !== 'string') ||
|
||||||
|
mnemonicPhrase.length === 0
|
||||||
|
) {
|
||||||
|
throw new TypeError('expected mnemonicPhrase to be an string array')
|
||||||
|
}
|
||||||
|
|
||||||
|
const mySecret = await SEA.secret(user._.sea.epub, user._.sea)
|
||||||
|
const encryptedSeed = await SEA.encrypt(mnemonicPhrase.join(' '), mySecret)
|
||||||
|
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
user.get(Key.SEED_BACKUP).put(encryptedSeed, ack => {
|
||||||
|
if (ack.err) {
|
||||||
|
rej(ack.err)
|
||||||
|
} else {
|
||||||
|
res()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
INITIAL_MSG,
|
INITIAL_MSG,
|
||||||
__createOutgoingFeed,
|
__createOutgoingFeed,
|
||||||
|
|
@ -791,8 +1059,13 @@ module.exports = {
|
||||||
blacklist,
|
blacklist,
|
||||||
generateHandshakeAddress,
|
generateHandshakeAddress,
|
||||||
sendHandshakeRequest,
|
sendHandshakeRequest,
|
||||||
|
deleteMessage,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
sendHRWithInitialMsg,
|
sendHRWithInitialMsg,
|
||||||
setAvatar,
|
setAvatar,
|
||||||
setDisplayName
|
setDisplayName,
|
||||||
|
sendPayment,
|
||||||
|
generateOrderAddress,
|
||||||
|
setBio,
|
||||||
|
saveSeedBackup
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,3 +41,5 @@ exports.ALREADY_REQUESTED_HANDSHAKE = 'ALREADY_REQUESTED_HANDSHAKE'
|
||||||
exports.STALE_HANDSHAKE_ADDRESS = 'STALE_HANDSHAKE_ADDRESS'
|
exports.STALE_HANDSHAKE_ADDRESS = 'STALE_HANDSHAKE_ADDRESS'
|
||||||
|
|
||||||
exports.TIMEOUT_ERR = 'TIMEOUT_ERR'
|
exports.TIMEOUT_ERR = 'TIMEOUT_ERR'
|
||||||
|
|
||||||
|
exports.ORDER_NOT_ANSWERED_IN_TIME = 'ORDER_NOT_ANSWERED_IN_TIME'
|
||||||
|
|
|
||||||
|
|
@ -1204,6 +1204,60 @@ const onSimplerSentRequests = async (cb, gun, user, SEA) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {string|null} */
|
||||||
|
let currentBio = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {(bio: string|null) => void} cb
|
||||||
|
* @param {UserGUNNode} user Pass only for testing purposes.
|
||||||
|
* @throws {Error} If user hasn't been auth.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const onBio = (cb, user) => {
|
||||||
|
if (!user.is) {
|
||||||
|
throw new Error(ErrorCode.NOT_AUTH)
|
||||||
|
}
|
||||||
|
|
||||||
|
const callb = debounce(cb, DEBOUNCE_WAIT_TIME)
|
||||||
|
// Initial value if avvatar is undefined in gun
|
||||||
|
callb(currentBio)
|
||||||
|
|
||||||
|
user.get(Key.BIO).on(bio => {
|
||||||
|
if (typeof bio === 'string' || bio === null) {
|
||||||
|
currentBio = bio
|
||||||
|
callb(bio)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {string|null} */
|
||||||
|
let currentSeedBackup = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {(seedBackup: string|null) => void} cb
|
||||||
|
* @param {UserGUNNode} user
|
||||||
|
* @param {ISEA} SEA
|
||||||
|
* @throws {Error} If user hasn't been auth.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
const onSeedBackup = async (cb, user, SEA) => {
|
||||||
|
if (!user.is) {
|
||||||
|
throw new Error(ErrorCode.NOT_AUTH)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mySecret = await SEA.secret(user._.sea.epub, user._.sea)
|
||||||
|
|
||||||
|
const callb = debounce(cb, DEBOUNCE_WAIT_TIME)
|
||||||
|
callb(currentSeedBackup)
|
||||||
|
|
||||||
|
user.get(Key.SEED_BACKUP).on(async seedBackup => {
|
||||||
|
if (typeof seedBackup === 'string') {
|
||||||
|
currentSeedBackup = await SEA.decrypt(seedBackup, mySecret)
|
||||||
|
callb(currentSeedBackup)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
__onSentRequestToUser,
|
__onSentRequestToUser,
|
||||||
__onUserToIncoming,
|
__onUserToIncoming,
|
||||||
|
|
@ -1215,5 +1269,7 @@ module.exports = {
|
||||||
onOutgoing,
|
onOutgoing,
|
||||||
onChats,
|
onChats,
|
||||||
onSimplerReceivedRequests,
|
onSimplerReceivedRequests,
|
||||||
onSimplerSentRequests
|
onSimplerSentRequests,
|
||||||
|
onBio,
|
||||||
|
onSeedBackup
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
services/gunDB/contact-api/getters.js
Normal file
16
services/gunDB/contact-api/getters.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
const Key = require('./key')
|
||||||
|
const Utils = require('./utils')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} pub
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
|
||||||
|
return currAddr
|
||||||
|
}
|
||||||
18
services/gunDB/contact-api/jobs/index.js
Normal file
18
services/gunDB/contact-api/jobs/index.js
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -1,24 +1,15 @@
|
||||||
/**
|
/**
|
||||||
* @prettier
|
* @format
|
||||||
* 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
|
|
||||||
*/
|
*/
|
||||||
const ErrorCode = require('./errorCode')
|
const ErrorCode = require('../errorCode')
|
||||||
const Key = require('./key')
|
const Key = require('../key')
|
||||||
const Schema = require('./schema')
|
const Schema = require('../schema')
|
||||||
const Utils = require('./utils')
|
const Utils = require('../utils')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('./SimpleGUN').GUNNode} GUNNode
|
* @typedef {import('../SimpleGUN').GUNNode} GUNNode
|
||||||
* @typedef {import('./SimpleGUN').ISEA} ISEA
|
* @typedef {import('../SimpleGUN').ISEA} ISEA
|
||||||
* @typedef {import('./SimpleGUN').UserGUNNode} UserGUNNode
|
* @typedef {import('../SimpleGUN').UserGUNNode} UserGUNNode
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -148,6 +139,4 @@ const onAcceptedRequests = async (user, SEA) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = onAcceptedRequests
|
||||||
onAcceptedRequests
|
|
||||||
}
|
|
||||||
116
services/gunDB/contact-api/jobs/onOrders.js
Normal file
116
services/gunDB/contact-api/jobs/onOrders.js
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
/**
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
const LightningServices = require('../../../../utils/lightningServices')
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
try {
|
||||||
|
if (addr !== currentOrderAddr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Schema.isOrder(order)) {
|
||||||
|
throw new Error(`Expected an order instead got: ${JSON.stringify(order)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderToResponse = user.get(Key.ORDER_TO_RESPONSE)
|
||||||
|
|
||||||
|
if (await orderToResponse.get(orderID).then()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
const invoice = await new Promise((resolve, rej) => {
|
||||||
|
const {
|
||||||
|
services: { lightning }
|
||||||
|
} = LightningServices
|
||||||
|
|
||||||
|
lightning.addInvoice(
|
||||||
|
{
|
||||||
|
expiry: 36000,
|
||||||
|
memo,
|
||||||
|
value: amount
|
||||||
|
},
|
||||||
|
(
|
||||||
|
/** @type {any} */ error,
|
||||||
|
/** @type {{ payment_request: string }} */ response
|
||||||
|
) => {
|
||||||
|
if (error) {
|
||||||
|
rej(error)
|
||||||
|
} else {
|
||||||
|
resolve(response.payment_request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
|
@ -24,3 +24,13 @@ exports.DISPLAY_NAME = 'displayName'
|
||||||
* Maps user to the last request sent to them.
|
* Maps user to the last request sent to them.
|
||||||
*/
|
*/
|
||||||
exports.USER_TO_LAST_REQUEST_SENT = 'USER_TO_LAST_REQUEST_SENT'
|
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'
|
||||||
|
|
||||||
|
exports.BIO = 'bio'
|
||||||
|
|
||||||
|
exports.SEED_BACKUP = 'seedBackup'
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* @prettier
|
* @format
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* @typedef {object} HandshakeRequest
|
* @typedef {object} HandshakeRequest
|
||||||
|
|
@ -316,7 +316,6 @@ exports.isPartialOutgoing = item => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @param {any} item
|
* @param {any} item
|
||||||
* @returns {item is Outgoing}
|
* @returns {item is Outgoing}
|
||||||
*/
|
*/
|
||||||
|
|
@ -337,3 +336,41 @@ exports.isOutgoing = item => {
|
||||||
|
|
||||||
return typeof obj.with === 'string' && messagesAreMessages
|
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'
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@ const Events = {
|
||||||
ON_DISPLAY_NAME: "ON_DISPLAY_NAME",
|
ON_DISPLAY_NAME: "ON_DISPLAY_NAME",
|
||||||
ON_HANDSHAKE_ADDRESS: "ON_HANDSHAKE_ADDRESS",
|
ON_HANDSHAKE_ADDRESS: "ON_HANDSHAKE_ADDRESS",
|
||||||
ON_RECEIVED_REQUESTS: "ON_RECEIVED_REQUESTS",
|
ON_RECEIVED_REQUESTS: "ON_RECEIVED_REQUESTS",
|
||||||
ON_SENT_REQUESTS: "ON_SENT_REQUESTS"
|
ON_SENT_REQUESTS: "ON_SENT_REQUESTS",
|
||||||
|
ON_BIO: "ON_BIO",
|
||||||
|
ON_SEED_BACKUP: "ON_SEED_BACKUP",
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = Events;
|
module.exports = Events;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ const Encryption = require("../utils/encryptionStore");
|
||||||
const LightningServices = require("../utils/lightningServices");
|
const LightningServices = require("../utils/lightningServices");
|
||||||
const GunDB = require("../services/gunDB/Mediator");
|
const GunDB = require("../services/gunDB/Mediator");
|
||||||
const { unprotectedRoutes, nonEncryptedRoutes } = require("../utils/protectedRoutes");
|
const { unprotectedRoutes, nonEncryptedRoutes } = require("../utils/protectedRoutes");
|
||||||
|
const GunActions = require("../services/gunDB/contact-api/actions")
|
||||||
|
|
||||||
const DEFAULT_MAX_NUM_ROUTES_TO_QUERY = 10;
|
const DEFAULT_MAX_NUM_ROUTES_TO_QUERY = 10;
|
||||||
const SESSION_ID = uuid();
|
const SESSION_ID = uuid();
|
||||||
|
|
@ -536,7 +537,13 @@ module.exports = async (
|
||||||
|
|
||||||
// Register user before creating wallet
|
// Register user before creating wallet
|
||||||
const publicKey = await GunDB.register(alias, password);
|
const publicKey = await GunDB.register(alias, password);
|
||||||
|
|
||||||
|
await GunActions.saveSeedBackup(
|
||||||
|
mnemonicPhrase,
|
||||||
|
GunDB.getUser(),
|
||||||
|
GunDB.mySEA
|
||||||
|
)
|
||||||
|
|
||||||
walletUnlocker.initWallet(
|
walletUnlocker.initWallet(
|
||||||
walletArgs,
|
walletArgs,
|
||||||
async (initWalletErr, initWalletResponse) => {
|
async (initWalletErr, initWalletResponse) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue