commit
2c744fb92a
9 changed files with 187 additions and 244 deletions
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"eslint.enable": true,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"debug.node.autoAttach": "on"
|
||||
"debug.node.autoAttach": "on",
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ module.exports = (mainnet = false) => {
|
|||
maxNumRoutesToQuery: 20,
|
||||
lndProto: parsePath(`${__dirname}/rpc.proto`),
|
||||
routerProto: parsePath(`${__dirname}/router.proto`),
|
||||
invoicesProto: parsePath(`${__dirname}/invoices.proto`),
|
||||
walletUnlockerProto: parsePath(`${__dirname}/walletunlocker.proto`),
|
||||
lndHost: "localhost:10009",
|
||||
lndCertPath: parsePath(`${lndDirectory}/tls.cert`),
|
||||
|
|
|
|||
122
config/invoices.proto
Normal file
122
config/invoices.proto
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
syntax = "proto3";
|
||||
|
||||
import "rpc.proto";
|
||||
|
||||
package invoicesrpc;
|
||||
|
||||
option go_package = "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc";
|
||||
|
||||
// Invoices is a service that can be used to create, accept, settle and cancel
|
||||
// invoices.
|
||||
service Invoices {
|
||||
/*
|
||||
SubscribeSingleInvoice returns a uni-directional stream (server -> client)
|
||||
to notify the client of state transitions of the specified invoice.
|
||||
Initially the current invoice state is always sent out.
|
||||
*/
|
||||
rpc SubscribeSingleInvoice (SubscribeSingleInvoiceRequest)
|
||||
returns (stream lnrpc.Invoice);
|
||||
|
||||
/*
|
||||
CancelInvoice cancels a currently open invoice. If the invoice is already
|
||||
canceled, this call will succeed. If the invoice is already settled, it will
|
||||
fail.
|
||||
*/
|
||||
rpc CancelInvoice (CancelInvoiceMsg) returns (CancelInvoiceResp);
|
||||
|
||||
/*
|
||||
AddHoldInvoice creates a hold invoice. It ties the invoice to the hash
|
||||
supplied in the request.
|
||||
*/
|
||||
rpc AddHoldInvoice (AddHoldInvoiceRequest) returns (AddHoldInvoiceResp);
|
||||
|
||||
/*
|
||||
SettleInvoice settles an accepted invoice. If the invoice is already
|
||||
settled, this call will succeed.
|
||||
*/
|
||||
rpc SettleInvoice (SettleInvoiceMsg) returns (SettleInvoiceResp);
|
||||
}
|
||||
|
||||
message CancelInvoiceMsg {
|
||||
// Hash corresponding to the (hold) invoice to cancel.
|
||||
bytes payment_hash = 1;
|
||||
}
|
||||
message CancelInvoiceResp {
|
||||
}
|
||||
|
||||
message AddHoldInvoiceRequest {
|
||||
/*
|
||||
An optional memo to attach along with the invoice. Used for record keeping
|
||||
purposes for the invoice's creator, and will also be set in the description
|
||||
field of the encoded payment request if the description_hash field is not
|
||||
being used.
|
||||
*/
|
||||
string memo = 1;
|
||||
|
||||
// The hash of the preimage
|
||||
bytes hash = 2;
|
||||
|
||||
/*
|
||||
The value of this invoice in satoshis
|
||||
|
||||
The fields value and value_msat are mutually exclusive.
|
||||
*/
|
||||
int64 value = 3;
|
||||
|
||||
/*
|
||||
The value of this invoice in millisatoshis
|
||||
|
||||
The fields value and value_msat are mutually exclusive.
|
||||
*/
|
||||
int64 value_msat = 10;
|
||||
|
||||
/*
|
||||
Hash (SHA-256) of a description of the payment. Used if the description of
|
||||
payment (memo) is too long to naturally fit within the description field
|
||||
of an encoded payment request.
|
||||
*/
|
||||
bytes description_hash = 4;
|
||||
|
||||
// Payment request expiry time in seconds. Default is 3600 (1 hour).
|
||||
int64 expiry = 5;
|
||||
|
||||
// Fallback on-chain address.
|
||||
string fallback_addr = 6;
|
||||
|
||||
// Delta to use for the time-lock of the CLTV extended to the final hop.
|
||||
uint64 cltv_expiry = 7;
|
||||
|
||||
/*
|
||||
Route hints that can each be individually used to assist in reaching the
|
||||
invoice's destination.
|
||||
*/
|
||||
repeated lnrpc.RouteHint route_hints = 8;
|
||||
|
||||
// Whether this invoice should include routing hints for private channels.
|
||||
bool private = 9;
|
||||
}
|
||||
|
||||
message AddHoldInvoiceResp {
|
||||
/*
|
||||
A bare-bones invoice for a payment within the Lightning Network. With the
|
||||
details of the invoice, the sender has all the data necessary to send a
|
||||
payment to the recipient.
|
||||
*/
|
||||
string payment_request = 1;
|
||||
}
|
||||
|
||||
message SettleInvoiceMsg {
|
||||
// Externally discovered pre-image that should be used to settle the hold
|
||||
// invoice.
|
||||
bytes preimage = 1;
|
||||
}
|
||||
|
||||
message SettleInvoiceResp {
|
||||
}
|
||||
|
||||
message SubscribeSingleInvoiceRequest {
|
||||
reserved 1;
|
||||
|
||||
// Hash corresponding to the (hold) invoice to subscribe to.
|
||||
bytes r_hash = 2;
|
||||
}
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
/**
|
||||
* @format
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
const { performance } = require('perf_hooks')
|
||||
const logger = require('winston')
|
||||
const isFinite = require('lodash/isFinite')
|
||||
const isNumber = require('lodash/isNumber')
|
||||
const isNaN = require('lodash/isNaN')
|
||||
const Common = require('shock-common')
|
||||
const {
|
||||
Constants: { ErrorCode },
|
||||
Schema
|
||||
} = require('shock-common')
|
||||
} = Common
|
||||
|
||||
const LightningServices = require('../../../../utils/lightningServices')
|
||||
|
||||
|
|
@ -226,30 +227,49 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
|||
|
||||
const invoicePutEndTime = performance.now() - invoicePutStartTime
|
||||
|
||||
const hash = invoice.r_hash.toString('base64')
|
||||
|
||||
// invoices should be settled right away so we can rely on this single
|
||||
// subscription instead of life-long all invoices subscription
|
||||
if (order.targetType === 'post') {
|
||||
const { subscribeSingleInvoice } = LightningServices.invoices
|
||||
const { postID } = order
|
||||
|
||||
if (!Common.isPopulatedString(postID)) {
|
||||
throw new TypeError(`postID not a a populated string`)
|
||||
}
|
||||
|
||||
const stream = subscribeSingleInvoice({ r_hash: hash })
|
||||
|
||||
/**
|
||||
* @param {Common.Invoice} invoice
|
||||
*/
|
||||
const onData = invoice => {
|
||||
if (invoice.settled) {
|
||||
getUser()
|
||||
.get('postToTipCount')
|
||||
.get(postID)
|
||||
.set(null) // each item in the set is a tip
|
||||
|
||||
stream.off()
|
||||
}
|
||||
}
|
||||
|
||||
stream.on('data', onData)
|
||||
|
||||
stream.on('status', (/** @type {any} */ status) => {
|
||||
logger.info(`Post tip, post: ${postID}, invoice status: ${status}`)
|
||||
})
|
||||
stream.on('end', () => {
|
||||
logger.warn(`Post tip, post: ${postID}, invoice stream ended`)
|
||||
})
|
||||
}
|
||||
|
||||
logger.info(`[PERF] Added invoice to GunDB in ${invoicePutEndTime}ms`)
|
||||
|
||||
const listenerEndTime = performance.now() - listenerStartTime
|
||||
|
||||
logger.info(`[PERF] Invoice generation completed in ${listenerEndTime}ms`)
|
||||
|
||||
const hash = invoice.r_hash.toString('base64')
|
||||
|
||||
if (order.targetType === 'post') {
|
||||
/** @type {TipPaymentStatus} */
|
||||
const paymentStatus = {
|
||||
hash,
|
||||
state: 'OPEN',
|
||||
targetType: order.targetType,
|
||||
postID: order.postID
|
||||
}
|
||||
getUser()
|
||||
.get(Key.TIPS_PAYMENT_STATUS)
|
||||
.get(hash)
|
||||
// @ts-ignore
|
||||
.put(paymentStatus, response => {
|
||||
console.log(response)
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`error inside onOrders, orderAddr: ${addr}, orderID: ${orderID}, order: ${JSON.stringify(
|
||||
|
|
|
|||
|
|
@ -59,8 +59,6 @@ exports.POSTS = 'posts'
|
|||
// Tips counter for posts
|
||||
exports.TOTAL_TIPS = 'totalTips'
|
||||
|
||||
exports.TIPS_PAYMENT_STATUS = 'tipsPaymentStatus'
|
||||
|
||||
exports.PROFILE_BINARY = 'profileBinary'
|
||||
|
||||
exports.POSTS_NEW = 'posts'
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const errorConstants = require("../../constants/errors");
|
|||
* @typedef LightningConfig
|
||||
* @prop {string} lnrpcProtoPath
|
||||
* @prop {string} routerProtoPath
|
||||
* @prop {string} invoicesProtoPath
|
||||
* @prop {string} walletUnlockerProtoPath
|
||||
* @prop {string} lndHost
|
||||
* @prop {string} lndCertPath
|
||||
|
|
@ -21,6 +22,7 @@ const errorConstants = require("../../constants/errors");
|
|||
* @prop {any} lightning
|
||||
* @prop {any} walletUnlocker
|
||||
* @prop {any} router
|
||||
* @prop {any} invoices
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
@ -30,10 +32,11 @@ const errorConstants = require("../../constants/errors");
|
|||
module.exports = async ({
|
||||
lnrpcProtoPath,
|
||||
routerProtoPath,
|
||||
invoicesProtoPath,
|
||||
walletUnlockerProtoPath,
|
||||
lndHost,
|
||||
lndCertPath,
|
||||
macaroonPath
|
||||
lndHost,
|
||||
lndCertPath,
|
||||
macaroonPath
|
||||
}) => {
|
||||
try {
|
||||
process.env.GRPC_SSL_CIPHER_SUITES = "HIGH+ECDSA";
|
||||
|
|
@ -46,9 +49,15 @@ module.exports = async ({
|
|||
includeDirs: ["node_modules/google-proto-files", "proto", Path.resolve(__dirname, "../../config")]
|
||||
}
|
||||
|
||||
const [lnrpcProto, routerProto, walletUnlockerProto] = await Promise.all([protoLoader.load(lnrpcProtoPath, protoLoaderConfig), protoLoader.load(routerProtoPath, protoLoaderConfig), protoLoader.load(walletUnlockerProtoPath, protoLoaderConfig)]);
|
||||
const [lnrpcProto, routerProto, walletUnlockerProto, invoicesProto] = await Promise.all([
|
||||
protoLoader.load(lnrpcProtoPath, protoLoaderConfig),
|
||||
protoLoader.load(routerProtoPath, protoLoaderConfig),
|
||||
protoLoader.load(walletUnlockerProtoPath, protoLoaderConfig),
|
||||
protoLoader.load(invoicesProtoPath, protoLoaderConfig)
|
||||
]);
|
||||
const { lnrpc } = grpc.loadPackageDefinition(lnrpcProto);
|
||||
const { routerrpc } = grpc.loadPackageDefinition(routerProto);
|
||||
const { invoicesrpc } = grpc.loadPackageDefinition(invoicesProto);
|
||||
const { lnrpc: walletunlockerrpc } = grpc.loadPackageDefinition(walletUnlockerProto);
|
||||
|
||||
const getCredentials = async () => {
|
||||
|
|
@ -93,11 +102,13 @@ module.exports = async ({
|
|||
const walletUnlocker = new walletunlockerrpc.WalletUnlocker(lndHost, credentials);
|
||||
// @ts-ignore
|
||||
const router = new routerrpc.Router(lndHost, credentials);
|
||||
|
||||
// @ts-ignore
|
||||
const invoices = new invoicesrpc.Invoices(lndHost, credentials);
|
||||
return {
|
||||
lightning,
|
||||
walletUnlocker,
|
||||
router
|
||||
router,
|
||||
invoices
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ const {
|
|||
sendPaymentV2Invoice,
|
||||
listPayments
|
||||
} = require('../utils/lightningServices/v2')
|
||||
const { startTipStatusJob } = require('../utils/lndJobs')
|
||||
const GunWriteRPC = require('../services/gunDB/rpc')
|
||||
|
||||
const DEFAULT_MAX_NUM_ROUTES_TO_QUERY = 10
|
||||
|
|
@ -690,7 +689,6 @@ module.exports = async (
|
|||
}
|
||||
|
||||
onNewChannelBackup()
|
||||
startTipStatusJob()
|
||||
|
||||
res.json({
|
||||
authorization: token,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const lnrpc = require('../../services/lnd/lightning')
|
|||
* @prop {string} macaroonPath
|
||||
* @prop {string} lndProto
|
||||
* @prop {string} routerProto
|
||||
* @prop {string} invoicesProto
|
||||
* @prop {string} walletUnlockerProto
|
||||
*/
|
||||
|
||||
|
|
@ -119,6 +120,7 @@ class LightningServices {
|
|||
const lnServices = await lnrpc({
|
||||
lnrpcProtoPath: this.defaults.lndProto,
|
||||
routerProtoPath: this.defaults.routerProto,
|
||||
invoicesProtoPath: this.defaults.invoicesProto,
|
||||
walletUnlockerProtoPath: this.defaults.walletUnlockerProto,
|
||||
lndHost,
|
||||
lndCertPath,
|
||||
|
|
@ -127,10 +129,11 @@ class LightningServices {
|
|||
if (!lnServices) {
|
||||
throw new Error(`Could not init lnServices`)
|
||||
}
|
||||
const { lightning, walletUnlocker, router } = lnServices
|
||||
const { lightning, walletUnlocker, router, invoices } = lnServices
|
||||
this.lightning = lightning
|
||||
this.walletUnlocker = walletUnlocker
|
||||
this.router = router
|
||||
this.invoices = invoices
|
||||
this.lnServicesData = {
|
||||
lndProto: this.defaults.lndProto,
|
||||
lndHost,
|
||||
|
|
|
|||
211
utils/lndJobs.js
211
utils/lndJobs.js
|
|
@ -1,211 +0,0 @@
|
|||
/**
|
||||
* @prettier
|
||||
*/
|
||||
const Logger = require('winston')
|
||||
const { wait } = require('./helpers')
|
||||
const Key = require('../services/gunDB/contact-api/key')
|
||||
const { getUser } = require('../services/gunDB/Mediator')
|
||||
const LightningServices = require('./lightningServices')
|
||||
|
||||
const ERROR_TRIES_THRESHOLD = 3
|
||||
const ERROR_TRIES_DELAY = 500
|
||||
const INVOICE_STATE = {
|
||||
OPEN: 'OPEN',
|
||||
SETTLED: 'SETTLED',
|
||||
CANCELLED: 'CANCELLED',
|
||||
ACCEPTED: 'ACCEPTED'
|
||||
}
|
||||
|
||||
const _lookupInvoice = hash =>
|
||||
new Promise((resolve, reject) => {
|
||||
const { lightning } = LightningServices.services
|
||||
lightning.lookupInvoice({ r_hash: hash }, (err, response) => {
|
||||
if (err) {
|
||||
Logger.error(
|
||||
'[TIP] An error has occurred while trying to lookup invoice:',
|
||||
err,
|
||||
'\nInvoice Hash:',
|
||||
hash
|
||||
)
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
|
||||
Logger.info('[TIP] Invoice lookup result:', response)
|
||||
resolve(response)
|
||||
})
|
||||
})
|
||||
|
||||
const _getPostTipInfo = ({ postID }) =>
|
||||
new Promise((resolve, reject) => {
|
||||
getUser()
|
||||
.get(Key.POSTS_NEW)
|
||||
.get(postID)
|
||||
.once(post => {
|
||||
if (post && post.date) {
|
||||
const { tipCounter, tipValue } = post
|
||||
resolve({
|
||||
tipCounter: typeof tipCounter === 'number' ? tipCounter : 0,
|
||||
tipValue: typeof tipValue === 'number' ? tipValue : 0
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
resolve(post)
|
||||
})
|
||||
})
|
||||
|
||||
const _incrementPost = ({ postID, orderAmount }) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const parsedAmount = parseFloat(orderAmount)
|
||||
|
||||
if (typeof parsedAmount !== 'number') {
|
||||
reject(new Error('Invalid order amount specified'))
|
||||
return
|
||||
}
|
||||
|
||||
Logger.info('[POST TIP] Getting Post Tip Values...')
|
||||
|
||||
return _getPostTipInfo({ postID })
|
||||
.then(({ tipValue, tipCounter }) => {
|
||||
const updatedTip = {
|
||||
tipCounter: tipCounter + 1,
|
||||
tipValue: tipValue + parsedAmount
|
||||
}
|
||||
|
||||
getUser()
|
||||
.get(Key.POSTS_NEW)
|
||||
.get(postID)
|
||||
.put(updatedTip, () => {
|
||||
Logger.info('[POST TIP] Successfully updated Post tip info')
|
||||
resolve(updatedTip)
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
Logger.error(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
|
||||
const _updateTipData = (invoiceHash, data) =>
|
||||
new Promise((resolve, reject) => {
|
||||
try {
|
||||
getUser()
|
||||
.get(Key.TIPS_PAYMENT_STATUS)
|
||||
.get(invoiceHash)
|
||||
.put(data, tip => {
|
||||
if (tip === undefined) {
|
||||
reject(new Error('Tip update failed'))
|
||||
return
|
||||
}
|
||||
|
||||
console.log(tip)
|
||||
|
||||
resolve(tip)
|
||||
})
|
||||
} catch (err) {
|
||||
Logger.error('An error has occurred while updating tip^data')
|
||||
throw err
|
||||
}
|
||||
})
|
||||
|
||||
const _getTipData = (invoiceHash, tries = 0) =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (tries >= ERROR_TRIES_THRESHOLD) {
|
||||
reject(new Error('Malformed data'))
|
||||
return
|
||||
}
|
||||
|
||||
getUser()
|
||||
.get(Key.TIPS_PAYMENT_STATUS)
|
||||
.get(invoiceHash)
|
||||
.once(async tip => {
|
||||
try {
|
||||
if (tip === undefined) {
|
||||
await wait(ERROR_TRIES_DELAY)
|
||||
const tip = await _getTipData(invoiceHash, tries + 1)
|
||||
|
||||
if (tip) {
|
||||
resolve(tip)
|
||||
return
|
||||
}
|
||||
|
||||
reject(new Error('Malformed data'))
|
||||
return
|
||||
}
|
||||
|
||||
resolve(tip)
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const executeTipAction = (tip, invoice) => {
|
||||
if (invoice.state !== INVOICE_STATE.SETTLED) {
|
||||
return
|
||||
}
|
||||
|
||||
// Execute actions once invoice is settled
|
||||
Logger.info('Invoice settled!', invoice)
|
||||
|
||||
if (tip.targetType === 'post') {
|
||||
_incrementPost({
|
||||
postID: tip.postID,
|
||||
orderAmount: invoice.amt_paid_sat
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const updateUnverifiedTips = () => {
|
||||
getUser()
|
||||
.get(Key.TIPS_PAYMENT_STATUS)
|
||||
.map()
|
||||
.once(async (tip, id) => {
|
||||
try {
|
||||
if (
|
||||
!tip ||
|
||||
tip.state !== INVOICE_STATE.OPEN ||
|
||||
(tip._errorCount && tip._errorCount >= ERROR_TRIES_THRESHOLD)
|
||||
) {
|
||||
return
|
||||
}
|
||||
Logger.info('Unverified invoice found!', tip)
|
||||
const invoice = await _lookupInvoice(tip.hash)
|
||||
Logger.info('Invoice located:', invoice)
|
||||
if (invoice.state !== tip.state) {
|
||||
await _updateTipData(id, { state: invoice.state })
|
||||
|
||||
// Actions to be executed when the tip's state is updated
|
||||
executeTipAction(tip, invoice)
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error('[TIP] An error has occurred while updating invoice', err)
|
||||
const errorCount = tip._errorCount ? tip._errorCount : 0
|
||||
_updateTipData(id, {
|
||||
_errorCount: errorCount + 1
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const startTipStatusJob = () => {
|
||||
const { lightning } = LightningServices.services
|
||||
const stream = lightning.subscribeInvoices({})
|
||||
updateUnverifiedTips()
|
||||
stream.on('data', async invoice => {
|
||||
const hash = invoice.r_hash.toString('base64')
|
||||
const tip = await _getTipData(hash)
|
||||
if (tip.state !== invoice.state) {
|
||||
await _updateTipData(hash, { state: invoice.state })
|
||||
executeTipAction(tip, invoice)
|
||||
}
|
||||
})
|
||||
stream.on('error', err => {
|
||||
Logger.error('Tip Job error' + err.details)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
startTipStatusJob
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue