commit
0a38cfe4f5
25 changed files with 1457 additions and 252 deletions
|
|
@ -83,7 +83,12 @@
|
||||||
|
|
||||||
"no-undefined": "off",
|
"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",
|
"parser": "babel-eslint",
|
||||||
"env": {
|
"env": {
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ module.exports = (mainnet = false) => {
|
||||||
logfile: "shockapi.log",
|
logfile: "shockapi.log",
|
||||||
lndLogFile: parsePath(`${lndDirectory}/logs/bitcoin/${network}/lnd.log`),
|
lndLogFile: parsePath(`${lndDirectory}/logs/bitcoin/${network}/lnd.log`),
|
||||||
lndDirPath: lndDirectory,
|
lndDirPath: lndDirectory,
|
||||||
peers: ['http://gun.shock.network:8765/gun','http://gun2.shock.network:8765/gun'],
|
peers: ['http://gun.shock.network:8765/gun'],
|
||||||
useTLS: false,
|
useTLS: false,
|
||||||
tokenExpirationMS: 259200000
|
tokenExpirationMS: 259200000
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,9 @@
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
gun = Gun({
|
gun = Gun({
|
||||||
peers: [
|
peers: [
|
||||||
`http://guntest.shock.network:8765/gun`
|
'http://gun.shock.network:8765/gun',
|
||||||
|
//'http://gun2.shock.network:8765/gun'
|
||||||
],
|
],
|
||||||
axe: false
|
axe: false
|
||||||
})
|
})
|
||||||
|
|
@ -60,4 +61,4 @@
|
||||||
background-color: black;
|
background-color: black;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
4
main.js
4
main.js
|
|
@ -1,8 +1,10 @@
|
||||||
const program = require("commander");
|
const program = require("commander");
|
||||||
|
|
||||||
|
const {version} = (JSON.parse(require('fs').readFileSync("./package.json", "utf-8")))
|
||||||
|
|
||||||
// parse command line parameters
|
// parse command line parameters
|
||||||
program
|
program
|
||||||
.version("1.0.0")
|
.version(version)
|
||||||
.option("-s, --serverport [port]", "web server http listening port (defaults to 8280)")
|
.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("-x, --httpsport [port]", "web server https listening port (defaults to 8283)")
|
||||||
.option("-h, --serverhost [host]", "web server listening host (defaults to localhost)")
|
.option("-h, --serverhost [host]", "web server listening host (defaults to localhost)")
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@
|
||||||
"basic-auth": "^2.0.0",
|
"basic-auth": "^2.0.0",
|
||||||
"big.js": "^5.2.2",
|
"big.js": "^5.2.2",
|
||||||
"bitcore-lib": "^0.15.0",
|
"bitcore-lib": "^0.15.0",
|
||||||
|
"bluebird": "^3.7.2",
|
||||||
"body-parser": "^1.16.0",
|
"body-parser": "^1.16.0",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"command-exists": "^1.2.6",
|
"command-exists": "^1.2.6",
|
||||||
|
|
@ -48,7 +49,7 @@
|
||||||
"request-promise": "^4.2.2",
|
"request-promise": "^4.2.2",
|
||||||
"response-time": "^2.3.2",
|
"response-time": "^2.3.2",
|
||||||
"shelljs": "^0.8.2",
|
"shelljs": "^0.8.2",
|
||||||
"shock-common": "8.0.0",
|
"shock-common": "16.x.x",
|
||||||
"socket.io": "2.1.1",
|
"socket.io": "2.1.1",
|
||||||
"text-encoding": "^0.7.0",
|
"text-encoding": "^0.7.0",
|
||||||
"tingodb": "^0.6.1",
|
"tingodb": "^0.6.1",
|
||||||
|
|
@ -57,7 +58,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-class-properties": "^7.5.5",
|
"@babel/plugin-proposal-class-properties": "^7.5.5",
|
||||||
"@types/bluebird": "*",
|
"@types/bluebird": "^3.5.32",
|
||||||
"@types/dotenv": "^6.1.1",
|
"@types/dotenv": "^6.1.1",
|
||||||
"@types/express": "^4.17.1",
|
"@types/express": "^4.17.1",
|
||||||
"@types/gun": "^0.9.2",
|
"@types/gun": "^0.9.2",
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ const Config = require('../config')
|
||||||
// TO DO: move to common repo
|
// TO DO: move to common repo
|
||||||
/**
|
/**
|
||||||
* @typedef {object} SimpleSocket
|
* @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 {(eventName: string, handler: (data: any) => void) => void} on
|
||||||
* @prop {{ query: { 'x-shockwallet-device-id': string }}} handshake
|
* @prop {{ query: { 'x-shockwallet-device-id': string }}} handshake
|
||||||
*/
|
*/
|
||||||
|
|
@ -224,7 +224,6 @@ let user
|
||||||
/** @type {string|null} */
|
/** @type {string|null} */
|
||||||
let _currentAlias = null
|
let _currentAlias = null
|
||||||
/** @type {string|null} */
|
/** @type {string|null} */
|
||||||
let _currentPass = null
|
|
||||||
|
|
||||||
/** @type {string|null} */
|
/** @type {string|null} */
|
||||||
let mySec = null
|
let mySec = null
|
||||||
|
|
@ -361,7 +360,6 @@ const authenticate = async (alias, pass, __user) => {
|
||||||
mySec = await mySEA.secret(_user._.sea.epub, _user._.sea)
|
mySec = await mySEA.secret(_user._.sea.epub, _user._.sea)
|
||||||
|
|
||||||
_currentAlias = alias
|
_currentAlias = alias
|
||||||
_currentPass = await mySEA.encrypt(pass, mySec)
|
|
||||||
|
|
||||||
await new Promise(res => setTimeout(res, 5000))
|
await new Promise(res => setTimeout(res, 5000))
|
||||||
|
|
||||||
|
|
@ -422,6 +420,7 @@ const instantiateGun = () => {
|
||||||
|
|
||||||
const _gun = /** @type {unknown} */ (new Gun({
|
const _gun = /** @type {unknown} */ (new Gun({
|
||||||
axe: false,
|
axe: false,
|
||||||
|
multicast: false,
|
||||||
peers: Config.PEERS
|
peers: Config.PEERS
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
@ -432,29 +431,11 @@ const instantiateGun = () => {
|
||||||
|
|
||||||
instantiateGun()
|
instantiateGun()
|
||||||
|
|
||||||
const freshGun = async () => {
|
const freshGun = () => {
|
||||||
const _gun = /** @type {unknown} */ (new Gun({
|
return {
|
||||||
axe: false,
|
gun,
|
||||||
peers: Config.PEERS
|
user
|
||||||
}))
|
|
||||||
|
|
||||||
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 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') {
|
if (typeof ack.err === 'string') {
|
||||||
throw new Error(ack.err)
|
throw new Error(ack.err)
|
||||||
} else if (typeof ack.pub === 'string' || typeof user._.sea === 'object') {
|
} else if (typeof ack.pub === 'string' || typeof user._.sea === 'object') {
|
||||||
const mySecret = await mySEA.secret(user._.sea.epub, user._.sea)
|
// OK
|
||||||
_currentAlias = alias
|
|
||||||
_currentPass = await mySEA.encrypt(pass, mySecret)
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error('unknown error, ack: ' + JSON.stringify(ack))
|
throw new Error('unknown error, ack: ' + JSON.stringify(ack))
|
||||||
}
|
}
|
||||||
|
|
@ -1475,5 +1454,6 @@ module.exports = {
|
||||||
getUser,
|
getUser,
|
||||||
mySEA,
|
mySEA,
|
||||||
getMySecret,
|
getMySecret,
|
||||||
freshGun
|
freshGun,
|
||||||
|
$$__SHOCKWALLET__ENCRYPTED__
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* @prettier
|
* @prettier
|
||||||
*/
|
*/
|
||||||
type Primitive = boolean | string | number
|
export type Primitive = boolean | string | number
|
||||||
|
|
||||||
export interface Data {
|
export interface Data {
|
||||||
[K: string]: ValidDataValue
|
[K: string]: ValidDataValue
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,14 @@ const uuidv1 = require('uuid/v1')
|
||||||
const logger = require('winston')
|
const logger = require('winston')
|
||||||
const Common = require('shock-common')
|
const Common = require('shock-common')
|
||||||
const { Constants, Schema } = Common
|
const { Constants, Schema } = Common
|
||||||
|
const Gun = require('gun')
|
||||||
|
|
||||||
const { ErrorCode } = Constants
|
const { ErrorCode } = Constants
|
||||||
|
|
||||||
const { sendPaymentV2Invoice } = require('../../../utils/lightningServices/v2')
|
const {
|
||||||
|
sendPaymentV2Invoice,
|
||||||
|
decodePayReq
|
||||||
|
} = require('../../../utils/lightningServices/v2')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../../../utils/lightningServices/types').PaymentV2} PaymentV2
|
* @typedef {import('../../../utils/lightningServices/types').PaymentV2} PaymentV2
|
||||||
|
|
@ -815,7 +819,7 @@ const setAvatar = (avatar, user) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
user
|
user
|
||||||
.get(Key.PROFILE)
|
.get(Key.PROFILE_BINARY)
|
||||||
.get(Key.AVATAR)
|
.get(Key.AVATAR)
|
||||||
.put(avatar, ack => {
|
.put(avatar, ack => {
|
||||||
if (ack.err && typeof ack.err !== 'number') {
|
if (ack.err && typeof ack.err !== 'number') {
|
||||||
|
|
@ -905,17 +909,30 @@ const sendHRWithInitialMsg = async (
|
||||||
await sendMessage(recipientPublicKey, initialMsg, user, SEA)
|
await sendMessage(recipientPublicKey, initialMsg, user, SEA)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} SpontPaymentOptions
|
||||||
|
* @prop {Common.Schema.OrderTargetType} type
|
||||||
|
* @prop {string=} postID
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the preimage corresponding to the payment.
|
* Returns the preimage corresponding to the payment.
|
||||||
* @param {string} to
|
* @param {string} to
|
||||||
* @param {number} amount
|
* @param {number} amount
|
||||||
* @param {string} memo
|
* @param {string} memo
|
||||||
* @param {number} feeLimit
|
* @param {number} feeLimit
|
||||||
|
* @param {SpontPaymentOptions} opts
|
||||||
* @throws {Error} If no response in less than 20 seconds from the recipient, or
|
* @throws {Error} If no response in less than 20 seconds from the recipient, or
|
||||||
* lightning cannot find a route for the payment.
|
* lightning cannot find a route for the payment.
|
||||||
* @returns {Promise<PaymentV2>} The payment's preimage.
|
* @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 {
|
try {
|
||||||
const SEA = require('../Mediator').mySEA
|
const SEA = require('../Mediator').mySEA
|
||||||
const getUser = () => require('../Mediator').getUser()
|
const getUser = () => require('../Mediator').getUser()
|
||||||
|
|
@ -935,7 +952,12 @@ const sendSpontaneousPayment = async (to, amount, memo, feeLimit) => {
|
||||||
amount: amount.toString(),
|
amount: amount.toString(),
|
||||||
from: getUser()._.sea.pub,
|
from: getUser()._.sea.pub,
|
||||||
memo: memo || 'no memo',
|
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))
|
logger.info(JSON.stringify(order))
|
||||||
|
|
@ -1021,6 +1043,21 @@ const sendSpontaneousPayment = async (to, amount, memo, feeLimit) => {
|
||||||
throw new Error(orderResponse.response)
|
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')
|
logger.info('Will now send payment through lightning')
|
||||||
|
|
||||||
const payment = await sendPaymentV2Invoice({
|
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[]} tags
|
||||||
* @param {string} title
|
* @param {string} title
|
||||||
|
|
@ -1311,26 +1391,24 @@ const createPost = async (tags, title, content) => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
/** @type {string} */
|
const [postID, newPost] = await createPostNew(tags, title, content)
|
||||||
const postID = await new Promise((res, rej) => {
|
|
||||||
const _n = require('../Mediator')
|
await Common.makePromise((res, rej) => {
|
||||||
|
require('../Mediator')
|
||||||
.getUser()
|
.getUser()
|
||||||
.get(Key.WALL)
|
.get(Key.WALL)
|
||||||
.get(Key.PAGES)
|
.get(Key.PAGES)
|
||||||
.get(pageIdx)
|
.get(pageIdx)
|
||||||
.get(Key.POSTS)
|
.get(Key.POSTS)
|
||||||
.set(
|
.get(postID)
|
||||||
{
|
.put(
|
||||||
date: Date.now(),
|
// @ts-expect-error
|
||||||
status: 'publish',
|
newPost,
|
||||||
tags: tags.join('-'),
|
|
||||||
title
|
|
||||||
},
|
|
||||||
ack => {
|
ack => {
|
||||||
if (ack.err && typeof ack.err !== 'number') {
|
if (ack.err && typeof ack.err !== 'number') {
|
||||||
rej(new Error(ack.err))
|
rej(new Error(ack.err))
|
||||||
} else {
|
} 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 => {
|
const loadedPost = await new Promise(res => {
|
||||||
require('../Mediator')
|
require('../Mediator')
|
||||||
.getUser()
|
.getUser()
|
||||||
|
|
@ -1434,11 +1466,25 @@ const createPost = async (tags, title, content) => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} postId
|
* @param {string} postId
|
||||||
|
* @param {string} page
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
const deletePost = async postId => {
|
const deletePost = async (postId, page) => {
|
||||||
await new Promise(res => {
|
await new Promise((res, rej) => {
|
||||||
res(postId)
|
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) {
|
if (!avatarSubbed) {
|
||||||
avatarSubbed = true
|
avatarSubbed = true
|
||||||
user
|
user
|
||||||
.get(Key.PROFILE)
|
.get(Key.PROFILE_BINARY)
|
||||||
.get(Key.AVATAR)
|
.get(Key.AVATAR)
|
||||||
.on(avatar => {
|
.on(avatar => {
|
||||||
if (typeof avatar === 'string' || avatar === null) {
|
if (typeof avatar === 'string' || avatar === null) {
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,7 @@ const setReceivedReqsMap = reqs => {
|
||||||
}
|
}
|
||||||
|
|
||||||
listeners.add(() => {
|
listeners.add(() => {
|
||||||
logger.info(
|
logger.info(`new received reqs: ${size(getReceivedReqs())}`)
|
||||||
`new received reqs: ${JSON.stringify(getReceivedReqs(), null, 4)}`
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const react = debounce(() => {
|
const react = debounce(() => {
|
||||||
|
|
|
||||||
|
|
@ -103,8 +103,40 @@ const getMyUser = async () => {
|
||||||
|
|
||||||
return u
|
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.getMyUser = getMyUser
|
||||||
|
module.exports.getUserInfo = getUserInfo
|
||||||
module.exports.Follows = require('./follows')
|
module.exports.Follows = require('./follows')
|
||||||
|
|
||||||
module.exports.getWallPage = Wall.getWallPage
|
module.exports.getWallPage = Wall.getWallPage
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const { performance } = require('perf_hooks')
|
||||||
const logger = require('winston')
|
const logger = require('winston')
|
||||||
const isFinite = require('lodash/isFinite')
|
const isFinite = require('lodash/isFinite')
|
||||||
const isNumber = require('lodash/isNumber')
|
const isNumber = require('lodash/isNumber')
|
||||||
|
|
@ -38,9 +39,27 @@ const ordersProcessed = new Set()
|
||||||
* @prop {boolean} private
|
* @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 = ''
|
let currentOrderAddr = ''
|
||||||
|
|
||||||
/** @param {InvoiceRequest} invoiceReq */
|
/**
|
||||||
|
* @param {InvoiceRequest} invoiceReq
|
||||||
|
* @returns {Promise<InvoiceResponse>}
|
||||||
|
*/
|
||||||
const _addInvoice = invoiceReq =>
|
const _addInvoice = invoiceReq =>
|
||||||
new Promise((resolve, rej) => {
|
new Promise((resolve, rej) => {
|
||||||
const {
|
const {
|
||||||
|
|
@ -49,12 +68,12 @@ const _addInvoice = invoiceReq =>
|
||||||
|
|
||||||
lightning.addInvoice(invoiceReq, (
|
lightning.addInvoice(invoiceReq, (
|
||||||
/** @type {any} */ error,
|
/** @type {any} */ error,
|
||||||
/** @type {{ payment_request: string }} */ response
|
/** @type {InvoiceResponse} */ response
|
||||||
) => {
|
) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
rej(error)
|
rej(error)
|
||||||
} else {
|
} else {
|
||||||
resolve(response.payment_request)
|
resolve(response)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -85,7 +104,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const listenerStartTime = Date.now()
|
const listenerStartTime = performance.now()
|
||||||
|
|
||||||
ordersProcessed.add(orderID)
|
ordersProcessed.add(orderID)
|
||||||
|
|
||||||
|
|
@ -95,7 +114,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
||||||
)} -- addr: ${addr}`
|
)} -- addr: ${addr}`
|
||||||
)
|
)
|
||||||
|
|
||||||
const orderAnswerStartTime = Date.now()
|
const orderAnswerStartTime = performance.now()
|
||||||
|
|
||||||
const alreadyAnswered = await getUser()
|
const alreadyAnswered = await getUser()
|
||||||
.get(Key.ORDER_TO_RESPONSE)
|
.get(Key.ORDER_TO_RESPONSE)
|
||||||
|
|
@ -107,11 +126,11 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderAnswerEndTime = Date.now() - orderAnswerStartTime
|
const orderAnswerEndTime = performance.now() - orderAnswerStartTime
|
||||||
|
|
||||||
logger.info(`[PERF] Order Already Answered: ${orderAnswerEndTime}ms`)
|
logger.info(`[PERF] Order Already Answered: ${orderAnswerEndTime}ms`)
|
||||||
|
|
||||||
const decryptStartTime = Date.now()
|
const decryptStartTime = performance.now()
|
||||||
|
|
||||||
const senderEpub = await Utils.pubToEpub(order.from)
|
const senderEpub = await Utils.pubToEpub(order.from)
|
||||||
const secret = await SEA.secret(senderEpub, getUser()._.sea)
|
const secret = await SEA.secret(senderEpub, getUser()._.sea)
|
||||||
|
|
@ -121,7 +140,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
||||||
SEA.decrypt(order.memo, secret)
|
SEA.decrypt(order.memo, secret)
|
||||||
])
|
])
|
||||||
|
|
||||||
const decryptEndTime = Date.now() - decryptStartTime
|
const decryptEndTime = performance.now() - decryptStartTime
|
||||||
|
|
||||||
logger.info(`[PERF] Decrypt invoice info: ${decryptEndTime}ms`)
|
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)}`
|
`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 invoice = await _addInvoice(invoiceReq)
|
||||||
|
|
||||||
const invoiceEndTime = Date.now() - invoiceStartTime
|
const invoiceEndTime = performance.now() - invoiceStartTime
|
||||||
|
|
||||||
logger.info(`[PERF] LND Invoice created in ${invoiceEndTime}ms`)
|
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'
|
'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`)
|
logger.info(`[PERF] Invoice encrypted in ${invoiceEncryptEndTime}ms`)
|
||||||
|
|
||||||
|
|
@ -189,12 +205,13 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
||||||
type: 'invoice'
|
type: 'invoice'
|
||||||
}
|
}
|
||||||
|
|
||||||
const invoicePutStartTime = Date.now()
|
const invoicePutStartTime = performance.now()
|
||||||
|
|
||||||
await new Promise((res, rej) => {
|
await new Promise((res, rej) => {
|
||||||
getUser()
|
getUser()
|
||||||
.get(Key.ORDER_TO_RESPONSE)
|
.get(Key.ORDER_TO_RESPONSE)
|
||||||
.get(orderID)
|
.get(orderID)
|
||||||
|
// @ts-expect-error
|
||||||
.put(orderResponse, ack => {
|
.put(orderResponse, ack => {
|
||||||
if (ack.err && typeof ack.err !== 'number') {
|
if (ack.err && typeof ack.err !== 'number') {
|
||||||
rej(
|
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`)
|
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`)
|
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) {
|
} catch (err) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`error inside onOrders, orderAddr: ${addr}, orderID: ${orderID}, order: ${JSON.stringify(
|
`error inside onOrders, orderAddr: ${addr}, orderID: ${orderID}, order: ${JSON.stringify(
|
||||||
|
|
@ -232,6 +268,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
||||||
getUser()
|
getUser()
|
||||||
.get(Key.ORDER_TO_RESPONSE)
|
.get(Key.ORDER_TO_RESPONSE)
|
||||||
.get(orderID)
|
.get(orderID)
|
||||||
|
// @ts-expect-error
|
||||||
.put(orderResponse, ack => {
|
.put(orderResponse, ack => {
|
||||||
if (ack.err && typeof ack.err !== 'number') {
|
if (ack.err && typeof ack.err !== 'number') {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
|
|
||||||
|
|
@ -55,3 +55,12 @@ exports.CONTENT_ITEMS = 'contentItems'
|
||||||
exports.FOLLOWS = 'follows'
|
exports.FOLLOWS = 'follows'
|
||||||
|
|
||||||
exports.POSTS = 'posts'
|
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')
|
require('../../Mediator')
|
||||||
.getGun()
|
.getGun()
|
||||||
.user(pub)
|
.user(pub)
|
||||||
.get(Key.PROFILE)
|
.get(Key.PROFILE_BINARY)
|
||||||
.get(Key.AVATAR)
|
.get(Key.AVATAR)
|
||||||
.on(av => {
|
.on(av => {
|
||||||
if (typeof av === 'string' || av === null) {
|
if (typeof av === 'string' || av === null) {
|
||||||
|
|
|
||||||
|
|
@ -195,11 +195,11 @@ const tryAndWait = async (promGen, shouldRetry = () => false) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
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()}`
|
` 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))
|
return timeout10(promGen(gun, user))
|
||||||
/* eslint-enable no-empty */
|
/* 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 isARealUsableNumber = require('lodash/isFinite')
|
||||||
const Big = require('big.js')
|
const Big = require('big.js')
|
||||||
const size = require('lodash/size')
|
const size = require('lodash/size')
|
||||||
const { range, flatten } = require('ramda')
|
const { range, flatten, evolve } = require('ramda')
|
||||||
|
|
||||||
const getListPage = require('../utils/paginate')
|
const getListPage = require('../utils/paginate')
|
||||||
const auth = require('../services/auth/auth')
|
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 GunKey = require('../services/gunDB/contact-api/key')
|
||||||
const {
|
const {
|
||||||
sendPaymentV2Keysend,
|
sendPaymentV2Keysend,
|
||||||
sendPaymentV2Invoice
|
sendPaymentV2Invoice,
|
||||||
|
listPayments
|
||||||
} = require('../utils/lightningServices/v2')
|
} = require('../utils/lightningServices/v2')
|
||||||
|
const { startTipStatusJob } = require('../utils/lndJobs')
|
||||||
|
const GunWriteRPC = require('../services/gunDB/rpc')
|
||||||
|
|
||||||
const DEFAULT_MAX_NUM_ROUTES_TO_QUERY = 10
|
const DEFAULT_MAX_NUM_ROUTES_TO_QUERY = 10
|
||||||
const SESSION_ID = uuid()
|
const SESSION_ID = uuid()
|
||||||
|
|
@ -303,9 +306,9 @@ module.exports = async (
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
encryptedToken = req.body.token
|
encryptedToken = req.body.token
|
||||||
encryptedKey = req.body.encryptionKey
|
encryptedKey = req.body.encryptionKey || req.body.encryptedKey
|
||||||
IV = req.body.iv
|
IV = req.body.iv
|
||||||
reqData = req.body.data
|
reqData = req.body.data || req.body.encryptedData
|
||||||
}
|
}
|
||||||
const decryptedKey = Encryption.decryptKey({
|
const decryptedKey = Encryption.decryptKey({
|
||||||
deviceId,
|
deviceId,
|
||||||
|
|
@ -413,6 +416,17 @@ module.exports = async (
|
||||||
errorMessage: 'Please create a wallet before using the API'
|
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()
|
next()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err)
|
logger.error(err)
|
||||||
|
|
@ -659,7 +673,7 @@ module.exports = async (
|
||||||
'Channel backup LND locked, new registration in 60 seconds'
|
'Channel backup LND locked, new registration in 60 seconds'
|
||||||
)
|
)
|
||||||
process.nextTick(() =>
|
process.nextTick(() =>
|
||||||
setTimeout(() => onNewTransaction(socket, subID), 60000)
|
setTimeout(() => onNewChannelBackup(), 60000)
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -673,15 +687,19 @@ module.exports = async (
|
||||||
'Channel backup LND disconnected, sockets reconnecting in 30 seconds...'
|
'Channel backup LND disconnected, sockets reconnecting in 30 seconds...'
|
||||||
)
|
)
|
||||||
process.nextTick(() =>
|
process.nextTick(() =>
|
||||||
setTimeout(() => onNewTransaction(socket, subID), 30000)
|
setTimeout(() => onNewChannelBackup(), 30000)
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
default: {
|
||||||
|
logger.error('[event:transaction:new] UNKNOWN LND error')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onNewChannelBackup()
|
onNewChannelBackup()
|
||||||
|
startTipStatusJob()
|
||||||
|
|
||||||
// Generate auth token and send it as a JSON response
|
// Generate auth token and send it as a JSON response
|
||||||
const token = await auth.generateToken()
|
const token = await auth.generateToken()
|
||||||
|
|
@ -690,6 +708,24 @@ module.exports = async (
|
||||||
user: {
|
user: {
|
||||||
alias,
|
alias,
|
||||||
publicKey
|
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) => {
|
app.post('/api/lnd/unifiedTrx', async (req, res) => {
|
||||||
try {
|
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({
|
return res.status(415).json({
|
||||||
field: 'type',
|
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
|
if (type === 'post' && typeof postID !== 'string') {
|
||||||
.status(200)
|
return res.status(400).json({
|
||||||
.json(await GunActions.sendSpontaneousPayment(to, amt, memo, feeLimit))
|
field: 'postID',
|
||||||
|
errorMessage: `Send postID`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json(
|
||||||
|
await GunActions.sendSpontaneousPayment(to, amt, memo, feeLimit, {
|
||||||
|
type,
|
||||||
|
postID
|
||||||
|
})
|
||||||
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
errorMessage: e.message
|
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
|
// get lnd node invoices list
|
||||||
app.get('/api/lnd/listinvoices', (req, res) => {
|
app.get('/api/lnd/listinvoices', (req, res) => {
|
||||||
const { lightning } = LightningServices.services
|
const { lightning } = LightningServices.services
|
||||||
|
|
@ -2014,8 +2114,11 @@ module.exports = async (
|
||||||
app.get(`/api/gun/${GunEvent.ON_CHATS}`, (_, res) => {
|
app.get(`/api/gun/${GunEvent.ON_CHATS}`, (_, res) => {
|
||||||
try {
|
try {
|
||||||
const data = Events.getChats()
|
const data = Events.getChats()
|
||||||
|
const noAvatar = data.map(mex => {
|
||||||
|
return { ...mex, recipientAvatar: null }
|
||||||
|
})
|
||||||
res.json({
|
res.json({
|
||||||
data
|
data: noAvatar
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.info('Error in Chats poll:')
|
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) => {
|
app.get(`/api/gun/${GunEvent.ON_DISPLAY_NAME}`, async (_, res) => {
|
||||||
try {
|
try {
|
||||||
const user = require('../services/gunDB/Mediator').getUser()
|
const user = require('../services/gunDB/Mediator').getUser()
|
||||||
|
|
@ -2210,11 +2290,47 @@ module.exports = async (
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.delete(`/api/gun/wall/:postID`, (_, res) =>
|
app.delete(`/api/gun/wall/:postInfo`, async (req, res) => {
|
||||||
res.status(200).json({
|
try {
|
||||||
ok: 'true'
|
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
|
* @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/', apiGunFollowsGet)
|
||||||
ap.get('/api/gun/follows/:publicKey', apiGunFollowsGet)
|
ap.get('/api/gun/follows/:publicKey', apiGunFollowsGet)
|
||||||
ap.put(`/api/gun/follows/:publicKey`, apiGunFollowsPut)
|
ap.put(`/api/gun/follows/:publicKey`, apiGunFollowsPut)
|
||||||
|
|
@ -2520,8 +2647,11 @@ module.exports = async (
|
||||||
const apiGunRequestsReceivedGet = (_, res) => {
|
const apiGunRequestsReceivedGet = (_, res) => {
|
||||||
try {
|
try {
|
||||||
const data = Events.getCurrentReceivedReqs()
|
const data = Events.getCurrentReceivedReqs()
|
||||||
|
const noAvatar = data.map(req => {
|
||||||
|
return { ...req, recipientAvatar: null }
|
||||||
|
})
|
||||||
res.json({
|
res.json({
|
||||||
data
|
data: noAvatar
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err)
|
logger.error(err)
|
||||||
|
|
@ -2537,8 +2667,11 @@ module.exports = async (
|
||||||
const apiGunRequestsSentGet = (_, res) => {
|
const apiGunRequestsSentGet = (_, res) => {
|
||||||
try {
|
try {
|
||||||
const data = Events.getCurrentSentReqs()
|
const data = Events.getCurrentSentReqs()
|
||||||
|
const noAvatar = data.map(req => {
|
||||||
|
return { ...req, recipientAvatar: null }
|
||||||
|
})
|
||||||
res.json({
|
res.json({
|
||||||
data
|
data: noAvatar
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err)
|
logger.error(err)
|
||||||
|
|
@ -2909,4 +3042,206 @@ module.exports = async (
|
||||||
data: isAuthenticated()
|
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,
|
sensitiveRoutes,
|
||||||
nonEncryptedRoutes
|
nonEncryptedRoutes
|
||||||
} = require('../utils/protectedRoutes')
|
} = require('../utils/protectedRoutes')
|
||||||
|
|
||||||
// load app default configuration data
|
// load app default configuration data
|
||||||
const defaults = require('../config/defaults')(program.mainnet)
|
const defaults = require('../config/defaults')(program.mainnet)
|
||||||
const rootFolder = process.resourcesPath || __dirname
|
const rootFolder = process.resourcesPath || __dirname
|
||||||
// define useful global variables ======================================
|
|
||||||
|
// define env variables
|
||||||
Dotenv.config()
|
Dotenv.config()
|
||||||
module.useTLS = program.usetls
|
|
||||||
module.serverPort = program.serverport || defaults.serverPort
|
const serverPort = program.serverport || defaults.serverPort
|
||||||
module.httpsPort = module.serverPort
|
const serverHost = program.serverhost || defaults.serverHost
|
||||||
module.serverHost = program.serverhost || defaults.serverHost
|
|
||||||
|
|
||||||
// setup winston logging ==========
|
// setup winston logging ==========
|
||||||
const logger = require('../config/log')(
|
const logger = require('../config/log')(
|
||||||
|
|
@ -159,12 +160,24 @@ const server = program => {
|
||||||
const startServer = async () => {
|
const startServer = async () => {
|
||||||
try {
|
try {
|
||||||
LightningServices.setDefaults(program)
|
LightningServices.setDefaults(program)
|
||||||
await LightningServices.init()
|
if (!LightningServices.isInitialized()) {
|
||||||
|
await LightningServices.init()
|
||||||
|
}
|
||||||
|
|
||||||
// init lnd module =================
|
// init lnd module =================
|
||||||
const lnd = require('../services/lnd/lnd')(
|
const lnd = require('../services/lnd/lnd')(
|
||||||
LightningServices.services.lightning
|
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')
|
const auth = require('../services/auth/auth')
|
||||||
|
|
||||||
app.use(compression())
|
app.use(compression())
|
||||||
|
|
@ -198,6 +211,11 @@ const server = program => {
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
res.set('Version', program.version())
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
await Storage.init({
|
await Storage.init({
|
||||||
dir: Path.resolve(rootFolder, '../.storage')
|
dir: Path.resolve(rootFolder, '../.storage')
|
||||||
})
|
})
|
||||||
|
|
@ -274,8 +292,8 @@ const server = program => {
|
||||||
const Sockets = require('./sockets')(io)
|
const Sockets = require('./sockets')(io)
|
||||||
|
|
||||||
require('./routes')(app, defaults, Sockets, {
|
require('./routes')(app, defaults, Sockets, {
|
||||||
serverHost: module.serverHost,
|
serverHost,
|
||||||
serverPort: module.serverPort,
|
serverPort,
|
||||||
usetls: program.usetls,
|
usetls: program.usetls,
|
||||||
CA,
|
CA,
|
||||||
CA_KEY
|
CA_KEY
|
||||||
|
|
@ -290,20 +308,11 @@ const server = program => {
|
||||||
app.use(modifyResponseBody)
|
app.use(modifyResponseBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
serverInstance.listen(module.serverPort, module.serverhost)
|
serverInstance.listen(serverPort, serverHost)
|
||||||
|
|
||||||
logger.info(
|
logger.info('App listening on ' + serverHost + ' port ' + serverPort)
|
||||||
'App listening on ' + module.serverHost + ' port ' + module.serverPort
|
|
||||||
)
|
|
||||||
|
|
||||||
module.server = serverInstance
|
module.server = serverInstance
|
||||||
|
|
||||||
// const localtunnel = require('localtunnel');
|
|
||||||
//
|
|
||||||
// const tunnel = localtunnel(port, (err, t) => {
|
|
||||||
// logger.info('err', err);
|
|
||||||
// logger.info('t', t.url);
|
|
||||||
// });
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.info(err)
|
logger.info(err)
|
||||||
logger.info('Restarting server in 30 seconds...')
|
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 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 Encryption = require('../utils/encryptionStore')
|
||||||
const LightningServices = require('../utils/lightningServices')
|
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 = (
|
module.exports = (
|
||||||
/** @type {import('socket.io').Server} */
|
/** @type {import('socket.io').Server} */
|
||||||
io
|
io
|
||||||
) => {
|
) => {
|
||||||
const Mediator = require('../services/gunDB/Mediator/index.js')
|
|
||||||
|
|
||||||
// This should be used for encrypting and emitting your data
|
// This should be used for encrypting and emitting your data
|
||||||
const emitEncryptedEvent = ({ eventName, data, socket }) => {
|
const emitEncryptedEvent = ({ eventName, data, socket }) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -134,6 +148,12 @@ module.exports = (
|
||||||
logger.info('[event:invoice:new] stream ok')
|
logger.info('[event:invoice:new] stream ok')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case 1: {
|
||||||
|
logger.info(
|
||||||
|
'[event:invoice:new] stream canceled, probably socket disconnected'
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
case 2: {
|
case 2: {
|
||||||
logger.warn('[event:invoice:new] got UNKNOWN error status')
|
logger.warn('[event:invoice:new] got UNKNOWN error status')
|
||||||
break
|
break
|
||||||
|
|
@ -161,6 +181,9 @@ module.exports = (
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
default: {
|
||||||
|
logger.error('[event:invoice:new] UNKNOWN LND error')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -184,12 +207,18 @@ module.exports = (
|
||||||
logger.error('New transactions stream error:' + subID, err)
|
logger.error('New transactions stream error:' + subID, err)
|
||||||
})
|
})
|
||||||
stream.on('status', status => {
|
stream.on('status', status => {
|
||||||
logger.error('New transactions stream status:' + subID, status)
|
logger.info('New transactions stream status:' + subID, status)
|
||||||
switch (status.code) {
|
switch (status.code) {
|
||||||
case 0: {
|
case 0: {
|
||||||
logger.info('[event:transaction:new] stream ok')
|
logger.info('[event:transaction:new] stream ok')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case 1: {
|
||||||
|
logger.info(
|
||||||
|
'[event:transaction:new] stream canceled, probably socket disconnected'
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
case 2: {
|
case 2: {
|
||||||
//Happens to fire when the grpc client lose access to macaroon file
|
//Happens to fire when the grpc client lose access to macaroon file
|
||||||
logger.warn('[event:transaction:new] got UNKNOWN error status')
|
logger.warn('[event:transaction:new] got UNKNOWN error status')
|
||||||
|
|
@ -218,6 +247,9 @@ module.exports = (
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
default: {
|
||||||
|
logger.error('[event:transaction:new] UNKNOWN LND error')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -225,67 +257,232 @@ module.exports = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
io.on('connection', socket => {
|
io.of('default').on('connection', socket => {
|
||||||
logger.info(`io.onconnection`)
|
logger.info(`io.onconnection`)
|
||||||
|
|
||||||
logger.info('socket.handshake', socket.handshake)
|
logger.info('socket.handshake', socket.handshake)
|
||||||
|
|
||||||
const isOneTimeUseSocket = !!socket.handshake.query.IS_GUN_AUTH
|
|
||||||
const isLNDSocket = !!socket.handshake.query.IS_LND_SOCKET
|
const isLNDSocket = !!socket.handshake.query.IS_LND_SOCKET
|
||||||
const isNotificationsSocket = !!socket.handshake.query
|
const isNotificationsSocket = !!socket.handshake.query
|
||||||
.IS_NOTIFICATIONS_SOCKET
|
.IS_NOTIFICATIONS_SOCKET
|
||||||
|
|
||||||
if (!isLNDSocket) {
|
if (!isLNDSocket) {
|
||||||
/** printing out the client who joined */
|
/** printing out the client who joined */
|
||||||
logger.info('New socket client connected (id=' + socket.id + ').')
|
logger.info('New socket client connected (id=' + socket.id + ').')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOneTimeUseSocket) {
|
if (isLNDSocket) {
|
||||||
logger.info('New socket is one time use')
|
const subID = Math.floor(Math.random() * 1000).toString()
|
||||||
socket.on('IS_GUN_AUTH', () => {
|
const isNotifications = isNotificationsSocket ? 'notifications' : ''
|
||||||
try {
|
logger.info('[LND] New LND Socket created:' + isNotifications + subID)
|
||||||
const isGunAuth = Mediator.isAuthenticated()
|
const cancelInvoiceStream = onNewInvoice(socket, subID)
|
||||||
socket.emit('IS_GUN_AUTH', {
|
const cancelTransactionStream = onNewTransaction(socket, subID)
|
||||||
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 */
|
|
||||||
socket.on('disconnect', () => {
|
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
|
return io
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,13 @@ const asyncFilter = async (arr, cb) => {
|
||||||
return arr.filter((_, i) => results[i])
|
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 = {
|
module.exports = {
|
||||||
asyncFilter
|
asyncFilter,
|
||||||
|
wait
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ class LNDErrorManager {
|
||||||
this._healthListeners.length = 0
|
this._healthListeners.length = 0
|
||||||
this._isCheckingHealth = false
|
this._isCheckingHealth = false
|
||||||
}
|
}
|
||||||
const deadline = Date.now() + 4000
|
const deadline = Date.now() + 10000
|
||||||
lightning.getInfo({},{deadline}, callback)
|
lightning.getInfo({},{deadline}, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
*/
|
*/
|
||||||
const Crypto = require('crypto')
|
const Crypto = require('crypto')
|
||||||
const logger = require('winston')
|
const logger = require('winston')
|
||||||
|
const Common = require('shock-common')
|
||||||
|
const Ramda = require('ramda')
|
||||||
|
|
||||||
const lightningServices = require('./lightning-services')
|
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 = {
|
module.exports = {
|
||||||
sendPaymentV2Keysend,
|
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:
|
dependencies:
|
||||||
"@babel/types" "^7.3.0"
|
"@babel/types" "^7.3.0"
|
||||||
|
|
||||||
"@types/bluebird@*":
|
"@types/bluebird@^3.5.32":
|
||||||
version "3.5.31"
|
version "3.5.32"
|
||||||
resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.31.tgz#d17fa0ec242b51c3db302481c557ce3813bf45cb"
|
resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.32.tgz#381e7b59e39f010d20bbf7e044e48f5caf1ab620"
|
||||||
integrity sha512-0PKlnDIxOh3xJHwJpVONR2PP11LhdM+QYiLJGLIbzMqRwLAPxN6lQar2RpdRhfIEh/HjVMgMdhHWJA0CgC5X6w==
|
integrity sha512-dIOxFfI0C+jz89g6lQ+TqhGgPQ0MxSnh/E4xuC0blhFtyW269+mPG5QeLgbdwst/LvdP8o1y0o/Gz5EHXLec/g==
|
||||||
|
|
||||||
"@types/body-parser@*":
|
"@types/body-parser@*":
|
||||||
version "1.17.1"
|
version "1.17.1"
|
||||||
|
|
@ -1335,6 +1335,11 @@ bluebird@^3.5.0:
|
||||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
|
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
|
||||||
integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==
|
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:
|
bn.js@=4.11.8, bn.js@^4.4.0:
|
||||||
version "4.11.8"
|
version "4.11.8"
|
||||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
|
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"
|
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
|
||||||
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
|
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
|
||||||
|
|
||||||
shock-common@8.0.0:
|
shock-common@16.x.x:
|
||||||
version "8.0.0"
|
version "16.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/shock-common/-/shock-common-8.0.0.tgz#4dbc8c917adfb221a00b6d1e815c4d26d205ce66"
|
resolved "https://registry.yarnpkg.com/shock-common/-/shock-common-16.0.0.tgz#6ef5c6b18ecfb2a558edee8693511c455784f848"
|
||||||
integrity sha512-X9jkSxNUjQOcVdEAGBl6dlBgBxF9MpjV50Cih4hoqLqeGfrAYHK/iqgXgDyaHkLraHRxdP6FWJ2DoWOpuBgpDQ==
|
integrity sha512-8kDZYzWWyOKdBSmm1M72LneS79ma9fM7j0S+etiOILl7zXe8JMidfITex094vi3bkKOlqxzQ+ud2hogqCTmYow==
|
||||||
dependencies:
|
dependencies:
|
||||||
immer "^6.0.6"
|
immer "^6.0.6"
|
||||||
lodash "^4.17.19"
|
lodash "^4.17.19"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue