Merge pull request #166 from shocknet/tipping

Tipping
This commit is contained in:
CapDog 2020-08-25 21:20:26 -04:00 committed by GitHub
commit ac5b5e675d
7 changed files with 600 additions and 163 deletions

View file

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

View file

@ -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) => {

View file

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

View file

@ -0,0 +1 @@
module.exports = require('./lightning-services')

View file

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

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

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