commit
0a38cfe4f5
25 changed files with 1457 additions and 252 deletions
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,8 +21,9 @@
|
|||
<script>
|
||||
|
||||
gun = Gun({
|
||||
peers: [
|
||||
`http://guntest.shock.network:8765/gun`
|
||||
peers: [
|
||||
'http://gun.shock.network:8765/gun',
|
||||
//'http://gun2.shock.network:8765/gun'
|
||||
],
|
||||
axe: false
|
||||
})
|
||||
|
|
|
|||
4
main.js
4
main.js
|
|
@ -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)")
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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__
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* @prettier
|
||||
*/
|
||||
type Primitive = boolean | string | number
|
||||
export type Primitive = boolean | string | number
|
||||
|
||||
export interface Data {
|
||||
[K: string]: ValidDataValue
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
247
services/gunDB/rpc/index.js
Normal 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
|
||||
}
|
||||
8
services/gunDB/rpc/types.ts
Normal file
8
services/gunDB/rpc/types.ts
Normal 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>
|
||||
421
src/routes.js
421
src/routes.js
|
|
@ -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({
|
||||
ok: 'true'
|
||||
})
|
||||
)
|
||||
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
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
await LightningServices.init()
|
||||
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...')
|
||||
|
|
|
|||
299
src/sockets.js
299
src/sockets.js
|
|
@ -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,67 +257,232 @@ 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' : ''
|
||||
logger.info('[LND] New LND Socket created:' + isNotifications + subID)
|
||||
const cancelInvoiceStream = onNewInvoice(socket, subID)
|
||||
const cancelTransactionStream = onNewTransaction(socket, subID)
|
||||
socket.on('disconnect', () => {
|
||||
logger.info('LND socket disconnected:' + isNotifications + subID)
|
||||
cancelInvoiceStream()
|
||||
cancelTransactionStream()
|
||||
})
|
||||
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 */
|
||||
if (isLNDSocket) {
|
||||
const subID = Math.floor(Math.random() * 1000).toString()
|
||||
const isNotifications = isNotificationsSocket ? 'notifications' : ''
|
||||
logger.info('[LND] New LND Socket created:' + isNotifications + subID)
|
||||
const cancelInvoiceStream = onNewInvoice(socket, subID)
|
||||
const cancelTransactionStream = onNewTransaction(socket, subID)
|
||||
socket.on('disconnect', () => {
|
||||
logger.info('client disconnected (id=' + socket.id + ').')
|
||||
logger.info('LND socket disconnected:' + isNotifications + subID)
|
||||
cancelInvoiceStream()
|
||||
cancelTransactionStream()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
io.of('gun').on('connect', socket => {
|
||||
// TODO: off()
|
||||
|
||||
try {
|
||||
if (!isAuthenticated()) {
|
||||
socket.emit(Common.Constants.ErrorCode.NOT_AUTH)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
218
utils/lndJobs.js
Normal 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
|
||||
}
|
||||
21
yarn.lock
21
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue