commit
ac5b5e675d
7 changed files with 600 additions and 163 deletions
|
|
@ -8,7 +8,11 @@ const { Constants, Schema } = Common
|
||||||
|
|
||||||
const { ErrorCode } = Constants
|
const { ErrorCode } = Constants
|
||||||
|
|
||||||
const LightningServices = require('../../../utils/lightningServices')
|
const { sendPaymentV2Invoice } = require('../../../utils/lightningServices/v2')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('../../../utils/lightningServices/types').PaymentV2} PaymentV2
|
||||||
|
*/
|
||||||
|
|
||||||
const Getters = require('./getters')
|
const Getters = require('./getters')
|
||||||
const Key = require('./key')
|
const Key = require('./key')
|
||||||
|
|
@ -903,20 +907,11 @@ const sendHRWithInitialMsg = async (
|
||||||
* @param {number} amount
|
* @param {number} amount
|
||||||
* @param {string} memo
|
* @param {string} memo
|
||||||
* @param {number} feeLimit
|
* @param {number} feeLimit
|
||||||
* @param {number=} maxParts
|
|
||||||
* @param {number=} timeoutSeconds
|
|
||||||
* @throws {Error} If no response in less than 20 seconds from the recipient, or
|
* @throws {Error} If no response in less than 20 seconds from the recipient, or
|
||||||
* lightning cannot find a route for the payment.
|
* lightning cannot find a route for the payment.
|
||||||
* @returns {Promise<string>} The payment's preimage.
|
* @returns {Promise<PaymentV2>} The payment's preimage.
|
||||||
*/
|
*/
|
||||||
const sendPayment = async (
|
const sendSpontaneousPayment = async (to, amount, memo, feeLimit) => {
|
||||||
to,
|
|
||||||
amount,
|
|
||||||
memo,
|
|
||||||
feeLimit,
|
|
||||||
maxParts = 3,
|
|
||||||
timeoutSeconds = 5
|
|
||||||
) => {
|
|
||||||
try {
|
try {
|
||||||
const SEA = require('../Mediator').mySEA
|
const SEA = require('../Mediator').mySEA
|
||||||
const getUser = () => require('../Mediator').getUser()
|
const getUser = () => require('../Mediator').getUser()
|
||||||
|
|
@ -1046,69 +1041,27 @@ const sendPayment = async (
|
||||||
throw new Error(orderResponse.response)
|
throw new Error(orderResponse.response)
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
|
||||||
services: { router }
|
|
||||||
} = LightningServices
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {object} SendErr
|
|
||||||
* @prop {string} details
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Partial
|
|
||||||
* https://api.lightning.community/#sendpaymentv2
|
|
||||||
* @typedef {object} SendResponse
|
|
||||||
* @prop {string} failure_reason
|
|
||||||
* @prop {string} payment_preimage
|
|
||||||
*/
|
|
||||||
|
|
||||||
logger.info('Will now send payment through lightning')
|
logger.info('Will now send payment through lightning')
|
||||||
|
|
||||||
const sendPaymentV2Args = {
|
const payment = await sendPaymentV2Invoice({
|
||||||
/** @type {string} */
|
feeLimit,
|
||||||
payment_request: orderResponse.response,
|
payment_request: orderResponse.response
|
||||||
max_parts: maxParts,
|
|
||||||
timeout_seconds: timeoutSeconds,
|
|
||||||
no_inflight_updates: true,
|
|
||||||
fee_limit_sat: feeLimit
|
|
||||||
}
|
|
||||||
|
|
||||||
const preimage = await new Promise((res, rej) => {
|
|
||||||
const sentPaymentStream = router.sendPaymentV2(sendPaymentV2Args)
|
|
||||||
/**
|
|
||||||
* @param {SendResponse} response
|
|
||||||
*/
|
|
||||||
const dataCB = response => {
|
|
||||||
logger.info('SendPayment Data:', response)
|
|
||||||
if (response.failure_reason !== 'FAILURE_REASON_NONE') {
|
|
||||||
rej(new Error(response.failure_reason))
|
|
||||||
} else {
|
|
||||||
res(response.payment_preimage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sentPaymentStream.on('data', dataCB)
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {SendErr} err
|
|
||||||
*/
|
|
||||||
const errCB = err => {
|
|
||||||
logger.error('SendPayment Error:', err)
|
|
||||||
rej(err.details)
|
|
||||||
}
|
|
||||||
sentPaymentStream.on('error', errCB)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (Utils.successfulHandshakeAlreadyExists(to)) {
|
if (Utils.successfulHandshakeAlreadyExists(to)) {
|
||||||
await sendMessage(
|
await sendMessage(
|
||||||
to,
|
to,
|
||||||
Schema.encodeSpontaneousPayment(amount, memo || 'no memo', preimage),
|
Schema.encodeSpontaneousPayment(
|
||||||
|
amount,
|
||||||
|
memo || 'no memo',
|
||||||
|
payment.payment_preimage
|
||||||
|
),
|
||||||
require('../Mediator').getUser(),
|
require('../Mediator').getUser(),
|
||||||
require('../Mediator').mySEA
|
require('../Mediator').mySEA
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return preimage
|
return payment
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error inside sendPayment()')
|
logger.error('Error inside sendPayment()')
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
|
|
@ -1116,6 +1069,21 @@ const sendPayment = async (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the preimage corresponding to the payment.
|
||||||
|
* @param {string} to
|
||||||
|
* @param {number} amount
|
||||||
|
* @param {string} memo
|
||||||
|
* @param {number} feeLimit
|
||||||
|
* @throws {Error} If no response in less than 20 seconds from the recipient, or
|
||||||
|
* lightning cannot find a route for the payment.
|
||||||
|
* @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
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {UserGUNNode} user
|
* @param {UserGUNNode} user
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
|
|
@ -1632,5 +1600,6 @@ module.exports = {
|
||||||
follow,
|
follow,
|
||||||
unfollow,
|
unfollow,
|
||||||
initWall,
|
initWall,
|
||||||
sendMessageNew
|
sendMessageNew,
|
||||||
|
sendSpontaneousPayment
|
||||||
}
|
}
|
||||||
|
|
|
||||||
145
src/routes.js
145
src/routes.js
|
|
@ -29,6 +29,10 @@ const {
|
||||||
const GunActions = require('../services/gunDB/contact-api/actions')
|
const GunActions = require('../services/gunDB/contact-api/actions')
|
||||||
const GunGetters = require('../services/gunDB/contact-api/getters')
|
const GunGetters = require('../services/gunDB/contact-api/getters')
|
||||||
const GunKey = require('../services/gunDB/contact-api/key')
|
const GunKey = require('../services/gunDB/contact-api/key')
|
||||||
|
const {
|
||||||
|
sendPaymentV2Keysend,
|
||||||
|
sendPaymentV2Invoice
|
||||||
|
} = require('../utils/lightningServices/v2')
|
||||||
|
|
||||||
const DEFAULT_MAX_NUM_ROUTES_TO_QUERY = 10
|
const DEFAULT_MAX_NUM_ROUTES_TO_QUERY = 10
|
||||||
const SESSION_ID = uuid()
|
const SESSION_ID = uuid()
|
||||||
|
|
@ -1159,6 +1163,57 @@ module.exports = async (
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.post('/api/lnd/unifiedTrx', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { type, amt, to, memo, feeLimit } = req.body
|
||||||
|
|
||||||
|
if (type !== 'spont') {
|
||||||
|
return res.status(415).json({
|
||||||
|
field: 'type',
|
||||||
|
errorMessage: `Only 'spont' payments supported via this endpoint for now.`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const amount = Number(amt)
|
||||||
|
|
||||||
|
if (!isARealUsableNumber(amount)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
field: 'amt',
|
||||||
|
errorMessage: 'Not an usable number'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amount < 1) {
|
||||||
|
return res.status(400).json({
|
||||||
|
field: 'amt',
|
||||||
|
errorMessage: 'Must be 1 or greater.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isARealUsableNumber(feeLimit)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
field: 'feeLimit',
|
||||||
|
errorMessage: 'Not an usable number'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feeLimit < 1) {
|
||||||
|
return res.status(400).json({
|
||||||
|
field: 'feeLimit',
|
||||||
|
errorMessage: 'Must be 1 or greater.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
.status(200)
|
||||||
|
.json(await GunActions.sendSpontaneousPayment(to, amt, memo, feeLimit))
|
||||||
|
} catch (e) {
|
||||||
|
return res.status(500).json({
|
||||||
|
errorMessage: e.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// get lnd node payments list
|
// get lnd node payments list
|
||||||
app.get('/api/lnd/listpayments', (req, res) => {
|
app.get('/api/lnd/listpayments', (req, res) => {
|
||||||
const { lightning } = LightningServices.services
|
const { lightning } = LightningServices.services
|
||||||
|
|
@ -1442,92 +1497,46 @@ module.exports = async (
|
||||||
})
|
})
|
||||||
|
|
||||||
// sendpayment
|
// sendpayment
|
||||||
app.post('/api/lnd/sendpayment', (req, res) => {
|
app.post('/api/lnd/sendpayment', async (req, res) => {
|
||||||
const { router } = LightningServices.services
|
|
||||||
// this is the recommended value from lightning labs
|
// this is the recommended value from lightning labs
|
||||||
let paymentRequest = {}
|
|
||||||
const { keysend, maxParts = 3, timeoutSeconds = 5, feeLimit } = req.body
|
const { keysend, maxParts = 3, timeoutSeconds = 5, feeLimit } = req.body
|
||||||
|
|
||||||
if (!feeLimit) {
|
if (!feeLimit) {
|
||||||
return res.status(500).json({
|
return res.status(400).json({
|
||||||
errorMessage: 'please provide a "feeLimit" to the send payment request'
|
errorMessage: 'please provide a "feeLimit" to the send payment request'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keysend) {
|
if (keysend) {
|
||||||
const { dest, amt, finalCltvDelta = 40 } = req.body
|
const { dest, amt, finalCltvDelta = 40 } = req.body
|
||||||
if (!dest || !amt) {
|
if (!dest || !amt) {
|
||||||
return res.status(500).json({
|
return res.status(400).json({
|
||||||
errorMessage: 'please provide "dest" and "amt" for keysend payments'
|
errorMessage: 'please provide "dest" and "amt" for keysend payments'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const preimage = Crypto.randomBytes(32)
|
|
||||||
const r_hash = Crypto.createHash('sha256')
|
const payment = await sendPaymentV2Keysend({
|
||||||
.update(preimage)
|
|
||||||
.digest()
|
|
||||||
//https://github.com/lightningnetwork/lnd/blob/master/record/experimental.go#L5:2
|
|
||||||
//might break in future updates
|
|
||||||
const KeySendType = 5482373484
|
|
||||||
//https://api.lightning.community/#featurebit
|
|
||||||
const TLV_ONION_REQ = 8
|
|
||||||
paymentRequest = {
|
|
||||||
dest: Buffer.from(dest, 'hex'),
|
|
||||||
amt,
|
amt,
|
||||||
final_cltv_delta: finalCltvDelta,
|
dest,
|
||||||
dest_features: [TLV_ONION_REQ],
|
feeLimit,
|
||||||
dest_custom_records: {
|
finalCltvDelta,
|
||||||
[KeySendType]: preimage
|
maxParts,
|
||||||
},
|
timeoutSeconds
|
||||||
payment_hash: r_hash,
|
})
|
||||||
max_parts: maxParts,
|
|
||||||
timeout_seconds: timeoutSeconds,
|
|
||||||
no_inflight_updates: true,
|
|
||||||
fee_limit_sat: feeLimit
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const { payreq } = req.body
|
|
||||||
|
|
||||||
paymentRequest = {
|
return res.status(200).json(payment)
|
||||||
payment_request: payreq,
|
|
||||||
max_parts: maxParts,
|
|
||||||
timeout_seconds: timeoutSeconds,
|
|
||||||
no_inflight_updates: true,
|
|
||||||
fee_limit_sat: feeLimit
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.body.amt) {
|
|
||||||
paymentRequest.amt = req.body.amt
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const { payreq } = req.body
|
||||||
|
|
||||||
logger.info('Sending payment', paymentRequest)
|
const payment = await sendPaymentV2Invoice({
|
||||||
const sentPayment = router.sendPaymentV2(paymentRequest)
|
feeLimit,
|
||||||
sentPayment.on('data', response => {
|
payment_request: payreq,
|
||||||
logger.info('SendPayment Data:', response)
|
amt: req.body.amt,
|
||||||
if (response.failure_reason !== 'FAILURE_REASON_NONE') {
|
max_parts: maxParts,
|
||||||
res.status(500).json({
|
timeoutSeconds
|
||||||
errorMessage: response.failure_reason
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
res.json(response)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
sentPayment.on('status', status => {
|
return res.status(200).json(payment)
|
||||||
logger.info('SendPayment Status:', status)
|
|
||||||
})
|
|
||||||
|
|
||||||
sentPayment.on('error', async err => {
|
|
||||||
logger.error('SendPayment Error:', err)
|
|
||||||
const health = await checkHealth()
|
|
||||||
if (health.LNDStatus.success) {
|
|
||||||
res.status(500).json({
|
|
||||||
errorMessage: sanitizeLNDError(err.details)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
res.status(500)
|
|
||||||
res.json({ errorMessage: 'LND is down' })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
//sentPayment.on('end', () => {})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
app.post('/api/lnd/trackpayment', (req, res) => {
|
app.post('/api/lnd/trackpayment', (req, res) => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"include": ["./services/gunDB/**/*.*"],
|
"include": ["./services/gunDB/**/*.*", "./utils/lightningServices/**/*.*"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
/* Basic Options */
|
/* Basic Options */
|
||||||
// "incremental": true, /* Enable incremental compilation */
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
|
|
|
||||||
1
utils/lightningServices/index.js
Normal file
1
utils/lightningServices/index.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require('./lightning-services')
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
const FS = require("../utils/fs");
|
/**
|
||||||
const lnrpc = require("../services/lnd/lightning");
|
* @format
|
||||||
|
*/
|
||||||
/**
|
/**
|
||||||
* @typedef {import('commander').Command} Command
|
* @typedef {import('commander').Command} Command
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const FS = require('../../utils/fs')
|
||||||
|
const lnrpc = require('../../services/lnd/lightning')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {object} Config
|
* @typedef {object} Config
|
||||||
* @prop {boolean} useTLS
|
* @prop {boolean} useTLS
|
||||||
|
|
@ -43,9 +46,9 @@ class LightningServices {
|
||||||
/**
|
/**
|
||||||
* @type {Config}
|
* @type {Config}
|
||||||
*/
|
*/
|
||||||
const newDefaults = require("../config/defaults")(program.mainnet)
|
const newDefaults = require('../../config/defaults')(program.mainnet)
|
||||||
|
|
||||||
this.defaults = newDefaults;
|
this.defaults = newDefaults
|
||||||
|
|
||||||
this._config = {
|
this._config = {
|
||||||
...newDefaults,
|
...newDefaults,
|
||||||
|
|
@ -55,11 +58,11 @@ class LightningServices {
|
||||||
lndHost: program.lndhost || newDefaults.lndHost,
|
lndHost: program.lndhost || newDefaults.lndHost,
|
||||||
lndCertPath: program.lndCertPath || newDefaults.lndCertPath,
|
lndCertPath: program.lndCertPath || newDefaults.lndCertPath,
|
||||||
macaroonPath: program.macaroonPath || newDefaults.macaroonPath
|
macaroonPath: program.macaroonPath || newDefaults.macaroonPath
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isInitialized = () => {
|
isInitialized = () => {
|
||||||
return !!(this.lightning && this.walletUnlocker);
|
return !!(this.lightning && this.walletUnlocker)
|
||||||
}
|
}
|
||||||
|
|
||||||
get services() {
|
get services() {
|
||||||
|
|
@ -67,11 +70,11 @@ class LightningServices {
|
||||||
lightning: this.lightning,
|
lightning: this.lightning,
|
||||||
walletUnlocker: this.walletUnlocker,
|
walletUnlocker: this.walletUnlocker,
|
||||||
router: this.router
|
router: this.router
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get servicesData() {
|
get servicesData() {
|
||||||
return this.lnServicesData;
|
return this.lnServicesData
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -82,7 +85,9 @@ class LightningServices {
|
||||||
return this._config
|
return this._config
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('Tried to access LightningServices.servicesConfig without setting defaults first.')
|
throw new Error(
|
||||||
|
'Tried to access LightningServices.servicesConfig without setting defaults first.'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
get config() {
|
get config() {
|
||||||
|
|
@ -90,7 +95,9 @@ class LightningServices {
|
||||||
return this._config
|
return this._config
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('Tried to access LightningServices.config without setting defaults first.')
|
throw new Error(
|
||||||
|
'Tried to access LightningServices.config without setting defaults first.'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -101,38 +108,38 @@ class LightningServices {
|
||||||
return this._defaults
|
return this._defaults
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('Tried to access LightningServices.defaults without setting them first.')
|
throw new Error(
|
||||||
|
'Tried to access LightningServices.defaults without setting them first.'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
init = async () => {
|
init = async () => {
|
||||||
const { macaroonPath, lndHost, lndCertPath } = this.config;
|
const { macaroonPath, lndHost, lndCertPath } = this.config
|
||||||
const macaroonExists = await FS.access(macaroonPath);
|
const macaroonExists = await FS.access(macaroonPath)
|
||||||
const lnServices = await lnrpc(
|
const lnServices = await lnrpc({
|
||||||
{
|
lnrpcProtoPath: this.defaults.lndProto,
|
||||||
lnrpcProtoPath: this.defaults.lndProto,
|
routerProtoPath: this.defaults.routerProto,
|
||||||
routerProtoPath: this.defaults.routerProto,
|
walletUnlockerProtoPath: this.defaults.walletUnlockerProto,
|
||||||
walletUnlockerProtoPath: this.defaults.walletUnlockerProto,
|
lndHost,
|
||||||
lndHost,
|
lndCertPath,
|
||||||
lndCertPath,
|
macaroonPath: macaroonExists ? macaroonPath : null
|
||||||
macaroonPath: macaroonExists ? macaroonPath : null
|
})
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!lnServices) {
|
if (!lnServices) {
|
||||||
throw new Error(`Could not init lnServices`)
|
throw new Error(`Could not init lnServices`)
|
||||||
}
|
}
|
||||||
const { lightning, walletUnlocker, router } = lnServices;
|
const { lightning, walletUnlocker, router } = lnServices
|
||||||
this.lightning = lightning;
|
this.lightning = lightning
|
||||||
this.walletUnlocker = walletUnlocker
|
this.walletUnlocker = walletUnlocker
|
||||||
this.router = router;
|
this.router = router
|
||||||
this.lnServicesData = {
|
this.lnServicesData = {
|
||||||
lndProto: this.defaults.lndProto,
|
lndProto: this.defaults.lndProto,
|
||||||
lndHost,
|
lndHost,
|
||||||
lndCertPath,
|
lndCertPath,
|
||||||
macaroonPath: macaroonExists ? macaroonPath : null
|
macaroonPath: macaroonExists ? macaroonPath : null
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const lightningServices = new LightningServices();
|
const lightningServices = new LightningServices()
|
||||||
|
|
||||||
module.exports = lightningServices;
|
module.exports = lightningServices
|
||||||
108
utils/lightningServices/types.ts
Normal file
108
utils/lightningServices/types.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
/**
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface PaymentV2 {
|
||||||
|
payment_hash: string
|
||||||
|
|
||||||
|
creation_date: string
|
||||||
|
|
||||||
|
payment_preimage: string
|
||||||
|
|
||||||
|
value_sat: string
|
||||||
|
|
||||||
|
value_msat: string
|
||||||
|
|
||||||
|
payment_request: string
|
||||||
|
|
||||||
|
status: 'UNKNOWN' | 'IN_FLIGHT' | 'SUCCEEDED' | 'FAILED'
|
||||||
|
|
||||||
|
fee_sat: number
|
||||||
|
|
||||||
|
fee_msat: number
|
||||||
|
|
||||||
|
creation_time_ns: string
|
||||||
|
|
||||||
|
payment_index: string
|
||||||
|
|
||||||
|
failure_reason:
|
||||||
|
| 'FAILURE_REASON_NONE'
|
||||||
|
| 'FAILURE_REASON_TIMEOUT'
|
||||||
|
| 'FAILURE_REASON_NO_ROUTE'
|
||||||
|
| 'FAILURE_REASON_ERROR'
|
||||||
|
| 'FAILURE_REASON_INCORRECT_PAYMENT_DETAILS'
|
||||||
|
| 'FAILURE_REASON_INSUFFICIENT_BALANCE'
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FeatureBit {
|
||||||
|
DATALOSS_PROTECT_REQ = 0,
|
||||||
|
DATALOSS_PROTECT_OPT = 1,
|
||||||
|
INITIAL_ROUING_SYNC = 3,
|
||||||
|
UPFRONT_SHUTDOWN_SCRIPT_REQ = 4,
|
||||||
|
UPFRONT_SHUTDOWN_SCRIPT_OPT = 5,
|
||||||
|
GOSSIP_QUERIES_REQ = 6,
|
||||||
|
GOSSIP_QUERIES_OPT = 7,
|
||||||
|
TLV_ONION_REQ = 8,
|
||||||
|
TLV_ONION_OPT = 9,
|
||||||
|
EXT_GOSSIP_QUERIES_REQ = 10,
|
||||||
|
EXT_GOSSIP_QUERIES_OPT = 11,
|
||||||
|
STATIC_REMOTE_KEY_REQ = 12,
|
||||||
|
STATIC_REMOTE_KEY_OPT = 13,
|
||||||
|
PAYMENT_ADDR_REQ = 14,
|
||||||
|
PAYMENT_ADDR_OPT = 15,
|
||||||
|
MPP_REQ = 16,
|
||||||
|
MPP_OPT = 17
|
||||||
|
}
|
||||||
|
|
||||||
|
interface _SendPaymentV2Request {
|
||||||
|
dest: Buffer
|
||||||
|
/**
|
||||||
|
* Number of satoshis to send. The fields amt and amt_msat are mutually
|
||||||
|
* exclusive.
|
||||||
|
*/
|
||||||
|
amt: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The CLTV delta from the current height that should be used to set the
|
||||||
|
* timelock for the final hop.
|
||||||
|
*/
|
||||||
|
final_cltv_delta: number
|
||||||
|
|
||||||
|
dest_features: FeatureBit[]
|
||||||
|
|
||||||
|
dest_custom_records: Record<number, Buffer>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The hash to use within the payment's HTLC.
|
||||||
|
*/
|
||||||
|
payment_hash: Buffer
|
||||||
|
|
||||||
|
max_parts: number
|
||||||
|
|
||||||
|
timeout_seconds: number
|
||||||
|
|
||||||
|
no_inflight_updates: boolean
|
||||||
|
|
||||||
|
payment_request: string
|
||||||
|
|
||||||
|
fee_limit_sat: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SendPaymentV2Request = Partial<_SendPaymentV2Request>
|
||||||
|
|
||||||
|
export interface SendPaymentKeysendParams {
|
||||||
|
amt: string
|
||||||
|
dest: string
|
||||||
|
feeLimit: number
|
||||||
|
finalCltvDelta?: number
|
||||||
|
maxParts?: number
|
||||||
|
timeoutSeconds?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SendPaymentInvoiceParams {
|
||||||
|
amt?: string
|
||||||
|
feeLimit: number
|
||||||
|
max_parts?: number
|
||||||
|
payment_request: string
|
||||||
|
timeoutSeconds?: number
|
||||||
|
}
|
||||||
343
utils/lightningServices/v2.js
Normal file
343
utils/lightningServices/v2.js
Normal file
|
|
@ -0,0 +1,343 @@
|
||||||
|
/**
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
const Crypto = require('crypto')
|
||||||
|
const logger = require('winston')
|
||||||
|
|
||||||
|
const lightningServices = require('./lightning-services')
|
||||||
|
/**
|
||||||
|
* @typedef {import('./types').PaymentV2} PaymentV2
|
||||||
|
* @typedef {import('./types').SendPaymentV2Request} SendPaymentV2Request
|
||||||
|
* @typedef {import('./types').SendPaymentInvoiceParams} SendPaymentInvoiceParams
|
||||||
|
* @typedef {import('./types').SendPaymentKeysendParams} SendPaymentKeysendParams
|
||||||
|
*/
|
||||||
|
|
||||||
|
//https://github.com/lightningnetwork/lnd/blob/master/record/experimental.go#L5:2
|
||||||
|
//might break in future updates
|
||||||
|
const KeySendType = 5482373484
|
||||||
|
//https://api.lightning.community/#featurebit
|
||||||
|
const TLV_ONION_REQ = 8
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {SendPaymentV2Request} sendPaymentRequest
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
const isValidSendPaymentRequest = sendPaymentRequest => {
|
||||||
|
const {
|
||||||
|
amt,
|
||||||
|
dest,
|
||||||
|
dest_custom_records,
|
||||||
|
dest_features,
|
||||||
|
final_cltv_delta,
|
||||||
|
max_parts,
|
||||||
|
no_inflight_updates,
|
||||||
|
payment_hash,
|
||||||
|
timeout_seconds,
|
||||||
|
fee_limit_sat,
|
||||||
|
payment_request
|
||||||
|
} = sendPaymentRequest
|
||||||
|
|
||||||
|
if (typeof amt !== 'undefined' && typeof amt !== 'string') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof dest !== 'undefined' && !(dest instanceof Buffer)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof dest_custom_records !== 'undefined' &&
|
||||||
|
typeof dest_custom_records !== 'object'
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof dest_custom_records !== 'undefined' &&
|
||||||
|
dest_custom_records === null
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof dest_custom_records !== 'undefined' &&
|
||||||
|
Object.keys(dest_custom_records).length === 0
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof dest_features !== 'undefined' && !Array.isArray(dest_features)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof dest_features !== 'undefined' && dest_features.length === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof final_cltv_delta !== 'undefined' &&
|
||||||
|
typeof final_cltv_delta !== 'number'
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof payment_hash !== 'undefined' &&
|
||||||
|
!(payment_hash instanceof Buffer)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof max_parts !== 'undefined' && typeof max_parts !== 'number') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof timeout_seconds !== 'undefined' &&
|
||||||
|
typeof timeout_seconds !== 'number'
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof no_inflight_updates !== 'undefined' &&
|
||||||
|
typeof no_inflight_updates !== 'boolean'
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof fee_limit_sat !== 'undefined' &&
|
||||||
|
typeof fee_limit_sat !== 'number'
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof payment_request !== 'undefined' &&
|
||||||
|
typeof payment_request !== 'string'
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {SendPaymentKeysendParams} sendPaymentKeysendParams
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
const isValidSendPaymentKeysendParams = sendPaymentKeysendParams => {
|
||||||
|
const {
|
||||||
|
amt,
|
||||||
|
dest,
|
||||||
|
feeLimit,
|
||||||
|
finalCltvDelta,
|
||||||
|
maxParts,
|
||||||
|
timeoutSeconds
|
||||||
|
} = sendPaymentKeysendParams
|
||||||
|
|
||||||
|
if (typeof amt !== 'string') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof dest !== 'string') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof feeLimit !== 'number') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof finalCltvDelta !== 'undefined' &&
|
||||||
|
typeof finalCltvDelta !== 'number'
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof maxParts !== 'undefined' && typeof maxParts !== 'number') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof timeoutSeconds !== 'undefined' &&
|
||||||
|
typeof timeoutSeconds !== 'number'
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {SendPaymentInvoiceParams} sendPaymentInvoiceParams
|
||||||
|
*/
|
||||||
|
const isValidSendPaymentInvoiceParams = sendPaymentInvoiceParams => {
|
||||||
|
const {
|
||||||
|
amt,
|
||||||
|
feeLimit,
|
||||||
|
max_parts,
|
||||||
|
payment_request,
|
||||||
|
timeoutSeconds
|
||||||
|
} = sendPaymentInvoiceParams
|
||||||
|
|
||||||
|
// payment_request: string
|
||||||
|
// timeoutSeconds?: number
|
||||||
|
|
||||||
|
if (typeof amt !== 'undefined' && typeof amt !== 'string') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof feeLimit !== 'number') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof max_parts !== 'undefined' && typeof max_parts !== 'number') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof payment_request !== 'string') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof timeoutSeconds !== 'undefined' &&
|
||||||
|
typeof timeoutSeconds !== 'number'
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* aklssjdklasd
|
||||||
|
* @param {SendPaymentV2Request} sendPaymentRequest
|
||||||
|
* @returns {Promise<PaymentV2>}
|
||||||
|
*/
|
||||||
|
const sendPaymentV2 = sendPaymentRequest => {
|
||||||
|
const {
|
||||||
|
services: { router }
|
||||||
|
} = lightningServices
|
||||||
|
|
||||||
|
if (!isValidSendPaymentRequest(sendPaymentRequest)) {
|
||||||
|
throw new TypeError(
|
||||||
|
`Invalid SendPaymentRequest: ${JSON.stringify(sendPaymentRequest)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {SendPaymentV2Request}
|
||||||
|
*/
|
||||||
|
const paymentRequest = {
|
||||||
|
...sendPaymentRequest,
|
||||||
|
no_inflight_updates: true
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
const stream = router.sendPaymentV2(paymentRequest)
|
||||||
|
|
||||||
|
stream.on(
|
||||||
|
'data',
|
||||||
|
/**
|
||||||
|
* @param {import("./types").PaymentV2} streamingPaymentV2
|
||||||
|
*/ streamingPaymentV2 => {
|
||||||
|
if (streamingPaymentV2.failure_reason !== 'FAILURE_REASON_NONE') {
|
||||||
|
rej(new Error(streamingPaymentV2.failure_reason))
|
||||||
|
} else {
|
||||||
|
res(streamingPaymentV2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
stream.on('status', status => {
|
||||||
|
logger.info('SendPaymentV2 Status:', status)
|
||||||
|
})
|
||||||
|
|
||||||
|
stream.on(
|
||||||
|
'error',
|
||||||
|
/**
|
||||||
|
* @param {{ details: any; }} err
|
||||||
|
*/ err => {
|
||||||
|
logger.error('SendPaymentV2 Error:', err)
|
||||||
|
|
||||||
|
rej(err.details || err)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {SendPaymentKeysendParams} params
|
||||||
|
* @returns {Promise<PaymentV2>}
|
||||||
|
*/
|
||||||
|
const sendPaymentV2Keysend = params => {
|
||||||
|
const {
|
||||||
|
amt,
|
||||||
|
dest,
|
||||||
|
feeLimit,
|
||||||
|
finalCltvDelta,
|
||||||
|
maxParts,
|
||||||
|
timeoutSeconds
|
||||||
|
} = params
|
||||||
|
|
||||||
|
if (!isValidSendPaymentKeysendParams(params)) {
|
||||||
|
throw new TypeError(
|
||||||
|
`Invalid SendPaymentKeysendParams: ${JSON.stringify(params)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const preimage = Crypto.randomBytes(32)
|
||||||
|
const r_hash = Crypto.createHash('sha256')
|
||||||
|
.update(preimage)
|
||||||
|
.digest()
|
||||||
|
|
||||||
|
return sendPaymentV2({
|
||||||
|
dest: Buffer.from(dest, 'hex'),
|
||||||
|
amt,
|
||||||
|
final_cltv_delta: finalCltvDelta,
|
||||||
|
dest_features: [TLV_ONION_REQ],
|
||||||
|
dest_custom_records: {
|
||||||
|
[KeySendType]: preimage
|
||||||
|
},
|
||||||
|
payment_hash: r_hash,
|
||||||
|
max_parts: maxParts,
|
||||||
|
timeout_seconds: timeoutSeconds,
|
||||||
|
fee_limit_sat: feeLimit
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {SendPaymentInvoiceParams} params
|
||||||
|
* @returns {Promise<PaymentV2>}
|
||||||
|
*/
|
||||||
|
const sendPaymentV2Invoice = params => {
|
||||||
|
const {
|
||||||
|
feeLimit,
|
||||||
|
payment_request,
|
||||||
|
amt,
|
||||||
|
max_parts = 3,
|
||||||
|
timeoutSeconds = 5
|
||||||
|
} = params
|
||||||
|
|
||||||
|
if (!isValidSendPaymentInvoiceParams(params)) {
|
||||||
|
throw new TypeError(
|
||||||
|
`Invalid SendPaymentInvoiceParams: ${JSON.stringify(params)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendPaymentV2({
|
||||||
|
amt,
|
||||||
|
payment_request,
|
||||||
|
fee_limit_sat: feeLimit,
|
||||||
|
max_parts,
|
||||||
|
timeout_seconds: timeoutSeconds
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sendPaymentV2Keysend,
|
||||||
|
sendPaymentV2Invoice
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue