Merge pull request #311 from shocknet/fix/coordinates

Fix/coordinates
This commit is contained in:
CapDog 2021-02-27 15:07:44 -05:00 committed by GitHub
commit de45807e59
12 changed files with 1001 additions and 341 deletions

View file

@ -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

View file

@ -105,4 +105,4 @@
"pre-commit": "yarn lint && yarn typecheck && yarn lint-staged"
}
}
}
}

View file

@ -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)
}
}

View file

@ -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()

View file

@ -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(

View file

@ -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
View 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

View file

@ -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"
);
};

View file

@ -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.'

View file

@ -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()
})
})*/
}
})

View file

@ -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
}
}

View file

@ -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
}