diff --git a/utils/lightningServices/index.js b/utils/lightningServices/index.js new file mode 100644 index 00000000..a3555b28 --- /dev/null +++ b/utils/lightningServices/index.js @@ -0,0 +1 @@ +module.exports = require('./lightning-services') \ No newline at end of file diff --git a/utils/lightningServices.js b/utils/lightningServices/lightning-services.js similarity index 94% rename from utils/lightningServices.js rename to utils/lightningServices/lightning-services.js index 8985174f..a0631f1b 100644 --- a/utils/lightningServices.js +++ b/utils/lightningServices/lightning-services.js @@ -1,13 +1,13 @@ /** * @format */ -const FS = require('../utils/fs') -const lnrpc = require('../services/lnd/lightning') - /** * @typedef {import('commander').Command} Command */ +const FS = require('../../utils/fs') +const lnrpc = require('../../services/lnd/lightning') + /** * @typedef {object} Config * @prop {boolean} useTLS @@ -46,7 +46,7 @@ class LightningServices { /** * @type {Config} */ - const newDefaults = require('../config/defaults')(program.mainnet) + const newDefaults = require('../../config/defaults')(program.mainnet) this.defaults = newDefaults diff --git a/utils/lightningServices/types.ts b/utils/lightningServices/types.ts new file mode 100644 index 00000000..68729844 --- /dev/null +++ b/utils/lightningServices/types.ts @@ -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: string + + fee_msat: string + + 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 + + /** + * 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: string +} + +export type SendPaymentV2Request = Partial<_SendPaymentV2Request> + +export interface SendPaymentKeysendParams { + amt: string + dest: string + feeLimit: string + finalCltvDelta?: number + maxParts?: number + timeoutSeconds?: number +} + +export interface SendPaymentInvoiceParams { + amt?: string + feeLimit: string + max_parts?: number + payment_request: string + timeoutSeconds?: number +} diff --git a/utils/lightningServices/v2.js b/utils/lightningServices/v2.js new file mode 100644 index 00000000..68e21c36 --- /dev/null +++ b/utils/lightningServices/v2.js @@ -0,0 +1,339 @@ +/** + * @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 !== 'string') { + 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 !== 'string') { + 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} + */ +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} + */ +const sendPaymentV2Keysend = params => { + const { + amt, + dest, + feeLimit, + finalCltvDelta, + maxParts, + timeoutSeconds + } = params + + if (!isValidSendPaymentKeysendParams(params)) { + throw new TypeError('Invalid SendPaymentKeysendParams') + } + + 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} + */ +const sendPaymentV2Invoice = params => { + const { + feeLimit, + payment_request, + amt, + max_parts = 3, + timeoutSeconds = 5 + } = params + + if (!isValidSendPaymentInvoiceParams(params)) { + throw new TypeError('Invalid SendPaymentInvoiceParams') + } + + return sendPaymentV2({ + amt, + payment_request, + fee_limit_sat: feeLimit, + max_parts, + timeout_seconds: timeoutSeconds + }) +} + +module.exports = { + sendPaymentV2Keysend, + sendPaymentV2Invoice +}