diff --git a/.vscode/snippets.code-snippets b/.vscode/snippets.code-snippets new file mode 100644 index 00000000..53bd01c5 --- /dev/null +++ b/.vscode/snippets.code-snippets @@ -0,0 +1,33 @@ +{ + // Place your api workspace snippets here. Each snippet is defined under a + // snippet name and has a scope, prefix, body and description. Add comma + // separated ids of the languages where the snippet is applicable in the scope + // field. If scope is left empty or omitted, the snippet gets applied to all + // languages. The prefix is what is used to trigger the snippet and the body + // will be expanded and inserted. Possible variables are: $1, $2 for tab + // stops, $0 for the final cursor position, and ${1:label}, ${2:another} for + // placeholders. Placeholders with the same ids are connected. Example: "Print + // to console": {"scope": "javascript,typescript", "prefix": "log", "body": + // ["console.log('$1');", "$2" + // ], + // "description": "Log output to console" + // } + + "Route Body": { + "body": [ + "try {", + " return res.json({", + "", + " })", + "} catch (e) {", + " console.log(e)", + " return res.status(500).json({", + " errorMessage: e.message", + " })", + "}" + ], + "description": "Route Body", + "prefix": "rbody", + "scope": "javascript" + } +} \ No newline at end of file diff --git a/src/routes.js b/src/routes.js index 715cd43d..f61a4857 100644 --- a/src/routes.js +++ b/src/routes.js @@ -30,11 +30,7 @@ const { const GunActions = require('../services/gunDB/contact-api/actions') const GunGetters = require('../services/gunDB/contact-api/getters') const GunKey = require('../services/gunDB/contact-api/key') -const { - sendPaymentV2Keysend, - sendPaymentV2Invoice, - listPayments -} = require('../utils/lightningServices/v2') +const LV2 = require('../utils/lightningServices/v2') const GunWriteRPC = require('../services/gunDB/rpc') const DEFAULT_MAX_NUM_ROUTES_TO_QUERY = 10 @@ -1024,30 +1020,15 @@ module.exports = async ( ) }) // get lnd chan info - app.post('/api/lnd/getchaninfo', (req, res) => { - const { lightning } = LightningServices.services - - lightning.getChanInfo( - { chan_id: req.body.chan_id }, - async (err, response) => { - if (err) { - logger.debug('GetChanInfo Error:', err) - const health = await checkHealth() - if (health.LNDStatus.success) { - res.status(400) - res.json({ - field: 'getChanInfo', - errorMessage: sanitizeLNDError(err.message) - }) - } else { - res.status(500) - res.json({ errorMessage: 'LND is down' }) - } - } - logger.debug('GetChanInfo:', response) - res.json(response) - } - ) + app.post('/api/lnd/getchaninfo', async (req, res) => { + try { + return res.json(await LV2.getChanInfo(req.body.chan_id)) + } catch (e) { + console.log(e) + return res.status(500).json({ + errorMessage: e.message + }) + } }) app.get('/api/lnd/getnetworkinfo', (req, res) => { @@ -1072,47 +1053,30 @@ module.exports = async ( }) // get lnd node active channels list - app.get('/api/lnd/listpeers', (req, res) => { - const { lightning } = LightningServices.services - lightning.listPeers({}, async (err, response) => { - if (err) { - logger.debug('ListPeers Error:', err) - const health = await checkHealth() - if (health.LNDStatus.success) { - res.status(400).json({ - field: 'listPeers', - errorMessage: sanitizeLNDError(err.message) - }) - } else { - res.status(500) - res.json({ errorMessage: 'LND is down' }) - } - } - logger.debug('ListPeers:', response) - res.json(response) - }) + app.get('/api/lnd/listpeers', async (req, res) => { + try { + return res.json({ + peers: await LV2.listPeers(req.body.latestError) + }) + } catch (e) { + console.log(e) + return res.status(500).json({ + errorMessage: e.message + }) + } }) // newaddress - app.post('/api/lnd/newaddress', (req, res) => { - const { lightning } = LightningServices.services - lightning.newAddress({ type: req.body.type }, async (err, response) => { - if (err) { - logger.debug('NewAddress Error:', err) - const health = await checkHealth() - if (health.LNDStatus.success) { - res.status(400).json({ - field: 'newAddress', - errorMessage: sanitizeLNDError(err.message) - }) - } else { - res.status(500) - res.json({ errorMessage: 'LND is down' }) - } - } - logger.debug('NewAddress:', response) - res.json(response) - }) + app.post('/api/lnd/newaddress', async (req, res) => { + try { + return res.json({ + address: await LV2.newAddress(req.body.type) + }) + } catch (e) { + return res.status(500).json({ + errorMessage: e.message + }) + } }) // connect peer to lnd node @@ -1157,47 +1121,28 @@ module.exports = async ( }) // get lnd node opened channels list - app.get('/api/lnd/listchannels', (req, res) => { - const { lightning } = LightningServices.services - lightning.listChannels({}, async (err, response) => { - if (err) { - logger.debug('ListChannels Error:', err) - const health = await checkHealth() - if (health.LNDStatus.success) { - res.status(400).json({ - field: 'listChannels', - errorMessage: sanitizeLNDError(err.message) - }) - } else { - res.status(500) - res.json({ errorMessage: 'LND is down' }) - } - } - logger.debug('ListChannels:', response) - res.json(response) - }) + app.get('/api/lnd/listchannels', async (_, res) => { + try { + return res.json({ + channels: await LV2.listChannels() + }) + } catch (e) { + console.log(e) + return res.status(500).json({ + errorMessage: e.message + }) + } }) - // get lnd node pending channels list - app.get('/api/lnd/pendingchannels', (req, res) => { - const { lightning } = LightningServices.services - lightning.pendingChannels({}, async (err, response) => { - if (err) { - logger.debug('PendingChannels Error:', err) - const health = await checkHealth() - if (health.LNDStatus.success) { - res.status(400).json({ - field: 'pendingChannels', - errorMessage: sanitizeLNDError(err.message) - }) - } else { - res.status(500) - res.json({ errorMessage: 'LND is down' }) - } - } - logger.debug('PendingChannels:', response) - res.json(response) - }) + app.get('/api/lnd/pendingchannels', async (req, res) => { + try { + return res.json(await LV2.pendingChannels()) + } catch (e) { + console.log(e) + return res.status(500).json({ + errorMessage: e.message + }) + } }) app.get('/api/lnd/unifiedTrx', (req, res) => { @@ -1375,7 +1320,7 @@ module.exports = async ( } return res.status(200).json( - await listPayments({ + await LV2.listPayments({ include_incomplete, index_offset, max_payments, @@ -1662,7 +1607,7 @@ module.exports = async ( }) } - const payment = await sendPaymentV2Keysend({ + const payment = await LV2.sendPaymentV2Keysend({ amt, dest, feeLimit, @@ -1675,7 +1620,7 @@ module.exports = async ( } const { payreq } = req.body - const payment = await sendPaymentV2Invoice({ + const payment = await LV2.sendPaymentV2Invoice({ feeLimit, payment_request: payreq, amt: req.body.amt, @@ -1764,64 +1709,32 @@ module.exports = async ( }) // addinvoice - app.post('/api/lnd/addinvoice', (req, res) => { - const { lightning } = LightningServices.services - const invoiceRequest = { memo: req.body.memo, private: true } - if (req.body.value) { - invoiceRequest.value = req.body.value - } - if (req.body.expiry) { - invoiceRequest.expiry = req.body.expiry - } - lightning.addInvoice(invoiceRequest, async (err, newInvoice) => { - if (err) { - logger.debug('AddInvoice Error:', err) - const health = await checkHealth() - if (health.LNDStatus.success) { - res.status(400).json({ - field: 'addInvoice', - errorMessage: sanitizeLNDError(err.message) - }) - } else { - res.status(500) - res.json({ errorMessage: 'LND is down' }) + app.post('/api/lnd/addinvoice', async (req, res) => { + const { expiry, value, memo } = req.body + const addInvoiceRes = await LV2.addInvoice(value, memo, true, expiry) + + if (value) { + const channelsList = await LV2.listChannels({ active_only: true }) + let remoteBalance = Big(0) + channelsList.forEach(element => { + const remB = Big(element.remote_balance) + if (remB.gt(remoteBalance)) { + remoteBalance = remB } - return err - } - logger.debug('AddInvoice:', newInvoice) - if (req.body.value) { - logger.debug('AddInvoice liquidity check:') - lightning.listChannels({ active_only: true }, async (err, response) => { - if (err) { - logger.debug('ListChannels Error:', err) - const health = await checkHealth() - if (health.LNDStatus.success) { - res.status(400).json({ - field: 'listChannels', - errorMessage: sanitizeLNDError(err.message) - }) - } else { - res.status(500) - res.json({ errorMessage: 'LND is down' }) - } - } - logger.debug('ListChannels:', response) - const channelsList = response.channels - let remoteBalance = Big(0) - channelsList.forEach(element => { - const remB = Big(element.remote_balance) - if (remB.gt(remoteBalance)) { - remoteBalance = remB - } - }) - newInvoice.liquidityCheck = remoteBalance > req.body.value - //newInvoice.remoteBalance = remoteBalance - res.json(newInvoice) - }) - } else { - res.json(newInvoice) - } - }) + }) + + addInvoiceRes.liquidityCheck = remoteBalance > value + //newInvoice.remoteBalance = remoteBalance + } + + try { + return res.json(addInvoiceRes) + } catch (e) { + console.log(e) + return res.status(500).json({ + errorMessage: e.message + }) + } }) // signmessage @@ -1959,23 +1872,25 @@ module.exports = async ( ) }) - app.post('/api/lnd/listunspent', (req, res) => { - const { lightning } = LightningServices.services - const { minConfirmations = 3, maxConfirmations = 6 } = req.body - lightning.listUnspent( - { - min_confs: minConfirmations, - max_confs: maxConfirmations - }, - (err, unspent) => { - if (err) { - return handleError(res, err) - } - logger.debug('ListUnspent:', unspent) - res.json(unspent) - } - ) - }) + const listunspent = async (req, res) => { + try { + return res.status(200).json({ + utxos: await LV2.listUnspent( + req.body.minConfirmations, + req.body.maxConfirmations + ) + }) + } catch (e) { + return res.status(500).json({ + errorMessage: e.message + }) + } + } + + app.get('/api/lnd/listunspent', listunspent) + + // TODO: should be GET + app.post('/api/lnd/listunspent', listunspent) app.get('/api/lnd/transactions', (req, res) => { const { lightning } = LightningServices.services diff --git a/utils/lightningServices/lightning-services.js b/utils/lightningServices/lightning-services.js index 9c198c3b..71ef98d9 100644 --- a/utils/lightningServices/lightning-services.js +++ b/utils/lightningServices/lightning-services.js @@ -74,6 +74,13 @@ class LightningServices { } } + /** + * @returns {import('./types').Services} + */ + getServices() { + return this.services + } + get servicesData() { return this.lnServicesData } diff --git a/utils/lightningServices/types.ts b/utils/lightningServices/types.ts index f8e77d81..ca0d034f 100644 --- a/utils/lightningServices/types.ts +++ b/utils/lightningServices/types.ts @@ -1,6 +1,7 @@ /** * @format */ +import * as Common from 'shock-common' export interface PaymentV2 { payment_hash: string @@ -106,3 +107,90 @@ export interface SendPaymentInvoiceParams { payment_request: string timeoutSeconds?: number } + +type StreamListener = (data: any) => void + +/** + * Caution: Not all methods return an stream. + */ +interface LightningStream { + on(ev: 'data' | 'end' | 'error' | 'status', listener: StreamListener): void +} + +type LightningCB = (err: Error, data: Record) => void + +type LightningMethod = ( + args: Record, + cb?: LightningCB +) => LightningStream + +/** + * Makes it easier for code calling services. + */ +export interface Services { + lightning: Record + walletUnlocker: Record + router: Record +} + +export interface ListChannelsReq { + active_only: boolean + inactive_only: boolean + public_only: boolean + private_only: boolean + /** + * Filters the response for channels with a target peer's pubkey. If peer is + * empty, all channels will be returned. + */ + peer: Common.Bytes +} + +/** + * https://api.lightning.community/#pendingchannels + */ +export interface PendingChannelsRes { + /** + * The balance in satoshis encumbered in pending channels. + */ + total_limbo_balance: string + /** + * Channels pending opening. + */ + pending_open_channels: Common.PendingOpenChannel[] + /** + * Channels pending force closing. + */ + pending_force_closing_channels: Common.ForceClosedChannel[] + /** + * Channels waiting for closing tx to confirm. + */ + waiting_close_channels: Common.WaitingCloseChannel[] +} + +/** + * https://github.com/lightningnetwork/lnd/blob/daf7c8a85420fc67fffa18fa5f7d08c2040946e4/lnrpc/rpc.proto#L2948 + */ +export interface AddInvoiceRes { + /** + * + */ + r_hash: Common.Bytes + /** + * 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. + */ + payment_request: string + /** + * The "add" index of this invoice. Each newly created invoice will increment + * this index making it monotonically increasing. Callers to the + * SubscribeInvoices call can use this to instantly get notified of all added + * invoices with an add_index greater than this one. + */ + add_index: string + /** + * The payment address of the generated invoice. This value should be used in + * all payments for this invoice as we require it for end to end security. + */ + payment_addr: Common.Bytes +} diff --git a/utils/lightningServices/v2.js b/utils/lightningServices/v2.js index 34f42f44..b933c63b 100644 --- a/utils/lightningServices/v2.js +++ b/utils/lightningServices/v2.js @@ -402,9 +402,185 @@ const decodePayReq = payReq => ) }) +/** + * @param {0|1} type + * @returns {Promise} + */ +const newAddress = (type = 0) => { + const { lightning } = lightningServices.getServices() + + return Common.Utils.makePromise((res, rej) => { + lightning.newAddress({ type }, (err, response) => { + if (err) { + rej(new Error(err.message)) + } else { + res(response.address) + } + }) + }) +} + +/** + * @param {number} minConfs + * @param {number} maxConfs + * @returns {Promise} + */ +const listUnspent = (minConfs = 3, maxConfs = 6) => + Common.makePromise((res, rej) => { + const { lightning } = lightningServices.getServices() + + lightning.listUnspent( + { + min_confs: minConfs, + max_confs: maxConfs + }, + (err, unspent) => { + if (err) { + rej(new Error(err.message)) + } else { + res(unspent.utxos) + } + } + ) + }) + +/** + * @typedef {import('./types').ListChannelsReq} ListChannelsReq + */ + +/** + * @param {ListChannelsReq} req + * @returns {Promise} + */ +const listChannels = req => + Common.makePromise((res, rej) => { + const { lightning } = lightningServices.getServices() + + lightning.listChannels(req, (err, resp) => { + if (err) { + rej(new Error(err.message)) + } else { + res(resp.channels) + } + }) + }) + +/** + * https://api.lightning.community/#getchaninfo + * @param {string} chanID + * @returns {Promise} + */ +const getChanInfo = chanID => + Common.makePromise((res, rej) => { + const { lightning } = lightningServices.getServices() + + lightning.getChanInfo( + { + chan_id: chanID + }, + (err, resp) => { + if (err) { + rej(new Error(err.message)) + } else { + // Needs cast because typescript refuses to assign Record + // to an actual object :shrugs + res(/** @type {Common.ChannelEdge} */ (resp)) + } + } + ) + }) + +/** + * https://api.lightning.community/#listpeers + * @param {boolean=} latestError If true, only the last error that our peer sent + * us will be returned with the peer's information, rather than the full set of + * historic errors we have stored. + * @returns {Promise} + */ +const listPeers = latestError => + Common.makePromise((res, rej) => { + const { lightning } = lightningServices.getServices() + + lightning.listPeers( + { + latest_error: latestError + }, + (err, resp) => { + if (err) { + rej(new Error(err.message)) + } else { + res(resp.peers) + } + } + ) + }) + +/** + * @typedef {import('./types').PendingChannelsRes} PendingChannelsRes + */ + +/** + * @returns {Promise} + */ +const pendingChannels = () => + Common.makePromise((res, rej) => { + const { lightning } = lightningServices.getServices() + + lightning.pendingChannels({}, (err, resp) => { + if (err) { + rej(new Error(err.message)) + } else { + // Needs cast because typescript refuses to assign Record + // to an actual object :shrugs + res(/** @type {PendingChannelsRes} */ (resp)) + } + }) + }) + +/** + * @typedef {import('./types').AddInvoiceRes} AddInvoiceRes + */ +/** + * https://api.lightning.community/#addinvoice + * @param {number} value + * @param {string=} memo + * @param {boolean=} confidential Alias for `private`. + * @param {number=} expiry + * @returns {Promise} + */ +const addInvoice = (value, memo = '', confidential = true, expiry = 180) => + Common.makePromise((res, rej) => { + const { lightning } = lightningServices.getServices() + + lightning.addInvoice( + { + value, + memo, + private: confidential, + expiry + }, + (err, resp) => { + if (err) { + rej(new Error(err.message)) + } else { + // Needs cast because typescript refuses to assign Record + // to an actual object :shrugs + res(/** @type {AddInvoiceRes} */ (resp)) + } + } + ) + }) + module.exports = { sendPaymentV2Keysend, sendPaymentV2Invoice, listPayments, - decodePayReq + decodePayReq, + newAddress, + listUnspent, + listChannels, + getChanInfo, + listPeers, + pendingChannels, + addInvoice }