Merge pull request #240 from shocknet/dev

Dev
This commit is contained in:
CapDog 2020-11-05 14:50:16 -06:00 committed by GitHub
commit 0a38cfe4f5
25 changed files with 1457 additions and 252 deletions

View file

@ -83,7 +83,12 @@
"no-undefined": "off",
"no-process-env": "off"
"no-process-env": "off",
// I am now convinced TODO comments closer to the relevant code are better
// than GH issues. Especially when it only concerns a single function /
// routine.
"no-warning-comments": "off"
},
"parser": "babel-eslint",
"env": {

View file

@ -46,7 +46,7 @@ module.exports = (mainnet = false) => {
logfile: "shockapi.log",
lndLogFile: parsePath(`${lndDirectory}/logs/bitcoin/${network}/lnd.log`),
lndDirPath: lndDirectory,
peers: ['http://gun.shock.network:8765/gun','http://gun2.shock.network:8765/gun'],
peers: ['http://gun.shock.network:8765/gun'],
useTLS: false,
tokenExpirationMS: 259200000
};

View file

@ -22,7 +22,8 @@
gun = Gun({
peers: [
`http://guntest.shock.network:8765/gun`
'http://gun.shock.network:8765/gun',
//'http://gun2.shock.network:8765/gun'
],
axe: false
})

View file

@ -1,8 +1,10 @@
const program = require("commander");
const {version} = (JSON.parse(require('fs').readFileSync("./package.json", "utf-8")))
// parse command line parameters
program
.version("1.0.0")
.version(version)
.option("-s, --serverport [port]", "web server http listening port (defaults to 8280)")
.option("-x, --httpsport [port]", "web server https listening port (defaults to 8283)")
.option("-h, --serverhost [host]", "web server listening host (defaults to localhost)")

View file

@ -21,6 +21,7 @@
"basic-auth": "^2.0.0",
"big.js": "^5.2.2",
"bitcore-lib": "^0.15.0",
"bluebird": "^3.7.2",
"body-parser": "^1.16.0",
"colors": "^1.4.0",
"command-exists": "^1.2.6",
@ -48,7 +49,7 @@
"request-promise": "^4.2.2",
"response-time": "^2.3.2",
"shelljs": "^0.8.2",
"shock-common": "8.0.0",
"shock-common": "16.x.x",
"socket.io": "2.1.1",
"text-encoding": "^0.7.0",
"tingodb": "^0.6.1",
@ -57,7 +58,7 @@
},
"devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@types/bluebird": "*",
"@types/bluebird": "^3.5.32",
"@types/dotenv": "^6.1.1",
"@types/express": "^4.17.1",
"@types/gun": "^0.9.2",

View file

@ -205,7 +205,7 @@ const Config = require('../config')
// TO DO: move to common repo
/**
* @typedef {object} SimpleSocket
* @prop {(eventName: string, data: Emission|EncryptedEmission) => void} emit
* @prop {(eventName: string, data?: Emission|EncryptedEmission) => void} emit
* @prop {(eventName: string, handler: (data: any) => void) => void} on
* @prop {{ query: { 'x-shockwallet-device-id': string }}} handshake
*/
@ -224,7 +224,6 @@ let user
/** @type {string|null} */
let _currentAlias = null
/** @type {string|null} */
let _currentPass = null
/** @type {string|null} */
let mySec = null
@ -361,7 +360,6 @@ const authenticate = async (alias, pass, __user) => {
mySec = await mySEA.secret(_user._.sea.epub, _user._.sea)
_currentAlias = alias
_currentPass = await mySEA.encrypt(pass, mySec)
await new Promise(res => setTimeout(res, 5000))
@ -422,6 +420,7 @@ const instantiateGun = () => {
const _gun = /** @type {unknown} */ (new Gun({
axe: false,
multicast: false,
peers: Config.PEERS
}))
@ -432,29 +431,11 @@ const instantiateGun = () => {
instantiateGun()
const freshGun = async () => {
const _gun = /** @type {unknown} */ (new Gun({
axe: false,
peers: Config.PEERS
}))
const gun = /** @type {GUNNode} */ (_gun)
const user = gun.user()
if (!_currentAlias || !_currentPass || !mySec) {
throw new Error('Called freshGun() without alias, pass and secret cached')
const freshGun = () => {
return {
gun,
user
}
const pass = await mySEA.decrypt(_currentPass, mySec)
if (typeof pass !== 'string') {
throw new Error('could not decrypt stored in memory current pass')
}
await authenticate(_currentAlias, pass, user)
return { gun, user }
}
/**
@ -1426,9 +1407,7 @@ const register = async (alias, pass) => {
if (typeof ack.err === 'string') {
throw new Error(ack.err)
} else if (typeof ack.pub === 'string' || typeof user._.sea === 'object') {
const mySecret = await mySEA.secret(user._.sea.epub, user._.sea)
_currentAlias = alias
_currentPass = await mySEA.encrypt(pass, mySecret)
// OK
} else {
throw new Error('unknown error, ack: ' + JSON.stringify(ack))
}
@ -1475,5 +1454,6 @@ module.exports = {
getUser,
mySEA,
getMySecret,
freshGun
freshGun,
$$__SHOCKWALLET__ENCRYPTED__
}

View file

@ -1,7 +1,7 @@
/**
* @prettier
*/
type Primitive = boolean | string | number
export type Primitive = boolean | string | number
export interface Data {
[K: string]: ValidDataValue

View file

@ -5,10 +5,14 @@ const uuidv1 = require('uuid/v1')
const logger = require('winston')
const Common = require('shock-common')
const { Constants, Schema } = Common
const Gun = require('gun')
const { ErrorCode } = Constants
const { sendPaymentV2Invoice } = require('../../../utils/lightningServices/v2')
const {
sendPaymentV2Invoice,
decodePayReq
} = require('../../../utils/lightningServices/v2')
/**
* @typedef {import('../../../utils/lightningServices/types').PaymentV2} PaymentV2
@ -815,7 +819,7 @@ const setAvatar = (avatar, user) =>
}
user
.get(Key.PROFILE)
.get(Key.PROFILE_BINARY)
.get(Key.AVATAR)
.put(avatar, ack => {
if (ack.err && typeof ack.err !== 'number') {
@ -905,17 +909,30 @@ const sendHRWithInitialMsg = async (
await sendMessage(recipientPublicKey, initialMsg, user, SEA)
}
/**
* @typedef {object} SpontPaymentOptions
* @prop {Common.Schema.OrderTargetType} type
* @prop {string=} postID
*/
/**
* Returns the preimage corresponding to the payment.
* @param {string} to
* @param {number} amount
* @param {string} memo
* @param {number} feeLimit
* @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<PaymentV2>} The payment's preimage.
*/
const sendSpontaneousPayment = async (to, amount, memo, feeLimit) => {
const sendSpontaneousPayment = async (
to,
amount,
memo,
feeLimit,
opts = { type: 'user' }
) => {
try {
const SEA = require('../Mediator').mySEA
const getUser = () => require('../Mediator').getUser()
@ -935,7 +952,12 @@ const sendSpontaneousPayment = async (to, amount, memo, feeLimit) => {
amount: amount.toString(),
from: getUser()._.sea.pub,
memo: memo || 'no memo',
timestamp: Date.now()
timestamp: Date.now(),
targetType: opts.type
}
if (opts.type === 'post') {
order.postID = opts.postID
}
logger.info(JSON.stringify(order))
@ -1021,6 +1043,21 @@ const sendSpontaneousPayment = async (to, amount, memo, feeLimit) => {
throw new Error(orderResponse.response)
}
logger.info('Will now check for invoice amount mismatch')
const encodedInvoice = orderResponse.response
const { num_satoshis: decodedAmt } = await decodePayReq(encodedInvoice)
if (decodedAmt !== amount.toString()) {
throw new Error('Invoice amount mismatch')
}
// double check
if (Number(decodedAmt) !== amount) {
throw new Error('Invoice amount mismatch')
}
logger.info('Will now send payment through lightning')
const payment = await sendPaymentV2Invoice({
@ -1228,6 +1265,49 @@ const setLastSeenApp = () =>
})
)
/**
* @param {string[]} tags
* @param {string} title
* @param {Common.Schema.ContentItem[]} content
* @returns {Promise<[string, Common.Schema.RawPost]>}
*/
const createPostNew = async (tags, title, content) => {
/** @type {Common.Schema.RawPost} */
const newPost = {
date: Date.now(),
status: 'publish',
tags: tags.join('-'),
title,
contentItems: {}
}
content.forEach(c => {
// @ts-expect-error
const uuid = Gun.text.random()
newPost.contentItems[uuid] = c
})
/** @type {string} */
const postID = await Common.makePromise((res, rej) => {
const _n = require('../Mediator')
.getUser()
.get(Key.POSTS_NEW)
.set(
// @ts-expect-error
newPost,
ack => {
if (ack.err && typeof ack.err !== 'number') {
rej(new Error(ack.err))
} else {
res(_n._.get)
}
}
)
})
return [postID, newPost]
}
/**
* @param {string[]} tags
* @param {string} title
@ -1311,26 +1391,24 @@ const createPost = async (tags, title, content) => {
)
})
/** @type {string} */
const postID = await new Promise((res, rej) => {
const _n = require('../Mediator')
const [postID, newPost] = await createPostNew(tags, title, content)
await Common.makePromise((res, rej) => {
require('../Mediator')
.getUser()
.get(Key.WALL)
.get(Key.PAGES)
.get(pageIdx)
.get(Key.POSTS)
.set(
{
date: Date.now(),
status: 'publish',
tags: tags.join('-'),
title
},
.get(postID)
.put(
// @ts-expect-error
newPost,
ack => {
if (ack.err && typeof ack.err !== 'number') {
rej(new Error(ack.err))
} else {
res(_n._.get)
res()
}
}
)
@ -1352,52 +1430,6 @@ const createPost = async (tags, title, content) => {
})
}
const contentItems = require('../Mediator')
.getUser()
.get(Key.WALL)
.get(Key.PAGES)
.get(pageIdx)
.get(Key.POSTS)
.get(postID)
.get(Key.CONTENT_ITEMS)
try {
await Promise.all(
content.map(
ci =>
new Promise(res => {
// @ts-ignore
contentItems.set(ci, ack => {
if (ack.err && typeof ack.err !== 'number') {
throw new Error(ack.err)
}
res()
})
})
)
)
} catch (e) {
await new Promise(res => {
require('../Mediator')
.getUser()
.get(Key.WALL)
.get(Key.PAGES)
.get(pageIdx)
.get(Key.POSTS)
.get(postID)
.put(null, ack => {
if (ack.err && typeof ack.err !== 'number') {
throw new Error(ack.err)
}
res()
})
})
throw e
}
const loadedPost = await new Promise(res => {
require('../Mediator')
.getUser()
@ -1434,11 +1466,25 @@ const createPost = async (tags, title, content) => {
/**
* @param {string} postId
* @param {string} page
* @returns {Promise<void>}
*/
const deletePost = async postId => {
await new Promise(res => {
res(postId)
const deletePost = async (postId, page) => {
await new Promise((res, rej) => {
require('../Mediator')
.getUser()
.get(Key.WALL)
.get(Key.PAGES)
.get(page)
.get(Key.POSTS)
.get(postId)
.put(null, ack => {
if (ack.err && typeof ack.err !== 'number') {
rej(new Error(ack.err))
} else {
res()
}
})
})
}

View file

@ -114,7 +114,7 @@ const onAvatar = (cb, user) => {
if (!avatarSubbed) {
avatarSubbed = true
user
.get(Key.PROFILE)
.get(Key.PROFILE_BINARY)
.get(Key.AVATAR)
.on(avatar => {
if (typeof avatar === 'string' || avatar === null) {

View file

@ -36,9 +36,7 @@ const setReceivedReqsMap = reqs => {
}
listeners.add(() => {
logger.info(
`new received reqs: ${JSON.stringify(getReceivedReqs(), null, 4)}`
)
logger.info(`new received reqs: ${size(getReceivedReqs())}`)
})
const react = debounce(() => {

View file

@ -103,8 +103,40 @@ const getMyUser = async () => {
return u
}
/**
* @param {string} publicKey
*/
const getUserInfo = async publicKey => {
const userInfo = await Utils.tryAndWait(
gun =>
new Promise(res =>
gun
.user(publicKey)
.get(Key.PROFILE)
.load(res)
),
v => {
if (typeof v !== 'object') {
return true
}
if (v === null) {
return true
}
// load sometimes returns an empty set on the first try
return size(v) === 0
}
)
return {
publicKey,
avatar: userInfo.avatar,
displayName: userInfo.displayName
}
}
module.exports.getMyUser = getMyUser
module.exports.getUserInfo = getUserInfo
module.exports.Follows = require('./follows')
module.exports.getWallPage = Wall.getWallPage

View file

@ -2,6 +2,7 @@
* @format
*/
const { performance } = require('perf_hooks')
const logger = require('winston')
const isFinite = require('lodash/isFinite')
const isNumber = require('lodash/isNumber')
@ -38,9 +39,27 @@ const ordersProcessed = new Set()
* @prop {boolean} private
*/
/**
* @typedef {object} InvoiceResponse
* @prop {string} payment_request
* @prop {Buffer} r_hash
*/
/**
* @typedef {object} TipPaymentStatus
* @prop {string} hash
* @prop {import('shock-common').Schema.InvoiceState} state
* @prop {string} targetType
* @prop {(string)=} postID
* @prop {(number)=} postPage
*/
let currentOrderAddr = ''
/** @param {InvoiceRequest} invoiceReq */
/**
* @param {InvoiceRequest} invoiceReq
* @returns {Promise<InvoiceResponse>}
*/
const _addInvoice = invoiceReq =>
new Promise((resolve, rej) => {
const {
@ -49,12 +68,12 @@ const _addInvoice = invoiceReq =>
lightning.addInvoice(invoiceReq, (
/** @type {any} */ error,
/** @type {{ payment_request: string }} */ response
/** @type {InvoiceResponse} */ response
) => {
if (error) {
rej(error)
} else {
resolve(response.payment_request)
resolve(response)
}
})
})
@ -85,7 +104,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
return
}
const listenerStartTime = Date.now()
const listenerStartTime = performance.now()
ordersProcessed.add(orderID)
@ -95,7 +114,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
)} -- addr: ${addr}`
)
const orderAnswerStartTime = Date.now()
const orderAnswerStartTime = performance.now()
const alreadyAnswered = await getUser()
.get(Key.ORDER_TO_RESPONSE)
@ -107,11 +126,11 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
return
}
const orderAnswerEndTime = Date.now() - orderAnswerStartTime
const orderAnswerEndTime = performance.now() - orderAnswerStartTime
logger.info(`[PERF] Order Already Answered: ${orderAnswerEndTime}ms`)
const decryptStartTime = Date.now()
const decryptStartTime = performance.now()
const senderEpub = await Utils.pubToEpub(order.from)
const secret = await SEA.secret(senderEpub, getUser()._.sea)
@ -121,7 +140,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
SEA.decrypt(order.memo, secret)
])
const decryptEndTime = Date.now() - decryptStartTime
const decryptEndTime = performance.now() - decryptStartTime
logger.info(`[PERF] Decrypt invoice info: ${decryptEndTime}ms`)
@ -156,14 +175,11 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
`onOrders() -> Will now create an invoice : ${JSON.stringify(invoiceReq)}`
)
const invoiceStartTime = Date.now()
const invoiceStartTime = performance.now()
/**
* @type {string}
*/
const invoice = await _addInvoice(invoiceReq)
const invoiceEndTime = Date.now() - invoiceStartTime
const invoiceEndTime = performance.now() - invoiceStartTime
logger.info(`[PERF] LND Invoice created in ${invoiceEndTime}ms`)
@ -171,11 +187,11 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
'onOrders() -> Successfully created the invoice, will now encrypt it'
)
const invoiceEncryptStartTime = Date.now()
const invoiceEncryptStartTime = performance.now()
const encInvoice = await SEA.encrypt(invoice, secret)
const encInvoice = await SEA.encrypt(invoice.payment_request, secret)
const invoiceEncryptEndTime = Date.now() - invoiceEncryptStartTime
const invoiceEncryptEndTime = performance.now() - invoiceEncryptStartTime
logger.info(`[PERF] Invoice encrypted in ${invoiceEncryptEndTime}ms`)
@ -189,12 +205,13 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
type: 'invoice'
}
const invoicePutStartTime = Date.now()
const invoicePutStartTime = performance.now()
await new Promise((res, rej) => {
getUser()
.get(Key.ORDER_TO_RESPONSE)
.get(orderID)
// @ts-expect-error
.put(orderResponse, ack => {
if (ack.err && typeof ack.err !== 'number') {
rej(
@ -208,13 +225,32 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
})
})
const invoicePutEndTime = Date.now() - invoicePutStartTime
const invoicePutEndTime = performance.now() - invoicePutStartTime
logger.info(`[PERF] Added invoice to GunDB in ${invoicePutEndTime}ms`)
const listenerEndTime = Date.now() - listenerStartTime
const listenerEndTime = performance.now() - listenerStartTime
logger.info(`[PERF] Invoice generation completed in ${listenerEndTime}ms`)
const hash = invoice.r_hash.toString('base64')
if (order.targetType === 'post') {
/** @type {TipPaymentStatus} */
const paymentStatus = {
hash,
state: 'OPEN',
targetType: order.targetType,
postID: order.postID
}
getUser()
.get(Key.TIPS_PAYMENT_STATUS)
.get(hash)
// @ts-ignore
.put(paymentStatus, response => {
console.log(response)
})
}
} catch (err) {
logger.error(
`error inside onOrders, orderAddr: ${addr}, orderID: ${orderID}, order: ${JSON.stringify(
@ -232,6 +268,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
getUser()
.get(Key.ORDER_TO_RESPONSE)
.get(orderID)
// @ts-expect-error
.put(orderResponse, ack => {
if (ack.err && typeof ack.err !== 'number') {
logger.error(

View file

@ -55,3 +55,12 @@ exports.CONTENT_ITEMS = 'contentItems'
exports.FOLLOWS = 'follows'
exports.POSTS = 'posts'
// Tips counter for posts
exports.TOTAL_TIPS = 'totalTips'
exports.TIPS_PAYMENT_STATUS = 'tipsPaymentStatus'
exports.PROFILE_BINARY = 'profileBinary'
exports.POSTS_NEW = 'posts'

View file

@ -34,7 +34,7 @@ const onAvatar = (cb, pub) => {
require('../../Mediator')
.getGun()
.user(pub)
.get(Key.PROFILE)
.get(Key.PROFILE_BINARY)
.get(Key.AVATAR)
.on(av => {
if (typeof av === 'string' || av === null) {

View file

@ -195,11 +195,11 @@ const tryAndWait = async (promGen, shouldRetry = () => false) => {
}
logger.info(
`\n recreating a fresh gun and retrying one last time \n` +
`\n NOT recreating a fresh gun but retrying one last time \n` +
` args: ${promGen.toString()} -- ${shouldRetry.toString()}`
)
const { gun, user } = await require('../../Mediator/index').freshGun()
const { gun, user } = require('../../Mediator/index').freshGun()
return timeout10(promGen(gun, user))
/* eslint-enable no-empty */

247
services/gunDB/rpc/index.js Normal file
View file

@ -0,0 +1,247 @@
/**
* @format
*/
/* eslint-disable no-use-before-define */
// @ts-check
const { makePromise, Constants, Schema } = require('shock-common')
const mapValues = require('lodash/mapValues')
const Bluebird = require('bluebird')
const Gun = require('gun')
const { pubToEpub } = require('../contact-api/utils')
const {
getGun,
getUser,
mySEA: SEA,
getMySecret,
$$__SHOCKWALLET__ENCRYPTED__
} = require('../Mediator')
/**
* @typedef {import('../contact-api/SimpleGUN').ValidDataValue} ValidDataValue
* @typedef {import('./types').ValidRPCDataValue} ValidRPCDataValue
* @typedef {import('./types').RPCData} RPCData
*/
/**
* @param {ValidDataValue} value
* @param {string} publicKey
* @returns {Promise<ValidDataValue>}
*/
const deepDecryptIfNeeded = async (value, publicKey) => {
if (Schema.isObj(value)) {
return Bluebird.props(
mapValues(value, o => deepDecryptIfNeeded(o, publicKey))
)
}
if (
typeof value === 'string' &&
value.indexOf($$__SHOCKWALLET__ENCRYPTED__) === 0
) {
const user = getUser()
if (!user.is) {
throw new Error(Constants.ErrorCode.NOT_AUTH)
}
let sec = ''
if (user.is.pub === publicKey) {
sec = getMySecret()
} else {
sec = await SEA.secret(await pubToEpub(publicKey), user._.sea)
}
const decrypted = SEA.decrypt(value, sec)
return decrypted
}
return value
}
/**
* @param {ValidRPCDataValue} value
* @returns {Promise<ValidRPCDataValue>}
*/
// eslint-disable-next-line func-style
async function deepEncryptIfNeeded(value) {
const u = getUser()
if (!u.is) {
throw new Error(Constants.ErrorCode.NOT_AUTH)
}
if (!Schema.isObj(value)) {
return value
}
if (Array.isArray(value)) {
return Promise.all(value.map(v => deepEncryptIfNeeded(v)))
}
const pk = /** @type {string|undefined} */ (value.$$__ENCRYPT__FOR)
if (!pk) {
return Bluebird.props(mapValues(value, deepEncryptIfNeeded))
}
const actualValue = /** @type {string} */ (value.value)
let encryptedValue = ''
if (pk === u.is.pub) {
encryptedValue = await SEA.encrypt(actualValue, getMySecret())
} else {
const sec = await SEA.secret(await pubToEpub(pk), u._.sea)
encryptedValue = await SEA.encrypt(actualValue, sec)
}
return encryptedValue
}
/**
* @param {string} rawPath
* @param {ValidRPCDataValue} value
* @returns {Promise<void>}
*/
const put = async (rawPath, value) => {
const [root, ...path] = rawPath.split('>')
const node = (() => {
// eslint-disable-next-line init-declarations
let _node
if (root === '$gun') {
_node = getGun()
} else if (root === '$user') {
const u = getUser()
if (!u.is) {
throw new Error(Constants.ErrorCode.NOT_AUTH)
}
_node = u
} else {
throw new TypeError(
`Unknown kind of root, expected $gun or $user but got: ${root}`
)
}
for (const bit of path) {
_node = _node.get(bit)
}
return _node
})()
const theValue = await deepEncryptIfNeeded(value)
if (Array.isArray(theValue)) {
await Promise.all(theValue.map(v => set(rawPath, v)))
// Do not remove this return, an array is also an object
// eslint-disable-next-line no-useless-return
return
} else if (Schema.isObj(theValue)) {
const writes = mapValues(theValue, (v, k) => put(`${rawPath}.${k}`, v))
await Bluebird.props(writes)
} /* is primitive */ else {
await makePromise((res, rej) => {
node.put(/** @type {ValidDataValue} */ (theValue), ack => {
if (ack.err && typeof ack.err !== 'number') {
rej(new Error(ack.err))
} else {
res()
}
})
})
}
}
/**
* @param {string} rawPath
* @param {ValidRPCDataValue} value
* @returns {Promise<string>}
*/
// eslint-disable-next-line func-style
async function set(rawPath, value) {
const [root, ...path] = rawPath.split('>')
const node = (() => {
// eslint-disable-next-line init-declarations
let _node
if (root === '$gun') {
_node = getGun()
} else if (root === '$user') {
const u = getUser()
if (!u.is) {
throw new Error(Constants.ErrorCode.NOT_AUTH)
}
_node = u
} else {
throw new TypeError(
`Unknown kind of root, expected $gun or $user but got: ${root}`
)
}
for (const bit of path) {
_node = _node.get(bit)
}
return _node
})()
const theValue = await deepEncryptIfNeeded(value)
if (Array.isArray(theValue)) {
// we'll create a set of sets
// @ts-expect-error
const uuid = Gun.text.random()
// here we are simulating the top-most set()
const subPath = rawPath + '.' + uuid
const writes = theValue.map(v => set(subPath, v))
await Promise.all(writes)
return uuid
} else if (Schema.isObj(theValue)) {
// @ts-expect-error
const uuid = Gun.text.random() // we'll handle UUID ourselves
// so we can use our own put()
const subPath = rawPath + '.' + uuid
await put(subPath, theValue)
return uuid
}
/* else is primitive */
const id = await makePromise((res, rej) => {
const subNode = node.set(theValue, ack => {
if (ack.err && typeof ack.err !== 'number') {
rej(new Error(ack.err))
} else {
res(subNode._.get)
}
})
})
return id
}
module.exports = {
put,
set,
deepDecryptIfNeeded,
deepEncryptIfNeeded
}

View file

@ -0,0 +1,8 @@
import {Primitive} from '../contact-api/SimpleGUN'
export interface RPCData {
[K: string]: ValidRPCDataValue
}
export type ValidRPCDataValue = Primitive | null | RPCData | Array<ValidRPCDataValue>

View file

@ -14,7 +14,7 @@ const Common = require('shock-common')
const isARealUsableNumber = require('lodash/isFinite')
const Big = require('big.js')
const size = require('lodash/size')
const { range, flatten } = require('ramda')
const { range, flatten, evolve } = require('ramda')
const getListPage = require('../utils/paginate')
const auth = require('../services/auth/auth')
@ -32,8 +32,11 @@ const GunGetters = require('../services/gunDB/contact-api/getters')
const GunKey = require('../services/gunDB/contact-api/key')
const {
sendPaymentV2Keysend,
sendPaymentV2Invoice
sendPaymentV2Invoice,
listPayments
} = require('../utils/lightningServices/v2')
const { startTipStatusJob } = require('../utils/lndJobs')
const GunWriteRPC = require('../services/gunDB/rpc')
const DEFAULT_MAX_NUM_ROUTES_TO_QUERY = 10
const SESSION_ID = uuid()
@ -303,9 +306,9 @@ module.exports = async (
}
} else {
encryptedToken = req.body.token
encryptedKey = req.body.encryptionKey
encryptedKey = req.body.encryptionKey || req.body.encryptedKey
IV = req.body.iv
reqData = req.body.data
reqData = req.body.data || req.body.encryptedData
}
const decryptedKey = Encryption.decryptKey({
deviceId,
@ -413,6 +416,17 @@ module.exports = async (
errorMessage: 'Please create a wallet before using the API'
})
}
if (req.path.includes('/api/gun')) {
const authenticated = GunDB.isAuthenticated()
if (!authenticated) {
return res.status(401).json({
field: 'gun',
errorMessage: 'Please login in order to perform this action'
})
}
}
next()
} catch (err) {
logger.error(err)
@ -659,7 +673,7 @@ module.exports = async (
'Channel backup LND locked, new registration in 60 seconds'
)
process.nextTick(() =>
setTimeout(() => onNewTransaction(socket, subID), 60000)
setTimeout(() => onNewChannelBackup(), 60000)
)
break
}
@ -673,15 +687,19 @@ module.exports = async (
'Channel backup LND disconnected, sockets reconnecting in 30 seconds...'
)
process.nextTick(() =>
setTimeout(() => onNewTransaction(socket, subID), 30000)
setTimeout(() => onNewChannelBackup(), 30000)
)
break
}
default: {
logger.error('[event:transaction:new] UNKNOWN LND error')
}
}
})
}
onNewChannelBackup()
startTipStatusJob()
// Generate auth token and send it as a JSON response
const token = await auth.generateToken()
@ -690,6 +708,24 @@ module.exports = async (
user: {
alias,
publicKey
},
follows: await GunGetters.Follows.currentFollows(),
data: {
invoices: await Common.makePromise((res, rej) => {
lightning.listInvoices(
{
reversed: true,
num_max_invoices: 50
},
(err, lres) => {
if (err) {
rej(new Error(err.details))
} else {
res(lres)
}
}
)
})
}
})
@ -1242,12 +1278,12 @@ module.exports = async (
app.post('/api/lnd/unifiedTrx', async (req, res) => {
try {
const { type, amt, to, memo, feeLimit } = req.body
const { type, amt, to, memo, feeLimit, postID } = req.body
if (type !== 'spont') {
if (type !== 'spont' && type !== 'post') {
return res.status(415).json({
field: 'type',
errorMessage: `Only 'spont' payments supported via this endpoint for now.`
errorMessage: `Only 'spont' and 'post' payments supported via this endpoint for now.`
})
}
@ -1281,9 +1317,19 @@ module.exports = async (
})
}
return res
.status(200)
.json(await GunActions.sendSpontaneousPayment(to, amt, memo, feeLimit))
if (type === 'post' && typeof postID !== 'string') {
return res.status(400).json({
field: 'postID',
errorMessage: `Send postID`
})
}
return res.status(200).json(
await GunActions.sendSpontaneousPayment(to, amt, memo, feeLimit, {
type,
postID
})
)
} catch (e) {
return res.status(500).json({
errorMessage: e.message
@ -1315,6 +1361,60 @@ module.exports = async (
)
})
app.get('/api/lnd/payments', async (req, res) => {
const {
include_incomplete,
index_offset,
max_payments,
reversed
} = /** @type {Common.APISchema.ListPaymentsRequest} */ (evolve(
{
include_incomplete: x => x === 'true',
index_offset: x => Number(x),
max_payments: x => Number(x),
reversed: x => x === 'true'
},
req.query
))
if (typeof include_incomplete !== 'boolean') {
return res.status(400).json({
field: 'include_incomplete',
errorMessage: 'include_incomplete not a boolean'
})
}
if (!isARealUsableNumber(index_offset)) {
return res.status(400).json({
field: 'index_offset',
errorMessage: 'index_offset not a number'
})
}
if (!isARealUsableNumber(max_payments)) {
return res.status(400).json({
field: 'max_payments',
errorMessage: 'max_payments not a number'
})
}
if (typeof reversed !== 'boolean') {
return res.status(400).json({
field: 'reversed',
errorMessage: 'reversed not a boolean'
})
}
return res.status(200).json(
await listPayments({
include_incomplete,
index_offset,
max_payments,
reversed
})
)
})
// get lnd node invoices list
app.get('/api/lnd/listinvoices', (req, res) => {
const { lightning } = LightningServices.services
@ -2014,8 +2114,11 @@ module.exports = async (
app.get(`/api/gun/${GunEvent.ON_CHATS}`, (_, res) => {
try {
const data = Events.getChats()
const noAvatar = data.map(mex => {
return { ...mex, recipientAvatar: null }
})
res.json({
data
data: noAvatar
})
} catch (err) {
logger.info('Error in Chats poll:')
@ -2028,29 +2131,6 @@ module.exports = async (
}
})
app.get(`/api/gun/${GunEvent.ON_AVATAR}`, async (_, res) => {
try {
const user = require('../services/gunDB/Mediator').getUser()
const data = await timeout5(
user
.get(Key.PROFILE)
.get(Key.AVATAR)
.then()
)
res.json({
data
})
} catch (err) {
logger.info('Error in Avatar poll:')
logger.error(err)
res
.status(err.message === Common.Constants.ErrorCode.NOT_AUTH ? 401 : 500)
.json({
errorMessage: typeof err === 'string' ? err : err.message
})
}
})
app.get(`/api/gun/${GunEvent.ON_DISPLAY_NAME}`, async (_, res) => {
try {
const user = require('../services/gunDB/Mediator').getUser()
@ -2210,11 +2290,47 @@ module.exports = async (
}
})
app.delete(`/api/gun/wall/:postID`, (_, res) =>
res.status(200).json({
app.delete(`/api/gun/wall/:postInfo`, async (req, res) => {
try {
const { postInfo } = req.params
const parts = postInfo.split('&')
const [page, postId] = parts
if (!page || !postId) {
throw new Error(`please provide a "postId" and a "page"`)
}
await GunActions.deletePost(postId, page)
return res.status(200).json({
ok: 'true'
})
} catch (e) {
return res.status(500).json({
errorMessage:
(typeof e === 'string' ? e : e.message) || 'Unknown error.'
})
}
})
app.post(`/api/gun/userInfo`, async (req, res) => {
try {
const { pubs } = req.body
const reqs = pubs.map(
e =>
new Promise((res, rej) => {
GunGetters.getUserInfo(e)
.then(r => res(r))
.catch(e => rej(e))
})
)
const infos = await Promise.all(reqs)
return res.status(200).json({
pubInfos: infos
})
} catch (err) {
return res.status(500).json({
errorMessage: err.message
})
}
})
/////////////////////////////////
/**
* @template P
@ -2288,6 +2404,17 @@ module.exports = async (
}
}
ap.get('/api/gun/initwall', async (req, res) => {
try {
await GunActions.initWall()
res.json({ ok: true })
} catch (err) {
logger.error(err)
return res.status(500).json({
errorMessage: err.message
})
}
})
ap.get('/api/gun/follows/', apiGunFollowsGet)
ap.get('/api/gun/follows/:publicKey', apiGunFollowsGet)
ap.put(`/api/gun/follows/:publicKey`, apiGunFollowsPut)
@ -2520,8 +2647,11 @@ module.exports = async (
const apiGunRequestsReceivedGet = (_, res) => {
try {
const data = Events.getCurrentReceivedReqs()
const noAvatar = data.map(req => {
return { ...req, recipientAvatar: null }
})
res.json({
data
data: noAvatar
})
} catch (err) {
logger.error(err)
@ -2537,8 +2667,11 @@ module.exports = async (
const apiGunRequestsSentGet = (_, res) => {
try {
const data = Events.getCurrentSentReqs()
const noAvatar = data.map(req => {
return { ...req, recipientAvatar: null }
})
res.json({
data
data: noAvatar
})
} catch (err) {
logger.error(err)
@ -2909,4 +3042,206 @@ module.exports = async (
data: isAuthenticated()
})
})
/**
* @typedef {object} HandleGunFetchParams
* @prop {'once'|'load'} type
* @prop {boolean} startFromUserGraph
* @prop {string} path
* @prop {string=} publicKey
* @prop {string=} publicKeyForDecryption
*/
/**
* @param {HandleGunFetchParams} args0
* @returns {Promise<unknown>}
*/
const handleGunFetch = ({
type,
startFromUserGraph,
path,
publicKey,
publicKeyForDecryption
}) => {
const keys = path.split('>')
const { tryAndWait } = require('../services/gunDB/contact-api/utils')
return tryAndWait((gun, user) => {
// eslint-disable-next-line no-nested-ternary
let node = startFromUserGraph
? user
: publicKey
? gun.user(publicKey)
: gun
keys.forEach(key => (node = node.get(key)))
return new Promise(res => {
const listener = async data => {
if (publicKeyForDecryption) {
res(
await GunWriteRPC.deepDecryptIfNeeded(
data,
publicKeyForDecryption
)
)
} else {
res(data)
}
}
if (type === 'once') node.once(listener)
if (type === 'load') node.load(listener)
})
})
}
/**
* Used decryption of incoming data.
*/
const PUBKEY_FOR_DECRYPT_HEADER = 'public-key-for-decryption'
ap.get('/api/gun/once/:path', async (req, res) => {
const publicKeyForDecryption = req.header(PUBKEY_FOR_DECRYPT_HEADER)
const { path } = req.params
res.status(200).json({
data: await handleGunFetch({
path,
startFromUserGraph: false,
type: 'once',
publicKeyForDecryption
})
})
})
ap.get('/api/gun/load/:path', async (req, res) => {
const publicKeyForDecryption = req.header(PUBKEY_FOR_DECRYPT_HEADER)
const { path } = req.params
res.status(200).json({
data: await handleGunFetch({
path,
startFromUserGraph: false,
type: 'load',
publicKeyForDecryption
})
})
})
ap.get('/api/gun/user/once/:path', async (req, res) => {
const publicKeyForDecryption = req.header(PUBKEY_FOR_DECRYPT_HEADER)
const { path } = req.params
res.status(200).json({
data: await handleGunFetch({
path,
startFromUserGraph: true,
type: 'once',
publicKeyForDecryption
})
})
})
ap.get('/api/gun/user/load/:path', async (req, res) => {
const publicKeyForDecryption = req.header(PUBKEY_FOR_DECRYPT_HEADER)
const { path } = req.params
res.status(200).json({
data: await handleGunFetch({
path,
startFromUserGraph: true,
type: 'load',
publicKeyForDecryption
})
})
})
ap.get('/api/gun/otheruser/:publicKey/once/:path', async (req, res) => {
const publicKeyForDecryption = req.header(PUBKEY_FOR_DECRYPT_HEADER)
const { path, publicKey } = req.params
res.status(200).json({
data: await handleGunFetch({
path,
startFromUserGraph: false,
type: 'once',
publicKey,
publicKeyForDecryption
})
})
})
ap.get('/api/gun/otheruser/:publicKey/load/:path', async (req, res) => {
const publicKeyForDecryption = req.header(PUBKEY_FOR_DECRYPT_HEADER)
const { path, publicKey } = req.params
res.status(200).json({
data: await handleGunFetch({
path,
startFromUserGraph: false,
type: 'load',
publicKey,
publicKeyForDecryption
})
})
})
ap.post('/api/lnd/cb/:methodName', (req, res) => {
try {
const { lightning } = LightningServices.services
const { methodName } = req.params
const args = req.body
lightning[methodName](args, (err, lres) => {
if (err) {
res.status(500).json({
errorMessage: err.details
})
} else if (lres) {
res.status(200).json(lres)
} else {
res.status(500).json({
errorMessage: 'Unknown error'
})
}
})
} catch (err) {
logger.warn(`Error inside api cb:`)
logger.error(err)
logger.error(err.message)
return res.status(500).json({
errorMessage: err.message
})
}
})
ap.post('/api/gun/put', async (req, res) => {
try {
const { path, value } = req.body
await GunWriteRPC.put(path, value)
res.status(200).json({
ok: true
})
} catch (err) {
res
.status(err.message === Common.Constants.ErrorCode.NOT_AUTH ? 401 : 500)
.json({
errorMessage: err.message
})
}
})
ap.post('/api/gun/set', async (req, res) => {
try {
const { path, value } = req.body
const id = await GunWriteRPC.set(path, value)
res.status(200).json({
ok: true,
id
})
} catch (err) {
res
.status(err.message === Common.Constants.ErrorCode.NOT_AUTH ? 401 : 500)
.json({
errorMessage: err.message
})
}
})
}

View file

@ -26,15 +26,16 @@ const server = program => {
sensitiveRoutes,
nonEncryptedRoutes
} = require('../utils/protectedRoutes')
// load app default configuration data
const defaults = require('../config/defaults')(program.mainnet)
const rootFolder = process.resourcesPath || __dirname
// define useful global variables ======================================
// define env variables
Dotenv.config()
module.useTLS = program.usetls
module.serverPort = program.serverport || defaults.serverPort
module.httpsPort = module.serverPort
module.serverHost = program.serverhost || defaults.serverHost
const serverPort = program.serverport || defaults.serverPort
const serverHost = program.serverhost || defaults.serverHost
// setup winston logging ==========
const logger = require('../config/log')(
@ -159,12 +160,24 @@ const server = program => {
const startServer = async () => {
try {
LightningServices.setDefaults(program)
if (!LightningServices.isInitialized()) {
await LightningServices.init()
}
// init lnd module =================
const lnd = require('../services/lnd/lnd')(
LightningServices.services.lightning
)
await new Promise((resolve, reject) => {
LightningServices.services.lightning.getInfo({}, (err, res) => {
if (err && err.code !== 12) {
reject(err)
} else {
resolve()
}
})
})
const auth = require('../services/auth/auth')
app.use(compression())
@ -198,6 +211,11 @@ const server = program => {
next()
})
app.use((req, res, next) => {
res.set('Version', program.version())
next()
})
await Storage.init({
dir: Path.resolve(rootFolder, '../.storage')
})
@ -274,8 +292,8 @@ const server = program => {
const Sockets = require('./sockets')(io)
require('./routes')(app, defaults, Sockets, {
serverHost: module.serverHost,
serverPort: module.serverPort,
serverHost,
serverPort,
usetls: program.usetls,
CA,
CA_KEY
@ -290,20 +308,11 @@ const server = program => {
app.use(modifyResponseBody)
}
serverInstance.listen(module.serverPort, module.serverhost)
serverInstance.listen(serverPort, serverHost)
logger.info(
'App listening on ' + module.serverHost + ' port ' + module.serverPort
)
logger.info('App listening on ' + serverHost + ' port ' + serverPort)
module.server = serverInstance
// const localtunnel = require('localtunnel');
//
// const tunnel = localtunnel(port, (err, t) => {
// logger.info('err', err);
// logger.info('t', t.url);
// });
} catch (err) {
logger.info(err)
logger.info('Restarting server in 30 seconds...')

View file

@ -1,16 +1,30 @@
/** @prettier */
// app/sockets.js
/**
* @format
*/
// @ts-check
const logger = require('winston')
const Common = require('shock-common')
const mapValues = require('lodash/mapValues')
const auth = require('../services/auth/auth')
const Encryption = require('../utils/encryptionStore')
const LightningServices = require('../utils/lightningServices')
const {
getGun,
getUser,
isAuthenticated
} = require('../services/gunDB/Mediator')
const { deepDecryptIfNeeded } = require('../services/gunDB/rpc')
/**
* @typedef {import('../services/gunDB/Mediator').SimpleSocket} SimpleSocket
* @typedef {import('../services/gunDB/contact-api/SimpleGUN').ValidDataValue} ValidDataValue
*/
module.exports = (
/** @type {import('socket.io').Server} */
io
) => {
const Mediator = require('../services/gunDB/Mediator/index.js')
// This should be used for encrypting and emitting your data
const emitEncryptedEvent = ({ eventName, data, socket }) => {
try {
@ -134,6 +148,12 @@ module.exports = (
logger.info('[event:invoice:new] stream ok')
break
}
case 1: {
logger.info(
'[event:invoice:new] stream canceled, probably socket disconnected'
)
break
}
case 2: {
logger.warn('[event:invoice:new] got UNKNOWN error status')
break
@ -161,6 +181,9 @@ module.exports = (
)
break
}
default: {
logger.error('[event:invoice:new] UNKNOWN LND error')
}
}
})
return () => {
@ -184,12 +207,18 @@ module.exports = (
logger.error('New transactions stream error:' + subID, err)
})
stream.on('status', status => {
logger.error('New transactions stream status:' + subID, status)
logger.info('New transactions stream status:' + subID, status)
switch (status.code) {
case 0: {
logger.info('[event:transaction:new] stream ok')
break
}
case 1: {
logger.info(
'[event:transaction:new] stream canceled, probably socket disconnected'
)
break
}
case 2: {
//Happens to fire when the grpc client lose access to macaroon file
logger.warn('[event:transaction:new] got UNKNOWN error status')
@ -218,6 +247,9 @@ module.exports = (
)
break
}
default: {
logger.error('[event:transaction:new] UNKNOWN LND error')
}
}
})
return () => {
@ -225,43 +257,19 @@ module.exports = (
}
}
io.on('connection', socket => {
io.of('default').on('connection', socket => {
logger.info(`io.onconnection`)
logger.info('socket.handshake', socket.handshake)
const isOneTimeUseSocket = !!socket.handshake.query.IS_GUN_AUTH
const isLNDSocket = !!socket.handshake.query.IS_LND_SOCKET
const isNotificationsSocket = !!socket.handshake.query
.IS_NOTIFICATIONS_SOCKET
if (!isLNDSocket) {
/** printing out the client who joined */
logger.info('New socket client connected (id=' + socket.id + ').')
}
if (isOneTimeUseSocket) {
logger.info('New socket is one time use')
socket.on('IS_GUN_AUTH', () => {
try {
const isGunAuth = Mediator.isAuthenticated()
socket.emit('IS_GUN_AUTH', {
ok: true,
msg: {
isGunAuth
},
origBody: {}
})
socket.disconnect()
} catch (err) {
socket.emit('IS_GUN_AUTH', {
ok: false,
msg: err.message,
origBody: {}
})
socket.disconnect()
}
})
} else {
if (isLNDSocket) {
const subID = Math.floor(Math.random() * 1000).toString()
const isNotifications = isNotificationsSocket ? 'notifications' : ''
@ -273,19 +281,208 @@ module.exports = (
cancelInvoiceStream()
cancelTransactionStream()
})
}
})
io.of('gun').on('connect', socket => {
// TODO: off()
try {
if (!isAuthenticated()) {
socket.emit(Common.Constants.ErrorCode.NOT_AUTH)
return
}
logger.info('New socket is NOT one time use')
// this is where we create the websocket connection
// with the GunDB service.
Mediator.createMediator(socket)
/** listening if client has disconnected */
socket.on('disconnect', () => {
logger.info('client disconnected (id=' + socket.id + ').')
})
const { $shock, publicKeyForDecryption } = socket.handshake.query
const [root, path, method] = $shock.split('::')
// eslint-disable-next-line init-declarations
let node
if (root === '$gun') {
node = getGun()
} else if (root === '$user') {
node = getUser()
} else {
node = getGun().user(root)
}
for (const bit of path.split('>')) {
node = node.get(bit)
}
/**
* @param {ValidDataValue} data
* @param {string} key
*/
const listener = async (data, key) => {
try {
if (publicKeyForDecryption) {
const decData = await deepDecryptIfNeeded(
data,
publicKeyForDecryption
)
socket.emit('$shock', decData, key)
} else {
socket.emit('$shock', data, key)
}
} catch (err) {
logger.error(
`Error for gun rpc socket, query ${$shock} -> ${err.message}`
)
}
}
if (method === 'on') {
node.on(listener)
} else if (method === 'open') {
node.open(listener)
} else if (method === 'map.on') {
node.map().on(listener)
} else if (method === 'map.once') {
node.map().once(listener)
} else {
throw new TypeError(
`Invalid method for gun rpc call : ${method}, query: ${$shock}`
)
}
} catch (err) {
logger.error('GUNRPC: ' + err.message)
}
})
io.of('lndstreaming').on('connect', socket => {
// TODO: unsubscription
/**
* Streaming stuff in LND uses these events: data, status, end, error.
*/
try {
if (!isAuthenticated()) {
socket.emit(Common.Constants.ErrorCode.NOT_AUTH)
return
}
const { services } = LightningServices
const { service, method, args: unParsed } = socket.handshake.query
const args = JSON.parse(unParsed)
const call = services[service][method](args)
call.on('data', _data => {
// socket.io serializes buffers differently from express
const data = (() => {
if (!Common.Schema.isObj(_data)) {
return _data
}
return mapValues(_data, (item, key) => {
if (!(item instanceof Buffer)) {
return item
}
return item.toJSON()
})
})()
socket.emit('data', data)
})
call.on('status', status => {
socket.emit('status', status)
})
call.on('end', () => {
socket.emit('end')
})
call.on('error', err => {
// 'error' is a reserved event name we can't use it
socket.emit('$error', err)
})
// Possibly allow streaming writes such as sendPaymentV2
socket.on('write', args => {
call.write(args)
})
} catch (err) {
logger.error('LNDRPC: ' + err.message)
}
})
/**
* @param {string} token
* @returns {Promise<boolean>}
*/
const isValidToken = async token => {
const validation = await auth.validateToken(token)
if (typeof validation !== 'object') {
return false
}
if (validation === null) {
return false
}
if (typeof validation.valid !== 'boolean') {
return false
}
return validation.valid
}
/** @type {null|NodeJS.Timeout} */
let pingIntervalID = null
io.of('shockping').on(
'connect',
// TODO: make this sync
async socket => {
try {
if (!isAuthenticated()) {
socket.emit(Common.Constants.ErrorCode.NOT_AUTH)
return
}
const { token } = socket.handshake.query
const isAuth = await isValidToken(token)
if (!isAuth) {
logger.warn('invalid token for socket ping')
socket.emit(Common.Constants.ErrorCode.NOT_AUTH)
return
}
if (pingIntervalID !== null) {
logger.error('Tried to set ping socket twice')
}
socket.emit('shockping')
pingIntervalID = setInterval(() => {
socket.emit('shockping')
}, 3000)
socket.on('disconnect', () => {
logger.warn('ping socket disconnected')
if (pingIntervalID !== null) {
clearInterval(pingIntervalID)
pingIntervalID = null
}
})
} catch (err) {
logger.error('GUNRPC: ' + err.message)
}
}
)
return io
}

View file

@ -19,6 +19,13 @@ const asyncFilter = async (arr, cb) => {
return arr.filter((_, i) => results[i])
}
const wait = (seconds = 0) =>
new Promise(resolve => {
/** @type {NodeJS.Timeout} */
const timer = setTimeout(() => resolve(timer), seconds * 1000)
})
module.exports = {
asyncFilter
asyncFilter,
wait
}

View file

@ -107,7 +107,7 @@ class LNDErrorManager {
this._healthListeners.length = 0
this._isCheckingHealth = false
}
const deadline = Date.now() + 4000
const deadline = Date.now() + 10000
lightning.getInfo({},{deadline}, callback)
}

View file

@ -3,6 +3,8 @@
*/
const Crypto = require('crypto')
const logger = require('winston')
const Common = require('shock-common')
const Ramda = require('ramda')
const lightningServices = require('./lightning-services')
/**
@ -337,7 +339,73 @@ const sendPaymentV2Invoice = params => {
})
}
/**
* @param {Common.APISchema.ListPaymentsRequest} req
* @throws {TypeError}
* @returns {Promise<Common.APISchema.ListPaymentsResponseParsed>}
*/
const listPayments = req => {
return Common.Utils.makePromise((res, rej) => {
lightningServices.lightning.listPayments(
req,
/**
* @param {{ details: any; }} err
* @param {unknown} lpres
*/ (err, lpres) => {
if (err) {
return rej(new Error(err.details || err))
}
if (!Common.APISchema.isListPaymentsResponse(lpres)) {
return rej(new TypeError(`Response from LND not in expected format.`))
}
/** @type {Common.APISchema.ListPaymentsResponseParsed} */
// @ts-expect-error
const parsed = Ramda.evolve(
{
first_index_offset: x => Number(x),
last_index_offset: x => Number(x),
payments: x => x
},
lpres
)
if (Common.APISchema.isListPaymentsResponseParsed(parsed)) {
return res(parsed)
}
return rej(new TypeError(`could not parse response from LND`))
}
)
})
}
/**
* @param {string} payReq
* @returns {Promise<Common.Schema.InvoiceWhenDecoded>}
*/
const decodePayReq = payReq =>
Common.Utils.makePromise((res, rej) => {
lightningServices.lightning.decodePayReq(
{ pay_req: payReq },
/**
* @param {{ message: any; }} err
* @param {any} paymentRequest
*/
(err, paymentRequest) => {
if (err) {
rej(new Error(err.message))
} else {
res(paymentRequest)
}
}
)
})
module.exports = {
sendPaymentV2Keysend,
sendPaymentV2Invoice
sendPaymentV2Invoice,
listPayments,
decodePayReq
}

218
utils/lndJobs.js Normal file
View file

@ -0,0 +1,218 @@
/**
* @prettier
*/
const Logger = require('winston')
const { wait } = require('./helpers')
const Key = require('../services/gunDB/contact-api/key')
const { getUser } = require('../services/gunDB/Mediator')
const LightningServices = require('./lightningServices')
const ERROR_TRIES_THRESHOLD = 3
const ERROR_TRIES_DELAY = 500
const INVOICE_STATE = {
OPEN: 'OPEN',
SETTLED: 'SETTLED',
CANCELLED: 'CANCELLED',
ACCEPTED: 'ACCEPTED'
}
const _lookupInvoice = hash =>
new Promise((resolve, reject) => {
const { lightning } = LightningServices.services
lightning.lookupInvoice({ r_hash: hash }, (err, response) => {
if (err) {
Logger.error(
'[TIP] An error has occurred while trying to lookup invoice:',
err,
'\nInvoice Hash:',
hash
)
reject(err)
return
}
Logger.info('[TIP] Invoice lookup result:', response)
resolve(response)
})
})
const _getPostTipInfo = ({ postID, page }) =>
new Promise((resolve, reject) => {
getUser()
.get(Key.WALL)
.get(Key.PAGES)
.get(page)
.get(Key.POSTS)
.get(postID)
.once(post => {
if (post && post.date) {
const { tipCounter, tipValue } = post
console.log(post)
resolve({
tipCounter: typeof tipCounter === 'number' ? tipCounter : 0,
tipValue: typeof tipValue === 'number' ? tipValue : 0
})
}
resolve(post)
})
})
const _incrementPost = ({ postID, page, orderAmount }) =>
new Promise((resolve, reject) => {
const parsedAmount = parseFloat(orderAmount)
if (typeof parsedAmount !== 'number') {
reject(new Error('Invalid order amount specified'))
return
}
Logger.info('[POST TIP] Getting Post Tip Values...')
return _getPostTipInfo({ postID, page })
.then(({ tipValue, tipCounter }) => {
const updatedTip = {
tipCounter: tipCounter + 1,
tipValue: tipValue + parsedAmount
}
getUser()
.get(Key.WALL)
.get(Key.PAGES)
.get(page)
.get(Key.POSTS)
.get(postID)
.put(updatedTip, () => {
Logger.info('[POST TIP] Successfully updated Post tip info')
resolve(updatedTip)
})
})
.catch(err => {
Logger.error(err)
reject(err)
})
})
const _updateTipData = (invoiceHash, data) =>
new Promise((resolve, reject) => {
try {
getUser()
.get(Key.TIPS_PAYMENT_STATUS)
.get(invoiceHash)
.put(data, tip => {
if (tip === undefined) {
reject(new Error('Tip update failed'))
return
}
console.log(tip)
resolve(tip)
})
} catch (err) {
Logger.error('An error has occurred while updating tip^data')
throw err
}
})
const _getTipData = (invoiceHash, tries = 0) =>
new Promise((resolve, reject) => {
if (tries >= ERROR_TRIES_THRESHOLD) {
reject(new Error('Malformed data'))
return
}
getUser()
.get(Key.TIPS_PAYMENT_STATUS)
.get(invoiceHash)
.once(async tip => {
try {
if (tip === undefined) {
await wait(ERROR_TRIES_DELAY)
const tip = await _getTipData(invoiceHash, tries + 1)
if (tip) {
resolve(tip)
return
}
reject(new Error('Malformed data'))
return
}
resolve(tip)
} catch (err) {
reject(err)
}
})
})
const executeTipAction = (tip, invoice) => {
if (invoice.state !== INVOICE_STATE.SETTLED) {
return
}
// Execute actions once invoice is settled
Logger.info('Invoice settled!', invoice)
if (tip.targetType === 'post') {
_incrementPost({
postID: tip.postID,
page: tip.postPage,
orderAmount: invoice.amt_paid_sat
})
}
}
const updateUnverifiedTips = () => {
getUser()
.get(Key.TIPS_PAYMENT_STATUS)
.map()
.once(async (tip, id) => {
try {
if (
!tip ||
tip.state !== INVOICE_STATE.OPEN ||
(tip._errorCount && tip._errorCount >= ERROR_TRIES_THRESHOLD)
) {
return
}
Logger.info('Unverified invoice found!', tip)
const invoice = await _lookupInvoice(tip.hash)
Logger.info('Invoice located:', invoice)
if (invoice.state !== tip.state) {
await _updateTipData(id, { state: invoice.state })
// Actions to be executed when the tip's state is updated
executeTipAction(tip, invoice)
}
} catch (err) {
Logger.error('[TIP] An error has occurred while updating invoice', err)
const errorCount = tip._errorCount ? tip._errorCount : 0
_updateTipData(id, {
_errorCount: errorCount + 1
})
}
})
}
const startTipStatusJob = () => {
const { lightning } = LightningServices.services
const stream = lightning.subscribeInvoices({})
updateUnverifiedTips()
stream.on('data', async invoice => {
const hash = invoice.r_hash.toString('base64')
const tip = await _getTipData(hash)
if (tip.state !== invoice.state) {
await _updateTipData(hash, { state: invoice.state })
executeTipAction(tip, invoice)
}
})
stream.on('error', err => {
Logger.error('Tip Job error' + err.details)
})
}
module.exports = {
startTipStatusJob
}

View file

@ -565,10 +565,10 @@
dependencies:
"@babel/types" "^7.3.0"
"@types/bluebird@*":
version "3.5.31"
resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.31.tgz#d17fa0ec242b51c3db302481c557ce3813bf45cb"
integrity sha512-0PKlnDIxOh3xJHwJpVONR2PP11LhdM+QYiLJGLIbzMqRwLAPxN6lQar2RpdRhfIEh/HjVMgMdhHWJA0CgC5X6w==
"@types/bluebird@^3.5.32":
version "3.5.32"
resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.32.tgz#381e7b59e39f010d20bbf7e044e48f5caf1ab620"
integrity sha512-dIOxFfI0C+jz89g6lQ+TqhGgPQ0MxSnh/E4xuC0blhFtyW269+mPG5QeLgbdwst/LvdP8o1y0o/Gz5EHXLec/g==
"@types/body-parser@*":
version "1.17.1"
@ -1335,6 +1335,11 @@ bluebird@^3.5.0:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==
bluebird@^3.7.2:
version "3.7.2"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
bn.js@=4.11.8, bn.js@^4.4.0:
version "4.11.8"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
@ -6016,10 +6021,10 @@ shellwords@^0.1.1:
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
shock-common@8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/shock-common/-/shock-common-8.0.0.tgz#4dbc8c917adfb221a00b6d1e815c4d26d205ce66"
integrity sha512-X9jkSxNUjQOcVdEAGBl6dlBgBxF9MpjV50Cih4hoqLqeGfrAYHK/iqgXgDyaHkLraHRxdP6FWJ2DoWOpuBgpDQ==
shock-common@16.x.x:
version "16.0.0"
resolved "https://registry.yarnpkg.com/shock-common/-/shock-common-16.0.0.tgz#6ef5c6b18ecfb2a558edee8693511c455784f848"
integrity sha512-8kDZYzWWyOKdBSmm1M72LneS79ma9fM7j0S+etiOILl7zXe8JMidfITex094vi3bkKOlqxzQ+ud2hogqCTmYow==
dependencies:
immer "^6.0.6"
lodash "^4.17.19"