commit
de45807e59
12 changed files with 1001 additions and 341 deletions
|
|
@ -7,4 +7,4 @@ SHOCK_CACHE=true
|
|||
TRUSTED_KEYS=true
|
||||
LOCAL_TUNNEL_SERVER=https://tunnel.rip
|
||||
TORRENT_SEED_URL=https://webtorrent.shock.network
|
||||
TORRENT_SEED_TOKEN=jibberish
|
||||
TORRENT_SEED_TOKEN=jibberish
|
||||
|
|
@ -105,4 +105,4 @@
|
|||
"pre-commit": "yarn lint && yarn typecheck && yarn lint-staged"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
/**
|
||||
* @format
|
||||
*/
|
||||
|
||||
const Common = require('shock-common')
|
||||
const mapValues = require('lodash/mapValues')
|
||||
const pickBy = require('lodash/pickBy')
|
||||
const Bluebird = require('bluebird')
|
||||
const Logger = require('winston')
|
||||
const Key = require('../services/gunDB/contact-api/key')
|
||||
|
||||
const { getUser, getMySecret, mySEA } = require('./gunDB/Mediator')
|
||||
|
||||
/**
|
||||
* @param {string} coordID
|
||||
* @param {Common.Coordinate} data
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
module.exports.writeCoordinate = async (coordID, data) => {
|
||||
if (coordID !== data.id) {
|
||||
throw new Error('CoordID must be equal to data.id')
|
||||
}
|
||||
|
||||
try {
|
||||
/**
|
||||
* Because there are optional properties, typescript can also allow them
|
||||
* to be specified but with a value of `undefined`. Filter out these.
|
||||
* @type {Record<string, number|boolean|string>}
|
||||
*/
|
||||
const sanitizedData = pickBy(data, v => typeof v !== 'undefined')
|
||||
|
||||
const encData = await Bluebird.props(
|
||||
mapValues(sanitizedData, v => {
|
||||
return mySEA.encrypt(v, getMySecret())
|
||||
})
|
||||
)
|
||||
|
||||
getUser()
|
||||
.get(Key.COORDINATES)
|
||||
.get(coordID)
|
||||
.put(encData, ack => {
|
||||
if (ack.err && typeof ack.err !== 'number') {
|
||||
Logger.info(
|
||||
`Error writting corrdinate, coordinate id: ${coordID}, data: ${JSON.stringify(
|
||||
data,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
)
|
||||
Logger.error(ack.err)
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
Logger.info(
|
||||
`Error writing coordinate, coordinate id: ${coordID}, data: ${JSON.stringify(
|
||||
data,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
)
|
||||
Logger.error(e.message)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,8 +11,7 @@ const { ErrorCode } = Constants
|
|||
|
||||
const {
|
||||
sendPaymentV2Invoice,
|
||||
decodePayReq,
|
||||
myLNDPub
|
||||
decodePayReq
|
||||
} = require('../../../utils/lightningServices/v2')
|
||||
|
||||
/**
|
||||
|
|
@ -22,7 +21,8 @@ const {
|
|||
const Getters = require('./getters')
|
||||
const Key = require('./key')
|
||||
const Utils = require('./utils')
|
||||
const { writeCoordinate } = require('../../coordinates')
|
||||
const SchemaManager = require('../../schema')
|
||||
const LNDHealthMananger = require('../../../utils/lightningServices/errors')
|
||||
|
||||
/**
|
||||
* @typedef {import('./SimpleGUN').GUNNode} GUNNode
|
||||
|
|
@ -260,7 +260,7 @@ const acceptRequest = async (
|
|||
newlyCreatedOutgoingFeedID,
|
||||
ourSecret
|
||||
)
|
||||
|
||||
//why await if you dont need the response?
|
||||
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
gun
|
||||
.get(Key.HANDSHAKE_NODES)
|
||||
|
|
@ -358,7 +358,7 @@ const generateHandshakeAddress = async () => {
|
|||
}
|
||||
})
|
||||
}))
|
||||
|
||||
//why await if you dont need the response?
|
||||
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
gun
|
||||
.get(Key.HANDSHAKE_NODES)
|
||||
|
|
@ -643,7 +643,7 @@ const sendHandshakeRequest = async (recipientPublicKey, gun, user, SEA) => {
|
|||
handshakeAddress: await SEA.encrypt(currentHandshakeAddress, mySecret),
|
||||
timestamp
|
||||
}
|
||||
|
||||
//why await if you dont need the response?
|
||||
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
//@ts-ignore
|
||||
user.get(Key.STORED_REQS).set(storedReq, ack => {
|
||||
|
|
@ -923,10 +923,13 @@ const sendHRWithInitialMsg = async (
|
|||
/**
|
||||
* @typedef {object} SpontPaymentOptions
|
||||
* @prop {Common.Schema.OrderTargetType} type
|
||||
* @prop {string=} postID
|
||||
* @prop {string=} ackInfo
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} OrderRes
|
||||
* @prop {PaymentV2} payment
|
||||
* @prop {object=} orderAck
|
||||
*/
|
||||
/**
|
||||
* Returns the preimage corresponding to the payment.
|
||||
* @param {string} to
|
||||
|
|
@ -936,7 +939,7 @@ const sendHRWithInitialMsg = async (
|
|||
* @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.
|
||||
* @returns {Promise<OrderRes>} The payment's preimage.
|
||||
*/
|
||||
const sendSpontaneousPayment = async (
|
||||
to,
|
||||
|
|
@ -965,11 +968,8 @@ const sendSpontaneousPayment = async (
|
|||
from: getUser()._.sea.pub,
|
||||
memo: memo || 'no memo',
|
||||
timestamp: Date.now(),
|
||||
targetType: opts.type
|
||||
}
|
||||
|
||||
if (opts.type === 'tip') {
|
||||
order.ackInfo = opts.postID
|
||||
targetType: opts.type,
|
||||
ackInfo: opts.ackInfo
|
||||
}
|
||||
|
||||
logger.info(JSON.stringify(order))
|
||||
|
|
@ -1006,7 +1006,7 @@ const sendSpontaneousPayment = async (
|
|||
)
|
||||
)
|
||||
} else {
|
||||
res(ord._.get)
|
||||
setTimeout(() => res(ord._.get), 0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -1017,7 +1017,8 @@ const sendSpontaneousPayment = async (
|
|||
)}`
|
||||
throw new Error(msg)
|
||||
}
|
||||
|
||||
console.log('ORDER ID')
|
||||
console.log(orderID)
|
||||
/** @type {import('shock-common').Schema.OrderResponse} */
|
||||
const encryptedOrderRes = await Utils.tryAndWait(
|
||||
gun =>
|
||||
|
|
@ -1027,12 +1028,13 @@ const sendSpontaneousPayment = async (
|
|||
.get(Key.ORDER_TO_RESPONSE)
|
||||
.get(orderID)
|
||||
.on(orderResponse => {
|
||||
console.log(orderResponse)
|
||||
if (Schema.isOrderResponse(orderResponse)) {
|
||||
res(orderResponse)
|
||||
}
|
||||
})
|
||||
}),
|
||||
v => !Schema.isOrderResponse(v)
|
||||
v => Schema.isOrderResponse(v)
|
||||
)
|
||||
|
||||
if (!Schema.isOrderResponse(encryptedOrderRes)) {
|
||||
|
|
@ -1043,10 +1045,12 @@ const sendSpontaneousPayment = async (
|
|||
throw e
|
||||
}
|
||||
|
||||
/** @type {import('shock-common').Schema.OrderResponse} */
|
||||
/** @type {import('shock-common').Schema.OrderResponse &{ackNode:string}} */
|
||||
const orderResponse = {
|
||||
response: await SEA.decrypt(encryptedOrderRes.response, ourSecret),
|
||||
type: encryptedOrderRes.type
|
||||
type: encryptedOrderRes.type,
|
||||
//@ts-expect-error
|
||||
ackNode: encryptedOrderRes.ackNode
|
||||
}
|
||||
|
||||
logger.info('decoded orderResponse: ' + JSON.stringify(orderResponse))
|
||||
|
|
@ -1076,35 +1080,87 @@ const sendSpontaneousPayment = async (
|
|||
feeLimit,
|
||||
payment_request: orderResponse.response
|
||||
})
|
||||
const myLndPub = LNDHealthMananger.lndPub
|
||||
if (
|
||||
(opts.type !== 'contentReveal' && opts.type !== 'torrentSeed') ||
|
||||
!orderResponse.ackNode
|
||||
) {
|
||||
SchemaManager.AddOrder({
|
||||
type: opts.type,
|
||||
amount: parseInt(payment.value_sat, 10),
|
||||
coordinateHash: payment.payment_hash,
|
||||
coordinateIndex: parseInt(payment.payment_index, 10),
|
||||
fromLndPub: myLndPub || undefined,
|
||||
inbound: false,
|
||||
fromGunPub: getUser()._.sea.pub,
|
||||
toGunPub: to,
|
||||
invoiceMemo: memo
|
||||
})
|
||||
return { payment }
|
||||
}
|
||||
console.log('ACK NODE')
|
||||
console.log(orderResponse.ackNode)
|
||||
/** @type {import('shock-common').Schema.OrderResponse} */
|
||||
const encryptedOrderAckRes = await Utils.tryAndWait(
|
||||
gun =>
|
||||
new Promise(res => {
|
||||
gun
|
||||
.user(to)
|
||||
.get(Key.ORDER_TO_RESPONSE)
|
||||
.get(orderResponse.ackNode)
|
||||
.on(orderResponse => {
|
||||
console.log(orderResponse)
|
||||
console.log(Schema.isOrderResponse(orderResponse))
|
||||
|
||||
await writeCoordinate(payment.payment_hash, {
|
||||
id: payment.payment_hash,
|
||||
type: (() => {
|
||||
if (opts.type === 'tip') {
|
||||
return 'tip'
|
||||
} else if (opts.type === 'spontaneousPayment') {
|
||||
return 'spontaneousPayment'
|
||||
} else if (opts.type === 'contentReveal') {
|
||||
return 'other' // TODO
|
||||
} else if (opts.type === 'other') {
|
||||
return 'other' // TODO
|
||||
} else if (opts.type === 'torrentSeed') {
|
||||
return 'other' // TODO
|
||||
}
|
||||
// ensures we handle all possible types
|
||||
/** @type {never} */
|
||||
const assertNever = opts.type
|
||||
//@ts-expect-error
|
||||
if (orderResponse && orderResponse.type === 'orderAck') {
|
||||
//@ts-expect-error
|
||||
res(orderResponse)
|
||||
}
|
||||
})
|
||||
}),
|
||||
//@ts-expect-error
|
||||
v => !v || !v.type
|
||||
)
|
||||
|
||||
return assertNever && opts.type // please TS
|
||||
})(),
|
||||
amount: Number(payment.value_sat),
|
||||
if (!encryptedOrderAckRes || !encryptedOrderAckRes.type) {
|
||||
const e = TypeError(
|
||||
`Expected encryptedOrderAckRes got: ${typeof encryptedOrderAckRes}`
|
||||
)
|
||||
logger.error(e)
|
||||
throw e
|
||||
}
|
||||
|
||||
/** @type {import('shock-common').Schema.OrderResponse} */
|
||||
const orderAck = {
|
||||
response: await SEA.decrypt(encryptedOrderAckRes.response, ourSecret),
|
||||
type: encryptedOrderAckRes.type
|
||||
}
|
||||
|
||||
logger.info('decoded encryptedOrderAck: ' + JSON.stringify(orderAck))
|
||||
|
||||
if (orderAck.type === 'err') {
|
||||
throw new Error(orderAck.response)
|
||||
}
|
||||
|
||||
if (orderAck.type !== 'orderAck') {
|
||||
throw new Error(`expected orderAck response, got: ${orderAck.type}`)
|
||||
}
|
||||
SchemaManager.AddOrder({
|
||||
type: opts.type,
|
||||
amount: parseInt(payment.value_sat, 10),
|
||||
coordinateHash: payment.payment_hash,
|
||||
coordinateIndex: parseInt(payment.payment_index, 10),
|
||||
fromLndPub: myLndPub || undefined,
|
||||
inbound: false,
|
||||
timestamp: Date.now(),
|
||||
toLndPub: await myLNDPub()
|
||||
fromGunPub: getUser()._.sea.pub,
|
||||
toGunPub: to,
|
||||
invoiceMemo: memo,
|
||||
metadata: JSON.stringify(orderAck)
|
||||
})
|
||||
|
||||
return payment
|
||||
return { payment, orderAck }
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
logger.error('Error inside sendPayment()')
|
||||
logger.error(e)
|
||||
throw e
|
||||
|
|
@ -1122,8 +1178,8 @@ const sendSpontaneousPayment = async (
|
|||
* @returns {Promise<string>} The payment's preimage.
|
||||
*/
|
||||
const sendPayment = async (to, amount, memo, feeLimit) => {
|
||||
const payment = await sendSpontaneousPayment(to, amount, memo, feeLimit)
|
||||
return payment.payment_preimage
|
||||
const res = await sendSpontaneousPayment(to, amount, memo, feeLimit)
|
||||
return res.payment.payment_preimage
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1307,12 +1363,6 @@ const createPostNew = async (tags, title, content) => {
|
|||
contentItems: {}
|
||||
}
|
||||
|
||||
content.forEach(c => {
|
||||
// @ts-expect-error
|
||||
const uuid = Gun.text.random()
|
||||
newPost.contentItems[uuid] = c
|
||||
})
|
||||
|
||||
const mySecret = require('../Mediator').getMySecret()
|
||||
|
||||
await Common.Utils.asyncForEach(content, async c => {
|
||||
|
|
@ -1543,7 +1593,7 @@ const follow = async (publicKey, isPrivate) => {
|
|||
status: 'ok',
|
||||
user: publicKey
|
||||
}
|
||||
|
||||
//why await if you dont need the response?
|
||||
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
require('../Mediator')
|
||||
.getUser()
|
||||
|
|
|
|||
|
|
@ -7,20 +7,14 @@ const isFinite = require('lodash/isFinite')
|
|||
const isNumber = require('lodash/isNumber')
|
||||
const isNaN = require('lodash/isNaN')
|
||||
const Common = require('shock-common')
|
||||
const crypto = require('crypto')
|
||||
const fetch = require('node-fetch')
|
||||
const {
|
||||
Constants: { ErrorCode },
|
||||
Schema
|
||||
} = Common
|
||||
const { assertNever } = require('assert-never')
|
||||
const crypto = require('crypto')
|
||||
const fetch = require('node-fetch')
|
||||
|
||||
const SchemaManager = require('../../../schema')
|
||||
const LightningServices = require('../../../../utils/lightningServices')
|
||||
const {
|
||||
addInvoice,
|
||||
myLNDPub
|
||||
} = require('../../../../utils/lightningServices/v2')
|
||||
const { writeCoordinate } = require('../../../coordinates')
|
||||
const Key = require('../key')
|
||||
const Utils = require('../utils')
|
||||
const { gunUUID } = require('../../../../utils')
|
||||
|
|
@ -63,6 +57,28 @@ const ordersProcessed = new Set()
|
|||
|
||||
let currentOrderAddr = ''
|
||||
|
||||
/**
|
||||
* @param {InvoiceRequest} invoiceReq
|
||||
* @returns {Promise<InvoiceResponse>}
|
||||
*/
|
||||
const _addInvoice = invoiceReq =>
|
||||
new Promise((resolve, rej) => {
|
||||
const {
|
||||
services: { lightning }
|
||||
} = LightningServices
|
||||
|
||||
lightning.addInvoice(invoiceReq, (
|
||||
/** @type {any} */ error,
|
||||
/** @type {InvoiceResponse} */ response
|
||||
) => {
|
||||
if (error) {
|
||||
rej(error)
|
||||
} else {
|
||||
resolve(response)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {string} addr
|
||||
* @param {ISEA} SEA
|
||||
|
|
@ -89,6 +105,8 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
return
|
||||
}
|
||||
|
||||
//const listenerStartTime = performance.now()
|
||||
|
||||
ordersProcessed.add(orderID)
|
||||
|
||||
logger.info(
|
||||
|
|
@ -146,12 +164,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
`onOrders() -> Will now create an invoice : ${JSON.stringify(invoiceReq)}`
|
||||
)
|
||||
|
||||
const invoice = await addInvoice(
|
||||
invoiceReq.value,
|
||||
invoiceReq.memo,
|
||||
true,
|
||||
invoiceReq.expiry
|
||||
)
|
||||
const invoice = await _addInvoice(invoiceReq)
|
||||
|
||||
logger.info(
|
||||
'onOrders() -> Successfully created the invoice, will now encrypt it'
|
||||
|
|
@ -162,11 +175,13 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
logger.info(
|
||||
`onOrders() -> Will now place the encrypted invoice in order to response usergraph: ${addr}`
|
||||
)
|
||||
const ackNode = gunUUID()
|
||||
|
||||
/** @type {import('shock-common').Schema.OrderResponse} */
|
||||
const orderResponse = {
|
||||
response: encInvoice,
|
||||
type: 'invoice'
|
||||
type: 'invoice',
|
||||
ackNode
|
||||
}
|
||||
|
||||
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
|
|
@ -187,86 +202,74 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
})
|
||||
}))
|
||||
|
||||
// invoices should be settled right away so we can rely on this single
|
||||
// subscription instead of life-long all invoices subscription
|
||||
if (order.targetType === 'tip') {
|
||||
const { ackInfo } = order
|
||||
if (!Common.isPopulatedString(ackInfo)) {
|
||||
throw new TypeError(`ackInfo(postID) not a a populated string`)
|
||||
}
|
||||
}
|
||||
|
||||
// A post tip order lifecycle is short enough that we can do it like this.
|
||||
const stream = LightningServices.invoices.subscribeSingleInvoice({
|
||||
r_hash: invoice.r_hash
|
||||
})
|
||||
|
||||
/** @type {Common.Coordinate} */
|
||||
const coord = {
|
||||
amount,
|
||||
id: invoice.r_hash.toString(),
|
||||
inbound: true,
|
||||
timestamp: Date.now(),
|
||||
type: 'invoice',
|
||||
invoiceMemo: memo,
|
||||
fromGunPub: order.from,
|
||||
toGunPub: getUser()._.sea.pub,
|
||||
toLndPub: await myLNDPub()
|
||||
}
|
||||
|
||||
if (order.targetType === 'tip') {
|
||||
coord.type = 'tip'
|
||||
} else {
|
||||
coord.type = 'spontaneousPayment'
|
||||
}
|
||||
|
||||
//logger.info(`[PERF] Added invoice to GunDB in ${invoicePutEndTime}ms`)
|
||||
/**
|
||||
* @param {Common.InvoiceWhenListed} invoice
|
||||
*
|
||||
* @param {Common.Schema.InvoiceWhenListed & {r_hash:Buffer,payment_addr:string}} paidInvoice
|
||||
*/
|
||||
const onData = async invoice => {
|
||||
if (invoice.settled) {
|
||||
writeCoordinate(invoice.r_hash.toString(), coord)
|
||||
|
||||
if (order.targetType === 'tip') {
|
||||
const invoicePaidCb = async paidInvoice => {
|
||||
console.log('INVOICE PAID')
|
||||
let breakError = null
|
||||
let orderMetadata //eslint-disable-line init-declarations
|
||||
const hashString = paidInvoice.r_hash.toString('hex')
|
||||
const {
|
||||
amt_paid_sat: amt,
|
||||
add_index: addIndex,
|
||||
payment_addr: paymentAddr
|
||||
} = paidInvoice
|
||||
const orderType = order.targetType
|
||||
const { ackInfo } = order //a string representing what has been requested
|
||||
switch (orderType) {
|
||||
case 'tip': {
|
||||
const postID = ackInfo
|
||||
if (!Common.isPopulatedString(postID)) {
|
||||
breakError = 'invalid ackInfo provided for postID'
|
||||
break //create the coordinate, but stop because of the invalid id
|
||||
}
|
||||
getUser()
|
||||
.get('postToTipCount')
|
||||
// CAST: Checked above.
|
||||
.get(/** @type {string} */ (order.ackInfo))
|
||||
.get(postID)
|
||||
.set(null) // each item in the set is a tip
|
||||
} else if (order.targetType === 'contentReveal') {
|
||||
// -----------------------------------------
|
||||
logger.debug('Content Reveal')
|
||||
|
||||
break
|
||||
}
|
||||
case 'spontaneousPayment': {
|
||||
//no action required
|
||||
break
|
||||
}
|
||||
case 'contentReveal': {
|
||||
console.log('cONTENT REVEAL')
|
||||
//assuming digital product that only requires to be unlocked
|
||||
const postID = order.ackInfo
|
||||
|
||||
const postID = ackInfo
|
||||
console.log('ACK INFO')
|
||||
console.log(ackInfo)
|
||||
if (!Common.isPopulatedString(postID)) {
|
||||
logger.error(`Invalid post ID`)
|
||||
logger.error(postID)
|
||||
return
|
||||
breakError = 'invalid ackInfo provided for postID'
|
||||
break //create the coordinate, but stop because of the invalid id
|
||||
}
|
||||
|
||||
// TODO: do this reactively
|
||||
console.log('IS STRING')
|
||||
const selectedPost = await new Promise(res => {
|
||||
getUser()
|
||||
.get(Key.POSTS_NEW)
|
||||
.get(postID)
|
||||
.load(res)
|
||||
})
|
||||
|
||||
logger.debug(selectedPost)
|
||||
|
||||
if (Common.isPost(selectedPost)) {
|
||||
logger.error('Post id provided does not correspond to a valid post')
|
||||
return
|
||||
console.log('LOAD ok')
|
||||
console.log(selectedPost)
|
||||
if (
|
||||
!selectedPost ||
|
||||
!selectedPost.status ||
|
||||
selectedPost.status !== 'publish'
|
||||
) {
|
||||
breakError = 'ackInfo provided does not correspond to a valid post'
|
||||
break //create the coordinate, but stop because of the invalid post
|
||||
}
|
||||
|
||||
console.log('IS POST')
|
||||
/**
|
||||
* @type {Record<string,string>} <contentID,decryptedRef>
|
||||
*/
|
||||
const contentsToSend = {}
|
||||
const mySecret = require('../../Mediator').getMySecret()
|
||||
logger.debug('SECRET OK')
|
||||
console.log('SECRET OK')
|
||||
let privateFound = false
|
||||
await Common.Utils.asyncForEach(
|
||||
Object.entries(selectedPost.contentItems),
|
||||
|
|
@ -286,8 +289,9 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
}
|
||||
)
|
||||
if (!privateFound) {
|
||||
logger.error(`Post provided does not contain private content`)
|
||||
return
|
||||
breakError =
|
||||
'post provided from ackInfo does not contain private content'
|
||||
break //no private content in this post
|
||||
}
|
||||
const ackData = { unlockedContents: contentsToSend }
|
||||
const toSend = JSON.stringify(ackData)
|
||||
|
|
@ -296,15 +300,12 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
type: 'orderAck',
|
||||
response: encrypted
|
||||
}
|
||||
logger.debug('RES READY')
|
||||
console.log('RES READY')
|
||||
|
||||
const uuid = gunUUID()
|
||||
orderResponse.ackNode = uuid
|
||||
|
||||
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
await new Promise((res, rej) => {
|
||||
getUser()
|
||||
.get(Key.ORDER_TO_RESPONSE)
|
||||
.get(uuid)
|
||||
.get(ackNode)
|
||||
.put(ordResponse, ack => {
|
||||
if (ack.err && typeof ack.err !== 'number') {
|
||||
rej(
|
||||
|
|
@ -313,29 +314,28 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
)
|
||||
)
|
||||
} else {
|
||||
res()
|
||||
res(null)
|
||||
}
|
||||
})
|
||||
}))
|
||||
logger.debug('RES SENT CONTENT')
|
||||
|
||||
// ----------------------------------------------------------------------------------
|
||||
} else if (order.targetType === 'spontaneousPayment') {
|
||||
// no action required
|
||||
} else if (order.targetType === 'torrentSeed') {
|
||||
logger.debug('TORRENT')
|
||||
const numberOfTokens = Number(order.ackInfo)
|
||||
})
|
||||
console.log('RES SENT CONTENT')
|
||||
orderMetadata = JSON.stringify(ordResponse)
|
||||
break
|
||||
}
|
||||
case 'torrentSeed': {
|
||||
console.log('TORRENT')
|
||||
const numberOfTokens = Number(ackInfo)
|
||||
if (isNaN(numberOfTokens)) {
|
||||
logger.error('ackInfo provided is not a valid number')
|
||||
return
|
||||
breakError = 'ackInfo provided is not a valid number'
|
||||
break
|
||||
}
|
||||
const seedUrl = process.env.TORRENT_SEED_URL
|
||||
const seedToken = process.env.TORRENT_SEED_TOKEN
|
||||
if (!seedUrl || !seedToken) {
|
||||
logger.error('torrentSeed service not available')
|
||||
return
|
||||
breakError = 'torrentSeed service not available'
|
||||
break //service not available
|
||||
}
|
||||
logger.debug('SEED URL OK')
|
||||
console.log('SEED URL OK')
|
||||
const tokens = Array(numberOfTokens)
|
||||
for (let i = 0; i < numberOfTokens; i++) {
|
||||
tokens[i] = crypto.randomBytes(32).toString('hex')
|
||||
|
|
@ -346,7 +346,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
seed_token: seedToken,
|
||||
wallet_token: token
|
||||
}
|
||||
// @ts-expect-error TODO
|
||||
//@ts-expect-error
|
||||
const res = await fetch(`${seedUrl}/api/enroll_token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
@ -359,7 +359,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
}
|
||||
}
|
||||
await Promise.all(tokens.map(enrollToken))
|
||||
logger.debug('RES SEED OK')
|
||||
console.log('RES SEED OK')
|
||||
const ackData = { seedUrl, tokens }
|
||||
const toSend = JSON.stringify(ackData)
|
||||
const encrypted = await SEA.encrypt(toSend, secret)
|
||||
|
|
@ -368,14 +368,10 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
response: encrypted
|
||||
}
|
||||
console.log('RES SEED SENT')
|
||||
|
||||
const uuid = gunUUID()
|
||||
orderResponse.ackNode = uuid
|
||||
|
||||
await /** @type {Promise<void>} */ (new Promise((res, rej) => {
|
||||
await new Promise((res, rej) => {
|
||||
getUser()
|
||||
.get(Key.ORDER_TO_RESPONSE)
|
||||
.get(uuid)
|
||||
.get(ackNode)
|
||||
.put(serviceResponse, ack => {
|
||||
if (ack.err && typeof ack.err !== 'number') {
|
||||
rej(
|
||||
|
|
@ -384,32 +380,68 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
)
|
||||
)
|
||||
} else {
|
||||
res()
|
||||
res(null)
|
||||
}
|
||||
})
|
||||
}))
|
||||
logger.debug('RES SENT SEED')
|
||||
} else if (order.targetType === 'other') {
|
||||
// TODO
|
||||
} else {
|
||||
assertNever(order.targetType)
|
||||
})
|
||||
console.log('RES SENT SEED')
|
||||
orderMetadata = JSON.stringify(serviceResponse)
|
||||
break
|
||||
}
|
||||
case 'other': //not implemented yet but save them as a coordinate anyways
|
||||
break
|
||||
default:
|
||||
return //exit because not implemented
|
||||
}
|
||||
const metadata = breakError ? JSON.stringify(breakError) : orderMetadata
|
||||
const myGunPub = getUser()._.sea.pub
|
||||
SchemaManager.AddOrder({
|
||||
type: orderType,
|
||||
coordinateHash: hashString,
|
||||
coordinateIndex: parseInt(addIndex, 10),
|
||||
inbound: true,
|
||||
amount: parseInt(amt, 10),
|
||||
|
||||
stream.off()
|
||||
toLndPub: paymentAddr,
|
||||
fromGunPub: order.from,
|
||||
toGunPub: myGunPub,
|
||||
invoiceMemo: memo,
|
||||
|
||||
metadata
|
||||
})
|
||||
if (breakError) {
|
||||
throw new Error(breakError)
|
||||
}
|
||||
}
|
||||
console.log('WAITING INVOICE TO BE PAID')
|
||||
new Promise(res => SchemaManager.addListenInvoice(invoice.r_hash, res))
|
||||
.then(invoicePaidCb)
|
||||
.catch(err => {
|
||||
logger.error(
|
||||
`error inside onOrders, orderAddr: ${addr}, orderID: ${orderID}, order: ${JSON.stringify(
|
||||
order
|
||||
)}`
|
||||
)
|
||||
logger.error(err)
|
||||
|
||||
stream.on('data', onData)
|
||||
/** @type {import('shock-common').Schema.OrderResponse} */
|
||||
const orderResponse = {
|
||||
response: err.message,
|
||||
type: 'err'
|
||||
}
|
||||
|
||||
stream.on('status', (/** @type {any} */ status) => {
|
||||
logger.info(`Post tip, post: ${order.ackInfo}, invoice status:`, status)
|
||||
})
|
||||
stream.on('end', () => {
|
||||
logger.warn(`Post tip, post: ${order.ackInfo}, invoice stream ended`)
|
||||
})
|
||||
stream.on('error', (/** @type {any} */ e) => {
|
||||
logger.warn(`Post tip, post: ${order.ackInfo}, error:`, e)
|
||||
})
|
||||
getUser()
|
||||
.get(Key.ORDER_TO_RESPONSE)
|
||||
.get(orderID)
|
||||
// @ts-expect-error
|
||||
.put(orderResponse, ack => {
|
||||
if (ack.err && typeof ack.err !== 'number') {
|
||||
logger.error(
|
||||
`Error saving encrypted invoice to order to response usergraph: ${ack}`
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`error inside onOrders, orderAddr: ${addr}, orderID: ${orderID}, order: ${JSON.stringify(
|
||||
|
|
|
|||
|
|
@ -63,4 +63,11 @@ exports.PROFILE_BINARY = 'profileBinary'
|
|||
|
||||
exports.POSTS_NEW = 'posts'
|
||||
|
||||
// For Coordinates
|
||||
exports.COORDINATES = 'coordinates'
|
||||
|
||||
exports.COORDINATE_INDEX = 'coordinateIndex'
|
||||
|
||||
exports.TMP_CHAIN_COORDINATE = 'tmpChainCoordinate'
|
||||
|
||||
exports.DATE_COORDINATE_INDEX = 'dateCoordinateIndex'
|
||||
|
|
|
|||
601
services/schema/index.js
Normal file
601
services/schema/index.js
Normal file
|
|
@ -0,0 +1,601 @@
|
|||
const Crypto = require('crypto')
|
||||
const logger = require('winston')
|
||||
const Common = require('shock-common')
|
||||
const getGunUser = () => require('../gunDB/Mediator').getUser()
|
||||
const isAuthenticated = () => require('../gunDB/Mediator').isAuthenticated()
|
||||
const Key = require('../gunDB/contact-api/key')
|
||||
const lndV2 = require('../../utils/lightningServices/v2')
|
||||
/**
|
||||
* @typedef {import('../gunDB/contact-api/SimpleGUN').ISEA} ISEA
|
||||
* @typedef { 'spontaneousPayment' | 'tip' | 'torrentSeed' | 'contentReveal' | 'other'|'invoice'|'payment'|'chainTx' } OrderType
|
||||
*
|
||||
* This represents a settled order only, unsettled orders have no coordinate
|
||||
* @typedef {object} CoordinateOrder //everything is optional for different types
|
||||
* @prop {string=} fromLndPub can be unknown when inbound
|
||||
* @prop {string=} toLndPub always known
|
||||
* @prop {string=} fromGunPub can be optional, if the payment/invoice is not related to an order
|
||||
* @prop {string=} toGunPub can be optional, if the payment/invoice is not related to an order
|
||||
* @prop {string=} fromBtcPub
|
||||
* @prop {string=} toBtcPub
|
||||
* @prop {boolean} inbound
|
||||
* NOTE: type specific checks are not made before creating the order node, filters must be done before rendering or processing
|
||||
*
|
||||
* @prop {string=} ownerGunPub Reserved for buddy system:
|
||||
* can be undefined, '', 'me', or node owner pub key to represent node owner,
|
||||
* otherwise it represents a buddy
|
||||
*
|
||||
* @prop {number} coordinateIndex can be payment_index, or add_index depending on if it's a payment or an invoice
|
||||
* @prop {string} coordinateHash can be payment_hash, or r_hash depending on if it's a payment or an invoice,
|
||||
* if it's a r_hash, must be hex encoded
|
||||
*
|
||||
* @prop {OrderType} type
|
||||
* @prop {number} amount
|
||||
* @prop {string=} description
|
||||
* @prop {string=} invoiceMemo
|
||||
* @prop {string=} metadata JSON encoded string to store extra data for special use cases
|
||||
* @prop {number=} timestamp timestamp will be added at processing time if empty
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {CoordinateOrder} order
|
||||
*/
|
||||
const checkOrderInfo = order => {
|
||||
const {
|
||||
fromLndPub,
|
||||
toLndPub,
|
||||
fromGunPub,
|
||||
toGunPub,
|
||||
fromBtcPub,
|
||||
toBtcPub,
|
||||
inbound,
|
||||
type,
|
||||
amount,
|
||||
description,
|
||||
coordinateIndex,
|
||||
coordinateHash,
|
||||
metadata,
|
||||
invoiceMemo
|
||||
} = order
|
||||
|
||||
if (fromLndPub && (typeof fromLndPub !== 'string' || fromLndPub === '')) {
|
||||
return 'invalid "fromLndPub" field provided to order coordinate'
|
||||
}
|
||||
if (toLndPub && (typeof toLndPub !== 'string' || toLndPub === '')) {
|
||||
return 'invalid or no "toLndPub" field provided to order coordinate'
|
||||
}
|
||||
if (fromGunPub && (typeof fromGunPub !== 'string' || fromGunPub === '')) {
|
||||
return 'invalid "fromGunPub" field provided to order coordinate'
|
||||
}
|
||||
if (toGunPub && (typeof toGunPub !== 'string' || toGunPub === '')) {
|
||||
return 'invalid "toGunPub" field provided to order coordinate'
|
||||
}
|
||||
if (fromBtcPub && (typeof fromBtcPub !== 'string' || fromBtcPub === '')) {
|
||||
return 'invalid "fromBtcPub" field provided to order coordinate'
|
||||
}
|
||||
if (toBtcPub && (typeof toBtcPub !== 'string' || toBtcPub === '')) {
|
||||
return 'invalid "toBtcPub" field provided to order coordinate'
|
||||
}
|
||||
if (typeof inbound !== 'boolean') {
|
||||
return 'invalid or no "inbound" field provided to order coordinate'
|
||||
}
|
||||
//@ts-expect-error
|
||||
if (typeof type !== 'string' || type === '') {
|
||||
return 'invalid or no "type" field provided to order coordinate'
|
||||
}
|
||||
if (typeof amount !== 'number') {
|
||||
return 'invalid or no "amount" field provided to order coordinate'
|
||||
}
|
||||
|
||||
if (typeof coordinateIndex !== 'number') {
|
||||
return 'invalid or no "coordinateIndex" field provided to order coordinate'
|
||||
}
|
||||
if (typeof coordinateHash !== 'string' || coordinateHash === '') {
|
||||
return 'invalid or no "coordinateHash" field provided to order coordinate'
|
||||
}
|
||||
|
||||
if (description && (typeof description !== 'string' || description === '')) {
|
||||
return 'invalid "description" field provided to order coordinate'
|
||||
}
|
||||
if (invoiceMemo && (typeof invoiceMemo !== 'string' || invoiceMemo === '')) {
|
||||
return 'invalid "invoiceMemo" field provided to order coordinate'
|
||||
}
|
||||
if (metadata && (typeof metadata !== 'string' || metadata === '')) {
|
||||
return 'invalid "metadata" field provided to order coordinate'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
* @param {CoordinateOrder} orderInfo
|
||||
* @param {string} coordinateSHA256
|
||||
*//*
|
||||
const dateIndexCreateCb = (orderInfo, coordinateSHA256) => {
|
||||
//if (this.memIndex) { need bind to use this here
|
||||
//update date memIndex
|
||||
//}
|
||||
const date = new Date(orderInfo.timestamp || 0)
|
||||
//use UTC for consistency?
|
||||
const year = date.getUTCFullYear().toString()
|
||||
const month = date.getUTCMonth().toString()
|
||||
|
||||
getGunUser()
|
||||
.get(Key.DATE_COORDINATE_INDEX)
|
||||
.get(year)
|
||||
.get(month)
|
||||
.set(coordinateSHA256)
|
||||
}*/
|
||||
|
||||
/*
|
||||
* if not provided, assume current month and year
|
||||
* @param {number|null} year
|
||||
* @param {number|null} month
|
||||
*//*
|
||||
const getMonthCoordinates = async (year = null, month = null) => {
|
||||
const now = Date.now()
|
||||
const stringYear = year !== null ? year.toString() : now.getUTCFullYear().toString()
|
||||
const stringMonth = month !== null ? month.toString() : now.getUTCMonth().toString()
|
||||
|
||||
const data = await new Promise(res => {
|
||||
getGunUser()
|
||||
.get(Key.DATE_COORDINATE_INDEX)
|
||||
.get(stringYear)
|
||||
.get(stringMonth)
|
||||
.load(res)
|
||||
})
|
||||
const coordinatesArray = Object
|
||||
.values(data)
|
||||
.filter(coordinateSHA256 => typeof coordinateSHA256 === 'string')
|
||||
|
||||
return coordinatesArray
|
||||
}*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string|undefined} address
|
||||
* @param {CoordinateOrder} orderInfo
|
||||
*/
|
||||
const AddTmpChainOrder = async (address, orderInfo) => {
|
||||
if (!address) {
|
||||
throw new Error("invalid address passed to AddTmpChainOrder")
|
||||
}
|
||||
if (!orderInfo.toBtcPub) {
|
||||
throw new Error("invalid toBtcPub passed to AddTmpChainOrder")
|
||||
}
|
||||
const checkErr = checkOrderInfo(orderInfo)
|
||||
if (checkErr) {
|
||||
throw new Error(checkErr)
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {CoordinateOrder}
|
||||
*/
|
||||
const filteredOrder = {
|
||||
fromLndPub: orderInfo.fromLndPub,
|
||||
toLndPub: orderInfo.toLndPub,
|
||||
fromGunPub: orderInfo.fromGunPub,
|
||||
toGunPub: orderInfo.toGunPub,
|
||||
inbound: orderInfo.inbound,
|
||||
ownerGunPub: orderInfo.ownerGunPub,
|
||||
coordinateIndex: orderInfo.coordinateIndex,
|
||||
coordinateHash: orderInfo.coordinateHash,
|
||||
type: orderInfo.type,
|
||||
amount: orderInfo.amount,
|
||||
description: orderInfo.description,
|
||||
metadata: orderInfo.metadata,
|
||||
|
||||
timestamp: orderInfo.timestamp || Date.now(),
|
||||
}
|
||||
const orderString = JSON.stringify(filteredOrder)
|
||||
const mySecret = require('../gunDB/Mediator').getMySecret()
|
||||
const SEA = require('../gunDB/Mediator').mySEA
|
||||
const encryptedOrderString = await SEA.encrypt(orderString, mySecret)
|
||||
|
||||
const addressSHA256 = Crypto.createHash('SHA256')
|
||||
.update(address)
|
||||
.digest('hex')
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
getGunUser()
|
||||
.get(Key.TMP_CHAIN_COORDINATE)
|
||||
.get(addressSHA256)
|
||||
.put(encryptedOrderString, ack => {
|
||||
if (ack.err && typeof ack.err !== 'number') {
|
||||
rej(
|
||||
new Error(
|
||||
`Error saving tmp chain coordinate order to user-graph: ${ack}`
|
||||
)
|
||||
)
|
||||
} else {
|
||||
res(null)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} address
|
||||
* @returns {Promise<false|CoordinateOrder>}
|
||||
*/
|
||||
const isTmpChainOrder = async (address) => {
|
||||
if (typeof address !== 'string' || address === '') {
|
||||
return false
|
||||
}
|
||||
const addressSHA256 = Crypto.createHash('SHA256')
|
||||
.update(address)
|
||||
.digest('hex')
|
||||
|
||||
const maybeData = await getGunUser()
|
||||
.get(Key.TMP_CHAIN_COORDINATE)
|
||||
.get(addressSHA256)
|
||||
.then()
|
||||
|
||||
if (typeof maybeData !== 'string' || maybeData === '') {
|
||||
return false
|
||||
}
|
||||
const mySecret = require('../gunDB/Mediator').getMySecret()
|
||||
const SEA = require('../gunDB/Mediator').mySEA
|
||||
const decryptedString = await SEA.decrypt(maybeData, mySecret)
|
||||
if (typeof decryptedString !== 'string' || decryptedString === '') {
|
||||
return false
|
||||
}
|
||||
|
||||
const tmpOrder = JSON.parse(decryptedString)
|
||||
const checkErr = checkOrderInfo(tmpOrder)
|
||||
if (checkErr) {
|
||||
return false
|
||||
}
|
||||
|
||||
return tmpOrder
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} address
|
||||
*/
|
||||
const clearTmpChainOrder = async (address) => {
|
||||
if (typeof address !== 'string' || address === '') {
|
||||
return
|
||||
}
|
||||
const addressSHA256 = Crypto.createHash('SHA256')
|
||||
.update(address)
|
||||
.digest('hex')
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
getGunUser()
|
||||
.get(Key.TMP_CHAIN_COORDINATE)
|
||||
.get(addressSHA256)
|
||||
.put(null, ack => {
|
||||
if (ack.err && typeof ack.err !== 'number') {
|
||||
rej(
|
||||
new Error(
|
||||
`Error nulling tmp chain coordinate order to user-graph: ${ack}`
|
||||
)
|
||||
)
|
||||
} else {
|
||||
res(null)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Common.Schema.ChainTransaction} tx
|
||||
* @param {CoordinateOrder|false| undefined} order
|
||||
*/
|
||||
const handleUnconfirmedTx = (tx, order) => {
|
||||
const { tx_hash } = tx
|
||||
const amountInt = parseInt(tx.amount, 10)
|
||||
if (order) {
|
||||
/*if an order already exists, update the order data
|
||||
if an unconfirmed transaction has a tmp order already
|
||||
it means the address was generated by shockAPI, or the tx was sent by shockAPI*/
|
||||
const orderUpdate = order
|
||||
orderUpdate.amount = Math.abs(amountInt)
|
||||
orderUpdate.inbound = amountInt > 0
|
||||
/*tmp coordinate does not have a coordinate hash until the transaction is created,
|
||||
before it will contain 'unknown' */
|
||||
orderUpdate.coordinateHash = tx_hash
|
||||
/*update the order data,
|
||||
provides a notification when the TX enters the mempool */
|
||||
AddTmpChainOrder(orderUpdate.toBtcPub, orderUpdate)
|
||||
} else {
|
||||
/*if an order does not exist, create the tmp order,
|
||||
and use tx_hash as key.
|
||||
this means the address was NOT generated by shockAPI, or the tx was NOT sent by shockAPI */
|
||||
AddTmpChainOrder(tx_hash, {
|
||||
type: 'chainTx',
|
||||
amount: Math.abs(amountInt),
|
||||
coordinateHash: tx_hash,
|
||||
coordinateIndex: 0, //coordinate index is 0 until the tx is confirmed and the block is known
|
||||
inbound: amountInt > 0,
|
||||
toBtcPub: 'unknown'
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SchemaManager {
|
||||
//constructor() {
|
||||
// this.orderCreateIndexCallbacks.push(dateIndexCreateCb) //create more Cbs and put them here for more indexes callbacks
|
||||
//}
|
||||
|
||||
//dateIndexName = 'dateIndex'
|
||||
/**
|
||||
* @type {((order : CoordinateOrder,coordinateSHA256 : string)=>void)[]}
|
||||
*/
|
||||
orderCreateIndexCallbacks = []
|
||||
|
||||
/**
|
||||
* @param {CoordinateOrder} orderInfo
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
async AddOrder(orderInfo) {
|
||||
const checkErr = checkOrderInfo(orderInfo)
|
||||
if (checkErr) {
|
||||
throw new Error(checkErr)
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {CoordinateOrder}
|
||||
*/
|
||||
const filteredOrder = {
|
||||
fromLndPub: orderInfo.fromLndPub,
|
||||
toLndPub: orderInfo.toLndPub,
|
||||
fromGunPub: orderInfo.fromGunPub,
|
||||
toGunPub: orderInfo.toGunPub,
|
||||
inbound: orderInfo.inbound,
|
||||
ownerGunPub: orderInfo.ownerGunPub,
|
||||
coordinateIndex: orderInfo.coordinateIndex,
|
||||
coordinateHash: orderInfo.coordinateHash,
|
||||
type: orderInfo.type,
|
||||
amount: orderInfo.amount,
|
||||
description: orderInfo.description,
|
||||
metadata: orderInfo.metadata,
|
||||
timestamp: orderInfo.timestamp || Date.now(),
|
||||
}
|
||||
const orderString = JSON.stringify(filteredOrder)
|
||||
const mySecret = require('../gunDB/Mediator').getMySecret()
|
||||
const SEA = require('../gunDB/Mediator').mySEA
|
||||
const encryptedOrderString = await SEA.encrypt(orderString, mySecret)
|
||||
const coordinatePub = filteredOrder.inbound ? filteredOrder.toLndPub : filteredOrder.fromLndPub
|
||||
const coordinate = `${coordinatePub}__${filteredOrder.coordinateIndex}__${filteredOrder.coordinateHash}`
|
||||
const coordinateSHA256 = Crypto.createHash('SHA256')
|
||||
.update(coordinate)
|
||||
.digest('hex')
|
||||
await new Promise((res, rej) => {
|
||||
getGunUser()
|
||||
.get(Key.COORDINATES)
|
||||
.get(coordinateSHA256)
|
||||
.put(encryptedOrderString, ack => {
|
||||
if (ack.err && typeof ack.err !== 'number') {
|
||||
console.log(ack)
|
||||
rej(
|
||||
new Error(
|
||||
`Error saving coordinate order to user-graph: ${ack}`
|
||||
)
|
||||
)
|
||||
} else {
|
||||
res(null)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
//update all indexes with
|
||||
//this.orderCreateIndexCallbacks.forEach(cb => cb(filteredOrder, coordinateSHA256))
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* if not provided, assume current month and year
|
||||
* @param {number|null} year
|
||||
* @param {number|null} month
|
||||
* @returns {Promise<CoordinateOrder[]>} from newer to older
|
||||
*//*
|
||||
async getMonthOrders(year = null, month = null) {
|
||||
const now = new Date()
|
||||
const intYear = year !== null ? year : now.getUTCFullYear()
|
||||
const intMonth = month !== null ? month : now.getUTCMonth()
|
||||
|
||||
let coordinates = null
|
||||
if (this.memIndex) {
|
||||
//get coordinates from this.memDateIndex
|
||||
} else {
|
||||
coordinates = await getMonthCoordinates(intYear, intMonth)
|
||||
}
|
||||
const orders = []
|
||||
if (!coordinates) {
|
||||
return orders
|
||||
}
|
||||
await Common.Utils.asyncForEach(coordinates, async coordinateSHA256 => {
|
||||
const encryptedOrderString = await getGunUser()
|
||||
.get(Key.COORDINATES)
|
||||
.get(coordinateSHA256)
|
||||
.then()
|
||||
if (typeof encryptedOrderString !== 'string') {
|
||||
return
|
||||
}
|
||||
const mySecret = require('../gunDB/Mediator').getMySecret()
|
||||
const SEA = require('../gunDB/Mediator').mySEA
|
||||
const decryptedString = await SEA.decrypt(encryptedOrderString, mySecret)
|
||||
const orderJSON = JSON.parse(decryptedString)
|
||||
orders.push(orderJSON)
|
||||
})
|
||||
const orderedOrders = orders.sort((a, b) => b.timestamp - a.timestamp)
|
||||
return orderedOrders
|
||||
}*/
|
||||
|
||||
/**
|
||||
* @typedef {Common.Schema.InvoiceWhenListed & {r_hash:Buffer,payment_addr:string}} Invoice
|
||||
*/
|
||||
/**
|
||||
* @type {Record<string,(invoice:Invoice) =>void>}
|
||||
*/
|
||||
_listeningInvoices = {}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Buffer} r_hash
|
||||
* @param {(invoice:Invoice) =>void} done
|
||||
*/
|
||||
addListenInvoice(r_hash, done) {
|
||||
const hashString = r_hash.toString("hex")
|
||||
this._listeningInvoices[hashString] = done
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Common.Schema.InvoiceWhenListed & {r_hash:Buffer,payment_addr:string}} data
|
||||
*/
|
||||
invoiceStreamDataCb(data) {
|
||||
if (!data.settled) {
|
||||
//invoice not paid yet
|
||||
return
|
||||
}
|
||||
const hashString = data.r_hash.toString('hex')
|
||||
const amt = parseInt(data.amt_paid_sat, 10)
|
||||
if (this._listeningInvoices[hashString]) {
|
||||
const done = this._listeningInvoices[hashString]
|
||||
delete this._listeningInvoices[hashString]
|
||||
done(data)
|
||||
} else {
|
||||
this.AddOrder({
|
||||
type: 'invoice',
|
||||
coordinateHash: hashString,
|
||||
coordinateIndex: parseInt(data.add_index, 10),
|
||||
inbound: true,
|
||||
amount: amt,
|
||||
toLndPub: data.payment_addr,
|
||||
invoiceMemo: data.memo
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @type {Record<string,boolean>}
|
||||
* lnd fires a confirmed transaction event TWICE, let's make sure it is only managed ONCE
|
||||
*/
|
||||
_confirmedTransactions = {}
|
||||
|
||||
/**
|
||||
* @param {Common.Schema.ChainTransaction} data
|
||||
*/
|
||||
async transactionStreamDataCb(data) {
|
||||
const { num_confirmations } = data
|
||||
const responses = await Promise.all(data.dest_addresses.map(isTmpChainOrder))
|
||||
const hasOrder = responses.find(res => res !== false)
|
||||
|
||||
if (num_confirmations === 0) {
|
||||
handleUnconfirmedTx(data, hasOrder)
|
||||
} else {
|
||||
this.handleConfirmedTx(data, hasOrder)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Common.Schema.ChainTransaction} tx
|
||||
* @param {CoordinateOrder|false| undefined} order
|
||||
*/
|
||||
handleConfirmedTx(tx, order) {
|
||||
const { tx_hash } = tx
|
||||
if (this._confirmedTransactions[tx_hash]) {
|
||||
//this tx confirmation was already handled
|
||||
return
|
||||
}
|
||||
if (!order) {
|
||||
/*confirmed transaction MUST have a tmp order,
|
||||
if not, means something gone wrong */
|
||||
logger.error('found a confirmed transaction that does not have a tmp order!!')
|
||||
return
|
||||
}
|
||||
if (!order.toBtcPub) {
|
||||
/*confirmed transaction tmp order MUST have a non null toBtcPub */
|
||||
logger.error('found a confirmed transaction that does not have toBtcPub in the order!!')
|
||||
return
|
||||
}
|
||||
const finalOrder = order
|
||||
finalOrder.coordinateIndex = tx.block_height
|
||||
this.AddOrder(finalOrder)
|
||||
if (order.toBtcPub === 'unknown') {
|
||||
clearTmpChainOrder(tx_hash)
|
||||
} else {
|
||||
clearTmpChainOrder(order.toBtcPub)
|
||||
}
|
||||
this._confirmedTransactions[tx_hash] = true
|
||||
}
|
||||
}
|
||||
|
||||
const Manager = new SchemaManager()
|
||||
/*invoice stream,
|
||||
this is the only place where it's needed,
|
||||
everything is a coordinate now*/
|
||||
let InvoiceShouldRetry = true
|
||||
setInterval(() => {
|
||||
if (!InvoiceShouldRetry) {
|
||||
return
|
||||
}
|
||||
if (!isAuthenticated()) {
|
||||
return
|
||||
}
|
||||
InvoiceShouldRetry = false
|
||||
|
||||
lndV2.subscribeInvoices(
|
||||
invoice => {
|
||||
if (!isAuthenticated) {
|
||||
logger.error("got an invoice while not authenticated, will ignore it and cancel the stream")
|
||||
return true
|
||||
}
|
||||
Manager.invoiceStreamDataCb(invoice)
|
||||
return false
|
||||
},
|
||||
error => {
|
||||
logger.error(`Error in invoices sub, will retry in two second, reason: ${error.reason}`)
|
||||
InvoiceShouldRetry = true
|
||||
}
|
||||
)
|
||||
|
||||
}, 2000)
|
||||
|
||||
/*transactions stream,
|
||||
this is the only place where it's needed,
|
||||
everything is a coordinate now*/
|
||||
let TransactionShouldRetry = true
|
||||
setInterval(() => {
|
||||
if (!TransactionShouldRetry) {
|
||||
return
|
||||
}
|
||||
if (!isAuthenticated()) {
|
||||
return
|
||||
}
|
||||
TransactionShouldRetry = false
|
||||
|
||||
lndV2.subscribeTransactions(
|
||||
transaction => {
|
||||
if (!isAuthenticated) {
|
||||
logger.error("got a transaction while not authenticated, will ignore it and cancel the stream")
|
||||
return true
|
||||
}
|
||||
Manager.transactionStreamDataCb(transaction)
|
||||
return false
|
||||
},
|
||||
error => {
|
||||
logger.error(`Error in transaction sub, will retry in two second, reason: ${error.reason}`)
|
||||
TransactionShouldRetry = true
|
||||
}
|
||||
)
|
||||
|
||||
}, 2000)
|
||||
|
||||
module.exports = Manager
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
const setAccessControlHeaders = (req, res) => {
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
res.header("Access-Control-Allow-Methods", "OPTIONS,POST,GET,PUT,DELETE")
|
||||
res.header(
|
||||
"Access-Control-Allow-Headers",
|
||||
"Origin, X-Requested-With, Content-Type, Accept, Authorization"
|
||||
"Origin, X-Requested-With, Content-Type, Accept, Authorization, public-key-for-decryption"
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1124,7 +1124,7 @@ module.exports = async (
|
|||
app.get('/api/lnd/listchannels', async (_, res) => {
|
||||
try {
|
||||
return res.json({
|
||||
channels: await LV2.listChannels()
|
||||
channels: await LV2.listChannels({ active_only: false })
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
|
|
@ -1192,11 +1192,8 @@ module.exports = async (
|
|||
|
||||
app.post('/api/lnd/unifiedTrx', async (req, res) => {
|
||||
try {
|
||||
const { type, amt, to, memo, feeLimit, postID, ackInfo } = req.body
|
||||
|
||||
const { type, amt, to, memo, feeLimit, ackInfo } = req.body
|
||||
if (
|
||||
type !== 'spont' &&
|
||||
type !== 'post' &&
|
||||
type !== 'spontaneousPayment' &&
|
||||
type !== 'tip' &&
|
||||
type !== 'torrentSeed' &&
|
||||
|
|
@ -1209,21 +1206,6 @@ module.exports = async (
|
|||
})
|
||||
}
|
||||
|
||||
const typesThatShouldContainAckInfo = [
|
||||
'tip',
|
||||
'torrentSeed',
|
||||
'contentReveal'
|
||||
]
|
||||
|
||||
const shouldContainAckInfo = typesThatShouldContainAckInfo.includes(type)
|
||||
|
||||
if (shouldContainAckInfo && !Common.isPopulatedString(ackInfo)) {
|
||||
return res.status(400).json({
|
||||
field: 'ackInfo',
|
||||
errorMessage: `Transactions of type ${typesThatShouldContainAckInfo} should contain an ackInfo field.`
|
||||
})
|
||||
}
|
||||
|
||||
const amount = Number(amt)
|
||||
|
||||
if (!isARealUsableNumber(amount)) {
|
||||
|
|
@ -1254,17 +1236,16 @@ module.exports = async (
|
|||
})
|
||||
}
|
||||
|
||||
if (type === 'post' && typeof postID !== 'string') {
|
||||
if (type === 'tip' && typeof ackInfo !== 'string') {
|
||||
return res.status(400).json({
|
||||
field: 'postID',
|
||||
errorMessage: `Send postID`
|
||||
field: 'ackInfo',
|
||||
errorMessage: `Send ackInfo`
|
||||
})
|
||||
}
|
||||
|
||||
return res.status(200).json(
|
||||
await GunActions.sendSpontaneousPayment(to, amt, memo, feeLimit, {
|
||||
type,
|
||||
postID,
|
||||
ackInfo
|
||||
})
|
||||
)
|
||||
|
|
@ -2188,10 +2169,12 @@ module.exports = async (
|
|||
app.post(`/api/gun/wall/`, async (req, res) => {
|
||||
try {
|
||||
const { tags, title, contentItems } = req.body
|
||||
const SEA = require('../services/gunDB/Mediator').mySEA
|
||||
return res
|
||||
.status(200)
|
||||
.json(await GunActions.createPostNew(tags, title, contentItems))
|
||||
.json(await GunActions.createPostNew(tags, title, contentItems, SEA))
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return res.status(500).json({
|
||||
errorMessage:
|
||||
(typeof e === 'string' ? e : e.message) || 'Unknown error.'
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const {
|
|||
} = require('../services/gunDB/Mediator')
|
||||
const { deepDecryptIfNeeded } = require('../services/gunDB/rpc')
|
||||
const GunEvents = require('../services/gunDB/contact-api/events')
|
||||
const SchemaManager = require('../services/schema')
|
||||
/**
|
||||
* @typedef {import('../services/gunDB/Mediator').SimpleSocket} SimpleSocket
|
||||
* @typedef {import('../services/gunDB/contact-api/SimpleGUN').ValidDataValue} ValidDataValue
|
||||
|
|
@ -73,6 +74,17 @@ module.exports = (
|
|||
stream.on('data', data => {
|
||||
logger.info('[SOCKET] New invoice data:', data)
|
||||
emitEncryptedEvent({ eventName: 'invoice:new', data, socket })
|
||||
if (!data.settled) {
|
||||
return
|
||||
}
|
||||
SchemaManager.AddOrder({
|
||||
type: 'invoice',
|
||||
amount: parseInt(data.amt_paid_sat, 10),
|
||||
coordinateHash: data.r_hash.toString('hex'),
|
||||
coordinateIndex: parseInt(data.add_index, 10),
|
||||
inbound: true,
|
||||
toLndPub: data.payment_addr
|
||||
})
|
||||
})
|
||||
stream.on('end', () => {
|
||||
logger.info('New invoice stream ended, starting a new one...')
|
||||
|
|
@ -138,7 +150,18 @@ module.exports = (
|
|||
logger.warn('Subscribing to transactions socket...' + subID)
|
||||
stream.on('data', data => {
|
||||
logger.info('[SOCKET] New transaction data:', data)
|
||||
emitEncryptedEvent({ eventName: 'transaction:new', data, socket })
|
||||
|
||||
Promise.all(data.dest_addresses.map(SchemaManager.isTmpChainOrder)).then(
|
||||
responses => {
|
||||
const hasOrder = responses.some(res => res !== false)
|
||||
if (hasOrder && data.num_confirmations > 0) {
|
||||
//buddy needs to manage this
|
||||
} else {
|
||||
//business as usual
|
||||
emitEncryptedEvent({ eventName: 'transaction:new', data, socket })
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
stream.on('end', () => {
|
||||
logger.info('New transactions stream ended, starting a new one...')
|
||||
|
|
@ -215,13 +238,14 @@ module.exports = (
|
|||
const subID = Math.floor(Math.random() * 1000).toString()
|
||||
const isNotifications = isNotificationsSocket ? 'notifications' : ''
|
||||
logger.info('[LND] New LND Socket created:' + isNotifications + subID)
|
||||
/* not used by wallet anymore
|
||||
const cancelInvoiceStream = onNewInvoice(socket, subID)
|
||||
const cancelTransactionStream = onNewTransaction(socket, subID)
|
||||
socket.on('disconnect', () => {
|
||||
logger.info('LND socket disconnected:' + isNotifications + subID)
|
||||
cancelInvoiceStream()
|
||||
cancelTransactionStream()
|
||||
})
|
||||
})*/
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -20,17 +20,26 @@ class LNDErrorManager {
|
|||
*/
|
||||
_isCheckingHealth = false
|
||||
|
||||
/**
|
||||
* @type {string|null}
|
||||
*/
|
||||
_lndPub = null
|
||||
|
||||
get lndPub() {
|
||||
return this._lndPub
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {HealthListener[]}
|
||||
*/
|
||||
_healthListeners = []
|
||||
|
||||
//rejects if(err && err.code !== 12)
|
||||
getAvailableService(){
|
||||
|
||||
getAvailableService() {
|
||||
|
||||
//require('shock-common').Utils.makePromise((res, rej) => ...)
|
||||
return new Promise((res,rej)=>{
|
||||
if(!this._isCheckingHealth){
|
||||
return new Promise((res, rej) => {
|
||||
if (!this._isCheckingHealth) {
|
||||
this._isCheckingHealth = true
|
||||
this.getInfo()
|
||||
}
|
||||
|
|
@ -39,7 +48,7 @@ class LNDErrorManager {
|
|||
* @param {LNDError} err
|
||||
* @param {object} response
|
||||
*/
|
||||
const listener = (err,response)=>{
|
||||
const listener = (err, response) => {
|
||||
if (err) {
|
||||
if (err.code === 12) {
|
||||
res({
|
||||
|
|
@ -58,7 +67,7 @@ class LNDErrorManager {
|
|||
walletStatus: 'unknown',
|
||||
success: false
|
||||
})
|
||||
} else if(err.code === 4){
|
||||
} else if (err.code === 4) {
|
||||
rej({
|
||||
service: 'unknown',
|
||||
message:
|
||||
|
|
@ -77,7 +86,7 @@ class LNDErrorManager {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
res({
|
||||
service: 'lightning',
|
||||
message: response,
|
||||
|
|
@ -85,7 +94,7 @@ class LNDErrorManager {
|
|||
walletStatus: 'unlocked',
|
||||
success: true
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
this._healthListeners.push(listener)
|
||||
})
|
||||
|
|
@ -93,28 +102,31 @@ class LNDErrorManager {
|
|||
}
|
||||
|
||||
//private
|
||||
getInfo(){
|
||||
getInfo() {
|
||||
const { lightning } = LightningServices.services
|
||||
/**
|
||||
*
|
||||
* @param {LNDError} err
|
||||
* @param {object} response
|
||||
* @param {{identity_pubkey:string}} response
|
||||
*/
|
||||
const callback = (err, response) => {
|
||||
this._healthListeners.forEach(l =>{
|
||||
l(err,response)
|
||||
if (response && response.identity_pubkey) {
|
||||
this._lndPub = response.identity_pubkey
|
||||
}
|
||||
this._healthListeners.forEach(l => {
|
||||
l(err, response)
|
||||
})
|
||||
this._healthListeners.length = 0
|
||||
this._isCheckingHealth = false
|
||||
}
|
||||
const deadline = Date.now() + 10000
|
||||
lightning.getInfo({},{deadline}, callback)
|
||||
lightning.getInfo({}, { deadline }, callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {LNDError} e
|
||||
*/
|
||||
handleError(e){
|
||||
handleError(e) {
|
||||
return this.sanitizeLNDError(e)
|
||||
}
|
||||
|
||||
|
|
@ -122,13 +134,13 @@ class LNDErrorManager {
|
|||
* @param {LNDError} e
|
||||
*/
|
||||
// eslint-disable-next-line
|
||||
sanitizeLNDError(e){
|
||||
sanitizeLNDError(e) {
|
||||
let eMessage = ''
|
||||
if(typeof e === 'string'){
|
||||
if (typeof e === 'string') {
|
||||
eMessage = e
|
||||
}else if(e.details){
|
||||
} else if (e.details) {
|
||||
eMessage = e.details
|
||||
} else if(e.message){
|
||||
} else if (e.message) {
|
||||
eMessage = e.message
|
||||
}
|
||||
if (eMessage.toLowerCase().includes('unknown')) {
|
||||
|
|
@ -137,13 +149,13 @@ class LNDErrorManager {
|
|||
? splittedMessage.slice(1).join('')
|
||||
: splittedMessage.join('')
|
||||
}
|
||||
if(eMessage === ''){
|
||||
if (eMessage === '') {
|
||||
return 'unknown LND error'
|
||||
}
|
||||
return eMessage
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ const logger = require('winston')
|
|||
const Common = require('shock-common')
|
||||
const Ramda = require('ramda')
|
||||
|
||||
const { writeCoordinate } = require('../../services/coordinates')
|
||||
|
||||
const lightningServices = require('./lightning-services')
|
||||
/**
|
||||
* @typedef {import('./types').PaymentV2} PaymentV2
|
||||
|
|
@ -237,28 +235,12 @@ const decodePayReq = payReq =>
|
|||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
const myLNDPub = () =>
|
||||
Common.makePromise((res, rej) => {
|
||||
const { lightning } = lightningServices.getServices()
|
||||
|
||||
lightning.getInfo({}, (err, data) => {
|
||||
if (err) {
|
||||
rej(new Error(err.message))
|
||||
} else {
|
||||
res(data.identity_pubkey)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* aklssjdklasd
|
||||
* @param {SendPaymentV2Request} sendPaymentRequest
|
||||
* @returns {Promise<PaymentV2>}
|
||||
*/
|
||||
const sendPaymentV2 = async sendPaymentRequest => {
|
||||
const sendPaymentV2 = sendPaymentRequest => {
|
||||
const {
|
||||
services: { router }
|
||||
} = lightningServices
|
||||
|
|
@ -269,10 +251,7 @@ const sendPaymentV2 = async sendPaymentRequest => {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import("./types").PaymentV2}
|
||||
*/
|
||||
const paymentV2 = await Common.makePromise((res, rej) => {
|
||||
return Common.makePromise((res, rej) => {
|
||||
const stream = router.sendPaymentV2(sendPaymentRequest)
|
||||
|
||||
stream.on(
|
||||
|
|
@ -311,33 +290,6 @@ const sendPaymentV2 = async sendPaymentRequest => {
|
|||
}
|
||||
)
|
||||
})
|
||||
|
||||
/** @type {Common.Coordinate} */
|
||||
const coord = {
|
||||
amount: Number(paymentV2.value_sat),
|
||||
id: paymentV2.payment_hash,
|
||||
inbound: false,
|
||||
timestamp: Date.now(),
|
||||
toLndPub: await myLNDPub(),
|
||||
fromLndPub: undefined,
|
||||
invoiceMemo: undefined,
|
||||
type: 'payment'
|
||||
}
|
||||
|
||||
if (sendPaymentRequest.payment_request) {
|
||||
const invoice = await decodePayReq(sendPaymentRequest.payment_request)
|
||||
|
||||
coord.invoiceMemo = invoice.description
|
||||
coord.toLndPub = invoice.destination
|
||||
}
|
||||
|
||||
if (sendPaymentRequest.dest) {
|
||||
coord.toLndPub = sendPaymentRequest.dest.toString('base64')
|
||||
}
|
||||
|
||||
await writeCoordinate(paymentV2.payment_hash, coord)
|
||||
|
||||
return paymentV2
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -619,6 +571,66 @@ const addInvoice = (value, memo = '', confidential = true, expiry = 180) =>
|
|||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* @typedef {object} lndErr
|
||||
* @prop {string} reason
|
||||
* @prop {number} code
|
||||
*
|
||||
*/
|
||||
/**
|
||||
* @param {(invoice:Common.Schema.InvoiceWhenListed & {r_hash:Buffer,payment_addr:string}) => (boolean | undefined)} dataCb
|
||||
* @param {(error:lndErr) => void} errorCb
|
||||
*/
|
||||
const subscribeInvoices = (dataCb, errorCb) => {
|
||||
const { lightning } = lightningServices.getServices()
|
||||
const stream = lightning.subscribeInvoices({})
|
||||
stream.on('data', invoice => {
|
||||
const cancelStream = dataCb(invoice)
|
||||
if (cancelStream) {
|
||||
//@ts-expect-error
|
||||
stream.cancel()
|
||||
}
|
||||
})
|
||||
stream.on('error', error => {
|
||||
errorCb(error)
|
||||
try {
|
||||
//@ts-expect-error
|
||||
stream.cancel()
|
||||
} catch {
|
||||
logger.info(
|
||||
'[subscribeInvoices] tried to cancel an already canceled stream'
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(tx:Common.Schema.ChainTransaction) => (boolean | undefined)} dataCb
|
||||
* @param {(error:lndErr) => void} errorCb
|
||||
*/
|
||||
const subscribeTransactions = (dataCb, errorCb) => {
|
||||
const { lightning } = lightningServices.getServices()
|
||||
const stream = lightning.subscribeTransactions({})
|
||||
stream.on('data', transaction => {
|
||||
const cancelStream = dataCb(transaction)
|
||||
if (cancelStream) {
|
||||
//@ts-expect-error
|
||||
stream.cancel()
|
||||
}
|
||||
})
|
||||
stream.on('error', error => {
|
||||
errorCb(error)
|
||||
try {
|
||||
//@ts-expect-error
|
||||
stream.cancel()
|
||||
} catch {
|
||||
logger.info(
|
||||
'[subscribeTransactions] tried to cancel an already canceled stream'
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendPaymentV2Keysend,
|
||||
sendPaymentV2Invoice,
|
||||
|
|
@ -631,5 +643,6 @@ module.exports = {
|
|||
listPeers,
|
||||
pendingChannels,
|
||||
addInvoice,
|
||||
myLNDPub
|
||||
subscribeInvoices,
|
||||
subscribeTransactions
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue