diff --git a/README.md b/README.md index 5e2b971..f4da16c 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,8 @@ The below run time configuration are available, which can be configured either v - Port - Port for serving the APIs - Documentation port - Port for serving the swagger documentation - Protocol - Http protocol for communication with the REST server. Two options are supported `https` and `http`(unencrypted and insecure communication between API server and the app) -- Execution mode - Control for more detailed log info +- Execution mode - Control for more detailed log info. The options supported are `test` and `production` +- Lightning-RPC Path - Configure the path where `lightning-rpc` file is located. It will default to standard lightning path if not configured - RPC Command - - Enable additional RPC commands for `/rpc` endpoint #### Option 1: Via Config file `cl-rest-config.json` @@ -41,6 +42,7 @@ For running the server, rename the file `sample-cl-rest-config.json` to `cl-rest - DOCPORT (Default: `4001`) - PROTOCOL (Default: `https`) - EXECMODE (Default: `production`) +- LNRPCPATH (Default: ` `) - RPCCOMMANDS (Default: `["*"]`) #### Option 2: With the plugin configuration, if used as a plugin @@ -49,6 +51,7 @@ If running as a plugin, configure the below options in your c-lightning `config` - `rest-docport` - `rest-protocol` - `rest-execmode` +- `rest-lnrpcpath` - `rest-rpc` Defaults are the same as in option # 1 with the exception that `rest-rpc` is a comma separated string. @@ -153,6 +156,7 @@ C-Lightning commands covered by this API suite is [here](docs/CLTCommandCoverage - getfees (/v1/getFees) - `GET`: Returns the routing fee collected by the node - signmessage (/v1/utility/signMessage) - `POST`: Creates a digital signature of the message using node's secret key - verifymessage(/v1/utility/verifyMessage) - `GET`: Verifies a signature and the pubkey of the node which signed the message +- decode (/v1/utility/decode) - `GET`: Decodes various invoice strings including BOLT12 ### On-Chain fund management - newaddr (/v1/newaddr) - `GET`: Generate address for recieving on-chain funds - withdraw (/v1/withdraw) - `POST`: Withdraw on-chain funds to an address @@ -185,6 +189,12 @@ C-Lightning commands covered by this API suite is [here](docs/CLTCommandCoverage - feerates (/v1/network/feeRates) - `GET`: Lookup fee rates on the network, for a given rate style (`perkb` or `perkw`) - estimatefees (/v1/network/estimateFees) - `GET`: Get the urgent, normal and slow Bitcoin feerates as sat/kVB. +### Offers +- offer (/v1/offers/offer) - `POST`: Create an offer +- listoffers (/v1/offers/listOffers) - `GET`: Returns a list of all the offers on the node +- fetchinvoice (/v1/offers/fetchInvoice) - `POST`: Fetch an invoice for an offer +- disableoffer (v1/offers/disableOffer) - `DEL`: Disables an existing offer, so that it cannot be used for future payments + ### RPC - rpc (/v1/rpc) - `POST`: additional access to RPC comands. Examples of body param for rpc: #### No param diff --git a/app.js b/app.js index 3dd7872..94f7862 100644 --- a/app.js +++ b/app.js @@ -1,34 +1,53 @@ const app = require('express')(); const bodyparser = require('body-parser'); +let configFile = './cl-rest-config.json'; +fs = require('fs'); + +function prepDataForLogging(msg) { + return typeof msg === 'string' ? msg : JSON.stringify(msg) +} + +function configLogger(config) { + return { + log(msg) { if (config.EXECMODE === 'test') config.PLUGIN.log(prepDataForLogging(msg), "info") }, + warn(msg) { config.PLUGIN.log(prepDataForLogging(msg), "warn") }, + error(msg) { config.PLUGIN.log(prepDataForLogging(msg), "error") } + } +} if (typeof global.REST_PLUGIN_CONFIG === 'undefined') { - global.logger = console + //Read config file when not running as a plugin + console.log("Reading config file"); + let rawconfig = fs.readFileSync(configFile, function (err) { + if (err) { + console.warn("Failed to read config key"); + console.error(error); + process.exit(1); + } + }); + global.config = JSON.parse(rawconfig); + global.config.PLUGIN = console; } else { - function pluginMsg(msg) { - return typeof msg === 'string' ? msg : JSON.stringify(msg) - } - global.logger = { - log(msg) {global.REST_PLUGIN_CONFIG.PLUGIN.log(pluginMsg(msg), "info")}, - warn(msg) {global.REST_PLUGIN_CONFIG.PLUGIN.log(pluginMsg(msg), "warn")}, - error(msg) {global.REST_PLUGIN_CONFIG.PLUGIN.log(pluginMsg(msg), "error")} - } + global.config = global.REST_PLUGIN_CONFIG; } +global.logger = configLogger(global.config); + //LN_PATH is the path containing lightning-rpc file -global.ln = require('./lightning-client-js')(process.env.LN_PATH); +global.ln = require('./lightning-client-js')(global.config.LNRPCPATH.trim() || process.env.LN_PATH); app.use(bodyparser.json()); -app.use(bodyparser.urlencoded({extended: false})); +app.use(bodyparser.urlencoded({ extended: false })); app.use((req, res, next) => { - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader( - "Access-Control-Allow-Headers", - "Origin, X-Requested-With, Content-Type, Accept, Authorization, filePath, macaroon, encodingtype" - ); - res.setHeader( - "Access-Control-Allow-Methods", - "GET, POST, PATCH, PUT, DELETE, OPTIONS" - ); - next(); + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader( + "Access-Control-Allow-Headers", + "Origin, X-Requested-With, Content-Type, Accept, Authorization, filePath, macaroon, encodingtype" + ); + res.setHeader( + "Access-Control-Allow-Methods", + "GET, POST, PATCH, PUT, DELETE, OPTIONS" + ); + next(); }); //Use declared routes here @@ -45,5 +64,6 @@ app.use('/v1/pay', require('./routes/payments')); app.use('/v1/invoice', require('./routes/invoice')); app.use('/v1/network', require('./routes/network')); app.use('/v1/rpc', require('./routes/rpc')); +app.use('/v1/offers', require('./routes/offers')); module.exports = app; \ No newline at end of file diff --git a/cl-rest.js b/cl-rest.js index 42e9bed..905a021 100644 --- a/cl-rest.js +++ b/cl-rest.js @@ -2,9 +2,6 @@ const app = require('./app'); const docapp = require('./docapp'); const mcrn = require('./utils/bakeMacaroons'); fs = require( 'fs' ); -var PORT = 3001; -var DOCPORT = 4001; -var EXECMODE = "production"; const { execSync } = require( 'child_process' ); const execOptions = { encoding: 'utf-8', windowsHide: true }; @@ -17,24 +14,8 @@ let key = './certs/key.pem'; let certificate = './certs/certificate.pem'; let macaroonFile = './certs/access.macaroon'; let rootKey = './certs/rootKey.key'; -let configFile = './cl-rest-config.json'; -if (typeof global.REST_PLUGIN_CONFIG === 'undefined') { - //Read config file when not running as a plugin - global.logger.log("Reading config file"); - let rawconfig = fs.readFileSync (configFile, function (err){ - if (err) - { - global.logger.warn("Failed to read config key"); - global.logger.error( error ); - process.exit(1); - } - }); - global.config = JSON.parse(rawconfig); -} else { - global.config = global.REST_PLUGIN_CONFIG -} -global.logger.log('--- Starting the cl-rest server ---'); +global.logger.warn('--- Starting the cl-rest server ---'); if (!config.PORT || !config.DOCPORT || !config.PROTOCOL || !config.EXECMODE) { @@ -43,9 +24,9 @@ if (!config.PORT || !config.DOCPORT || !config.PROTOCOL || !config.EXECMODE) } //Set config params -PORT = config.PORT; -EXECMODE = config.EXECMODE; -DOCPORT = config.DOCPORT; +const PORT = config.PORT; +const EXECMODE = config.EXECMODE; +const DOCPORT = config.DOCPORT; //Create certs folder try { @@ -144,12 +125,12 @@ docserver = require( 'http' ).createServer( docapp ); //Start the server server.listen(PORT, function() { - global.logger.log('--- cl-rest api server is ready and listening on port: ' + PORT + ' ---'); + global.logger.warn('--- cl-rest api server is ready and listening on port: ' + PORT + ' ---'); }) //Start the docserver docserver.listen(DOCPORT, function() { - global.logger.log('--- cl-rest doc server is ready and listening on port: ' + DOCPORT + ' ---'); + global.logger.warn('--- cl-rest doc server is ready and listening on port: ' + DOCPORT + ' ---'); }) exports.closeServer = function(){ diff --git a/controllers/channel.js b/controllers/channel.js index e2a285a..02fe223 100644 --- a/controllers/channel.js +++ b/controllers/channel.js @@ -11,6 +11,7 @@ * - Channel Management * name: fundchannel * summary: Opens channel with a network peer +* description: Core documentation - https://lightning.readthedocs.io/lightning-fundchannel.7.html * consumes: * - application/json * parameters: @@ -105,6 +106,7 @@ exports.openChannel = (req,res) => { * - Channel Management * name: listchannel * summary: Returns a list of channels on the node +* description: Core documentation - https://lightning.readthedocs.io/lightning-listchannels.7.html * responses: * 200: * description: An array of channels is returned @@ -225,6 +227,7 @@ exports.listChannels = (req,res) => { * - Channel Management * name: setchannelfee * summary: Update channel fee policy +* description: Core documentation - https://lightning.readthedocs.io/lightning-setchannelfee.7.html * parameters: * - in: body * name: id @@ -299,6 +302,7 @@ exports.setChannelFee = (req,res) => { * - Channel Management * name: close * summary: Close an existing channel with a peer +* description: Core documentation - https://lightning.readthedocs.io/lightning-close.7.html * parameters: * - in: route * name: id @@ -345,17 +349,14 @@ exports.closeChannel = (req,res) => { var id = req.params.id; //optional params - if(req.query['unilateralTimeout']) - var unilaterlaltimeout = req.query['unilateralTimeout']; - else - var unilaterlaltimeout = 0; - var dest = (req.query['dest']) ? req.query['dest'] : null; - var feeNegStep = (req.query['feeNegotiationStep']) ? req.query['feeNegotiationStep'] : null; + var unltrltmt = (req.query.unilateralTimeout) ? req.query.unilateralTimeout : null; + var dstntn = (req.query.dest) ? req.query.dest : null; + var feeNegStep = (req.query.feeNegotiationStep) ? req.query.feeNegotiationStep : null; //Call the close command with the params ln.close(id=id, - unilaterlaltimeout=unilaterlaltimeout, - destination=dest, + unilaterlaltimeout=unltrltmt, + destination=dstntn, fee_negotiation_step=feeNegStep).then(data => { global.logger.log('closeChannel success'); res.status(202).json(data); @@ -368,7 +369,7 @@ exports.closeChannel = (req,res) => { //Function # 5 //Invoke the 'listforwards' command to list the forwarded htlcs -//Arguments - None +//Arguments - status (optional), inChannel (optional), outChannel (optional) /** * @swagger * /channel/listForwards: @@ -377,36 +378,51 @@ exports.closeChannel = (req,res) => { * - Channel Management * name: listforwards * summary: Fetch the list of the forwarded htlcs +* description: Core Documentation - https://lightning.readthedocs.io/lightning-listforwards.7.html +* parameters: +* - in: query +* name: status +* description: status can be either "offered" or "settled" or "failed" or "local_failed" +* type: string * responses: * 200: -* description: channel closed successfully +* description: List of forwarded htlcs are returned per the params specified * schema: * type: object * properties: * in_channel: * type: string * description: in_channel -* out_channel: -* type: string -* description: out_channel -* in_msatoshi: -* type: string -* description: in_msatoshi * in_msat: * type: string * description: in_msat -* out_msatoshi: +* status: * type: string -* description: out_msatoshi -* out_msat: +* description: one of "offered", "settled", "local_failed", "failed" +* received_time: +* type: string +* description: the UNIX timestamp when this was received +* out_channel: * type: string -* description: out_msat -* fee: +* description: the channel that the HTLC was forwarded to +* payment_hash: * type: string -* description: fee +* description: payment hash sought by HTLC (always 64 characters) * fee_msat: * type: string -* description: fee_msat +* description: If out_channel is present, the amount this paid in fees +* out_msat: +* type: string +* description: If out_channel is present, the amount we sent out the out_channel +* resolved_time: +* type: string +* description: If status is "settled" or "failed", the UNIX timestamp when this was resolved +* failcode: +* type: string +* description: If status is "local_failed" or "failed", the numeric onion code returned +* failreason: +* type: string +* description: If status is "local_failed" or "failed", the name of the onion code returned * 500: * description: Server error */ @@ -417,7 +433,151 @@ exports.listForwards = (req,res) => { //Call the listforwards command ln.listforwards().then(data => { global.logger.log('listforwards success'); - res.status(200).json(data.forwards); + if(req.query.status) { + if(data.forwards.length === 0) + res.status(200).json(data.forwards); + else { + let filteredForwards = data.forwards.filter(function (currentElement){ + return currentElement.status === req.query.status; + }); + res.status(200).json(filteredForwards); + } + } + else + res.status(200).json(data.forwards); + }).catch(err => { + global.logger.warn(err); + res.status(500).json({error: err}); + }); + ln.removeListener('error', connFailed); +} + +//Function # 6 +//Invoke the 'listForwardsFilter' command to list the forwarded htlcs +//Arguments - reverse (optional), offset (optional), maxLen (optional) +/** +* @swagger +* /channel/listForwardsFilter: +* get: +* tags: +* - Channel Management +* name: listForwardFilter +* summary: Fetch the paginated list of the forwarded htlcs +* description: Core Documentation - https://lightning.readthedocs.io/lightning-listforwards.7.html +* parameters: +* - in: query +* name: reverse +* description: if true offset is from the end, else from the start +* type: boolean +* - in: query +* name: offset +* description: amount of forwards you want to skip from the list, from start if reverse is false, from end if reverse is true. +* type: integer +* - in: query +* name: maxLen +* description: maximum range after the offset you want to forward. +* type: integer +* responses: +* 200: +* description: An object is returned with index values and an array of forwards +* schema: +* type: object +* properties: +* firstIndexOffset: +* type: integer +* description: starting index of the subarray +* lastIndexOffset: +* type: integer +* description: last index of the subarray +* listForwards: +* type: object +* description: forwarded htlcs +* properties: +* in_channel: +* type: string +* description: the channel that received the HTLC +* in_msat: +* type: string +* description: the value of the incoming HTLC +* status: +* type: string +* description: still ongoing, completed, failed locally, or failed after forwarding +* received_time: +* type: string +* description: the UNIX timestamp when this was received +* out_channel: +* type: string +* description: the channel that the HTLC was forwarded to +* payment_hash: +* type: string +* description: payment hash sought by HTLC (always 64 characters) +* fee_msat: +* type: string +* description: If out_channel is present, the amount this paid in fees +* out_msat: +* type: string +* description: If out_channel is present, the amount we sent out the out_channel +* resolved_time: +* type: string +* description: If status is "settled" or "failed", the UNIX timestamp when this was resolved +* failcode: +* type: string +* description: If status is "local_failed" or "failed", the numeric onion code returned +* failreason: +* type: string +* description: If status is "local_failed" or "failed", the name of the onion code returned +* 500: +* description: Server error +*/ +exports.listForwardsFilter = (req,res) => { + function connFailed(err) { throw err } + ln.on('error', connFailed); + var {offset, maxLen, reverse} = req.query + + //Call the listforwards command + ln.listforwards().then(data => { + var forwards = data.forwards + if(!offset) { + offset = 0; + } + offset = parseInt(offset) + //below 2 lines will readjust the offset inside range incase they went out of it + offset = Math.max(offset, 0) + offset = Math.min(Math.max(forwards.length - 1, 0), offset) + if(!maxLen) { + maxLen = forwards.length - offset + } + maxLen = parseInt(maxLen) + // since length is a scalar quantity it will throw error if maxLen is negative + if(maxLen<0) { + throw Error ('maximum length cannot be negative') + } + if(!reverse) { + reverse = false + } + reverse = !(reverse === 'false' || reverse === false) + //below logic will adjust last index inside the range incase they went out + var lastIndex = 0 + var firstIndex = 0 + var fill = [] + if(reverse === true && forwards.length !== 0) { + if(offset === 0) + offset = forwards.length - offset; + lastIndex = offset - 1; + firstIndex = Math.max(0, offset-maxLen); + for(var i=lastIndex; i>=firstIndex; i--) { + fill.push(forwards[i]) + } + } else if(reverse === false && forwards.length !== 0) { + firstIndex = (offset === 0) ? offset : (offset + 1); + lastIndex = Math.min(forwards.length - 1, firstIndex+(maxLen-1)); + for(var i=lastIndex; i>=firstIndex; i--) { + fill.push(forwards[i]) + } + } + global.logger.log('listforwards success'); + var response = {firstIndexOffset:firstIndex, lastIndexOffset:lastIndex, listForwards:fill } + res.status(200).json(response); }).catch(err => { global.logger.warn(err); res.status(500).json({error: err}); diff --git a/controllers/getinfo.js b/controllers/getinfo.js index 0d28e79..e7526cb 100644 --- a/controllers/getinfo.js +++ b/controllers/getinfo.js @@ -25,7 +25,6 @@ exports.getinfoRtl = (req,res) => { chains: [{chain:'bitcoin', network: data.network}], version: data.version }; - global.logger.log(getinfodata); global.logger.log('getinfoRtl success'); res.status(200).json(getinfodata); }).catch(err => { @@ -127,7 +126,6 @@ exports.getinfo = (req,res) => { //Call the getinfo command ln.getinfo().then(data => { data.api_version = require('../package.json').version; - global.logger.log(data); global.logger.log('getinfo success'); res.status(200).json(data); }).catch(err => { @@ -159,7 +157,7 @@ exports.getinfo = (req,res) => { * - message * responses: * 201: -* description: OK +* description: Read more on https://lightning.readthedocs.io/lightning-signmessage.7.html * schema: * type: object * properties: @@ -181,7 +179,6 @@ exports.signMessage = (req,res) => { //Call the signmessage command ln.signmessage(req.body.message).then(data => { - global.logger.log(data); global.logger.log('signmessage success'); res.status(201).json(data); }).catch(err => { @@ -219,7 +216,7 @@ exports.signMessage = (req,res) => { * - zbase * responses: * 200: -* description: OK +* description: Read more on https://lightning.readthedocs.io/lightning-checkmessage.7.html * schema: * type: object * properties: @@ -238,7 +235,6 @@ exports.checkMessage = (req,res) => { //Call the checkmessage command ln.checkmessage(req.params.message, req.params.zbase).then(data => { - global.logger.log(data); global.logger.log('checkmessage success'); res.status(200).json(data); }).catch(err => { @@ -246,4 +242,47 @@ exports.checkMessage = (req,res) => { res.status(500).json({error: err}); }); ln.removeListener('error', connFailed); +} + +//Function # 5 +//Invoke the 'decode' command for decoding various invoice strings +//Arguments - invoice [required] +/** +* @swagger +* /utility/decode: +* get: +* tags: +* - General Information +* name: decode +* summary: Command for decoding an invoice string +* consumes: +* - application/json +* parameters: +* - in: route +* name: invoiceString +* description: bolt11 or bolt12 string +* type: string +* required: +* - invoiceString +* responses: +* 200: +* description: Read more on https://lightning.readthedocs.io/lightning-decode.7.html +* schema: +* type: object +* 500: +* description: Server error +*/ +exports.decode = (req,res) => { + function connFailed(err) { throw err } + ln.on('error', connFailed); + + //Call the checkmessage command + ln.decode(req.params.invoiceString).then(data => { + global.logger.log('decode success'); + res.status(200).json(data); + }).catch(err => { + global.logger.warn(err); + res.status(500).json({error: err}); + }); + ln.removeListener('error', connFailed); } \ No newline at end of file diff --git a/controllers/invoice.js b/controllers/invoice.js index a5b1e20..f59d333 100644 --- a/controllers/invoice.js +++ b/controllers/invoice.js @@ -281,5 +281,83 @@ exports.delInvoice = (req,res) => { res.status(500).json({error: err}); }); + ln.removeListener('error', connFailed); +} + +//Function # 4 +//Invoke the 'waitinvoice' command for waiting for specific payment +//Arguments - label (reqiured) +/** +* @swagger +* /invoice/waitInvoice: +* get: +* tags: +* - Invoice +* name: waitinvoice +* summary: Waits until a specific invoice is paid, then returns that single entry as per listinvoice +* parameters: +* - in: route +* name: label +* description: The unique label of the invoice +* type: string +* required: +* - label +* responses: +* 200: +* description: On success, an object is returned +* schema: +* type: object +* properties: +* label: +* type: string +* description: unique label supplied at invoice creation +* description: +* type: string +* description: description used in the invoice +* payment_hash: +* type: string +* description: the hash of the payment_preimage which will prove payment (always 64 characters) +* status: +* type: string +* description: Whether it's paid or expired (one of "paid", "expired") +* expires_at: +* type: string +* description: UNIX timestamp of when it will become / became unpayable +* amount_msat: +* type: string +* description: the amount required to pay this invoice +* bolt11: +* type: string +* description: the BOLT11 string (always present unless bolt12 is) +* bolt12: +* type: string +* description: the BOLT12 string (always present unless bolt11 is) +* pay_index: +* type: string +* description: If status is "paid", unique incrementing index for this payment +* amount_received_msat: +* type: string +* description: If status is "paid", the amount actually received +* paid_at: +* type: string +* description: If status is "paid", UNIX timestamp of when it was paid +* payment_preimage: +* type: string +* description: If status is "paid", proof of payment (always 64 characters) +* 500: +* description: Server error +*/ +exports.waitInvoice = (req,res) => { + function connFailed(err) { throw err } + ln.on('error', connFailed); + + ln.waitinvoice(req.params.label).then(data => { + global.logger.log('waitInvoice successful'); + res.status(200).json(data); + }).catch(err => { + global.logger.warn(err); + res.status(500).json({error: err}); + }); + ln.removeListener('error', connFailed); } \ No newline at end of file diff --git a/controllers/offers.js b/controllers/offers.js new file mode 100644 index 0000000..3fcb15d --- /dev/null +++ b/controllers/offers.js @@ -0,0 +1,414 @@ +//This controller houses all the offers functions + +//Function # 1 +//Invoke the 'offer' command to setup an offer +//Arguments - Amount (required), Description (required) +/** +* @swagger +* /offers/offer: +* post: +* tags: +* - Offers +* name: offer +* summary: Creates an offer +* description: Core documentation - https://lightning.readthedocs.io/lightning-offer.7.html +* consumes: +* - application/json +* parameters: +* - in: body +* name: amount +* description: Specify the amount as 'any' or 'sats'. E.g. '75sats' +* type: string +* required: +* - amount +* - in: body +* name: description +* description: Description of the offer, to be included on the invoice +* type: string +* required: +* - description +* - in: body +* name: vendor +* description: Reflects who is issuing this offer +* type: string +* - in: body +* name: label +* description: Internal-use name for the offer, which can be any UTF-8 string +* type: string +* - in: body +* name: quantity_min +* description: The presence of quantity_min or quantity_max indicates that the invoice can specify more than one of the items within this (inclusive) range +* type: number +* - in: body +* name: quantity_max +* description: The presence of quantity_min or quantity_max indicates that the invoice can specify more than one of the items within this (inclusive) range +* type: number +* - in: body +* name: absolute_expiry +* description: The time the offer is valid until, in seconds since the first day of 1970 UTC +* type: string +* - in: body +* name: recurrence +* description: Means invoice is expected at regular intervals. The argument is a positive number followed by one of "seconds", "minutes", "hours", "days", "weeks", "months" or "years" e.g. "2weeks". +* type: string +* - in: body +* name: recurrence_base +* description: Time in seconds since the first day of 1970 UTC. This indicates when the first period begins. The "@" prefix means that the invoice must start by paying the first period e.g. "@1609459200" +* type: string +* - in: body +* name: recurrence_paywindow +* description: Optional argument of form start of a period in which an invoice and payment is valid +* type: string +* - in: body +* name: recurrence_limit +* description: Optional argument to indicate the maximum period which exists for recurrence e.g. "12" means there are 13 periods of recurrence +* type: string +* - in: body +* name: single_use +* description: Indicates that the offer is only valid once +* type: boolean +* responses: +* 201: +* description: An offers object is returned +* schema: +* type: object +* properties: +* offer_id: +* type: string +* description: The hash of the offer +* active: +* type: string +* description: true if the offer is active +* single_use: +* type: boolean +* description: true if single use was specified for the offer +* bolt12: +* type: string +* description: The bolt12 offer, starting with "lno1" +* bolt12_unsigned: +* type: string +* description: The bolt12 encoding of the offer, without a signature +* used: +* type: boolean +* description: true if an associated invoice has been paid +* created: +* type: boolean +* description: false if the offer already existed +* label: +* type: string +* description: the (optional) user-specified label +* 500: +* description: Server error +*/ +exports.offer = (req,res) => { + global.logger.log('offer creation initiated...'); + + function connFailed(err) { throw err } + ln.on('error', connFailed); + //Set required params + var amnt = req.body.amount; + var desc = req.body.description; + //Set optional params + var vndr = (req.body.vendor) ? req.body.vendor : null; + var lbl = (req.body.label) ? req.body.lable : null; + var qty_min = (req.body.quantity_min) ? req.body.quantity_min : null; + var qty_max = (req.body.quantity_max) ? req.body.quantity_max : null; + var abs_expry = (req.body.absolute_expiry) ? req.body.absolute_expiry : null; + var rcrnc = (req.body.recurrence) ? req.body.recurrence : null; + var rcrnc_base = (req.body.recurrence_base) ? req.body.recurrence_base : null; + var rcrnc_wndw = (req.body.recurrence_paywindow) ? req.body.recurrence_paywindow : null; + var rcrnc_lmt = (req.body.recurrence_limit) ? req.body.recurrence_limit : null; + var sngl_use = (req.body.single_use === '0' || req.body.single_use === 'false' || !req.body.single_use) ? false : true; + + //Call the fundchannel command with the pub key and amount specified + ln.offer(amount=amnt, + description=desc, + vendor=vndr, + label=lbl, + quantity_min=qty_min, + quantity_max=qty_max, + absolute_expiry=abs_expry, + recurrence=rcrnc, + recurrence_base=rcrnc_base, + recurrence_paywindow=rcrnc_wndw, + recurrence_limit=rcrnc_lmt, + single_use=sngl_use + ).then(data => { + global.logger.log('offer creation success'); + res.status(201).json(data); + }).catch(err => { + global.logger.warn(err); + res.status(500).json({error: err}); + }); + ln.removeListener('error', connFailed); +} + +//Function # 2 +//Invoke the 'listoffers' command get the list of offers for the node +//Arguments - No arguments +/** +* @swagger +* /channel/listOffers: +* get: +* tags: +* - Offers +* name: listoffers +* summary: Returns a list of offers on the node +* description: Core documentation - https://lightning.readthedocs.io/lightning-listoffers.7.html +* parameters: +* - in: query +* name: offer_id +* description: List offer with only the offer with offer_id (if it exists) +* type: string +* - in: query +* name: active_only +* description: If specified, only active offers are returned +* type: string +* responses: +* 200: +* description: An array of offers is returned +* schema: +* type: object +* properties: +* offer_id: +* type: string +* description: The hash of the offer +* active: +* type: boolean +* description: true if the offer is active +* single_use: +* type: boolean +* description: true if single use was specified for the offer +* bolt12: +* type: string +* description: The bolt12 offer, starting with "lno1" +* bolt12_unsigned: +* type: string +* description: The bolt12 encoding of the offer, without signature +* used: +* type: boolean +* description: true if an associated invoice has been paid +* label: +* type: string +* description: The optional user specified label +* 500: +* description: Server error +*/ +exports.listOffers = (req,res) => { + function connFailed(err) { throw err } + ln.on('error', connFailed); + + //Set optional params + var offrid = (req.query.offer_id) ? req.query.offer_id : null; + var actvonly = (req.query.active_only === '0' || req.query.active_only === 'false' || !req.query.active_only) ? false : true; + + //Call the listforwards command + ln.listoffers( + offer_id=offrid, + active_only=actvonly + ).then(data => { + global.logger.log('listOffers success'); + res.status(200).json(data); + }).catch(err => { + global.logger.warn(err); + res.status(500).json({error: err}); + }); + ln.removeListener('error', connFailed); +} + +//This controller houses all the offers functions + +//Function # 3 +//Invoke the 'fetchinvoice' command to fetch an invoice for an offer +//Arguments - Offer (required), Amount (optional) +/** +* @swagger +* /offers/fetchInvoice: +* post: +* tags: +* - Offers +* name: fetchInvoice +* summary: Fetch an invoice for an offer +* description: Core documentation - https://lightning.readthedocs.io/lightning-fetchinvoice.7.html +* consumes: +* - application/json +* parameters: +* - in: body +* name: offer +* description: Bolt12 offer string beginning with "lno1" +* type: string +* required: +* - offer +* - in: body +* name: msatoshi +* description: Required only if the offer does not specify an amount at all +* type: string +* - in: body +* name: quantity +* description: Required if the offer specifies quantity_min or quantity_max, otherwise it is not allowed +* type: string +* - in: body +* name: recurrence_counter +* description: Required if the offer specifies recurrence, otherwise it is not allowed +* type: integer +* - in: body +* name: recurrence_start +* description: Required if the offer specifies recurrence_base with start_any_period set, otherwise it is not allowed +* type: integer +* - in: body +* name: recurrence_label +* description: Required if recurrence_counter is set, and otherwise is not allowed +* type: string +* - in: body +* name: timeout +* description: Optional timeout; if we don't get a reply before this we fail +* type: string +* default: 60 seconds +* responses: +* 201: +* description: On success, an object is returned +* schema: +* type: object +* properties: +* invoice: +* type: string +* description: The bolt12-encoded invoice string, starting with "lni1" +* changes: +* type: object +* description: Summary of changes from offer +* properties: +* description_appended: +* type: string +* description: extra characters appended to the description field +* description: +* type: string +* description: A completely replaced description field +* vendor_removed: +* type: string +* description: The vendor from the offer, which is missing in the invoice +* vendor: +* type: string +* description: A completely replaced vendor field +* msat: +* type: string +* description: The amount, if different from the offer amount multiplied by any quantity +* next_period: +* type: object +* description: Only for recurring invoices if the next period is under the recurrence_limit +* properties: +* counter: +* type: number +* description: the index of the next period to fetchinvoice +* starttime: +* type: number +* description: UNIX timestamp that the next period starts +* endtime: +* type: number +* description: UNIX timestamp that the next period ends +* paywindow_start: +* type: number +* description: UNIX timestamp of the earliest time that the next invoice can be fetched +* paywindow_end: +* type: number +* description: UNIX timestamp of the latest time that the next invoice can be fetched +* 500: +* description: Server error +*/ +exports.fetchInvoice = (req,res) => { + global.logger.log('fetch invoice initiated...'); + + function connFailed(err) { throw err } + ln.on('error', connFailed); + //Set required params + var offr = req.body.offer; + //Set optional params + var msats = (req.body.msatoshi) ? req.body.msatoshi : null; + var qty = (req.body.quantity) ? req.body.quantity: null; + var rcrnc_cntr = (req.body.recurrence_counter) ? req.body.recurrence_counter: null; + var rcrnc_strt = (req.body.recurrence_start) ? req.body.recurrence_start: null; + var rcrnc_lbl = (req.body.recurrence_label) ? req.body.recurrence_label: null; + var tmt = (req.body.timeout) ? req.body.timeout: null; + + //Call the fetchinvoice command with the offer and amount if specified + ln.fetchinvoice(offer=offr, + msatoshi=msats, + quantity=qty, + recurrence_counter=rcrnc_cntr, + recurrence_start=rcrnc_strt, + recurrence_label=rcrnc_lbl, + timeout=tmt + ).then(data => { + global.logger.log('fetch invoice creation success'); + res.status(201).json(data); + }).catch(err => { + global.logger.warn(err); + res.status(500).json({error: err}); + }); + ln.removeListener('error', connFailed); +} + +//Function # 4 +//Invoke the 'disableoffer' command to disable an offer +//Arguments - Offer id (required) +/** +* @swagger +* /offers/disableOffer: +* delete: +* tags: +* - Offers +* name: disableoffer +* summary: Disable an existing offer +* description: Core documentation - https://lightning.readthedocs.io/lightning-disableoffer.7.html +* parameters: +* - in: route +* name: offerid +* description: Offer ID +* type: string +* required: +* - offerid +* responses: +* 202: +* description: Offer disabled successfully +* schema: +* type: object +* properties: +* offer_id: +* type: string +* description: The merkle hash of the offer (always 64 characters) +* active: +* type: boolean +* description: Whether the offer can produce invoices/payments (always false) +* single_use: +* type: boolean +* description: Whether the offer is disabled after first successful use +* bolt12: +* type: string +* description: The bolt12 string representing this offer +* bolt12_unsigned: +* type: string +* description: The bolt12 string representing this offer, without signature +* used: +* type: boolean +* description: Whether the offer has had an invoice paid / payment made +* label: +* type: string +* description: The label provided when offer was created (optional) +* 500: +* description: Server error +*/ +exports.disableOffer = (req,res) => { + global.logger.log('disableOffer initiated...'); + + function connFailed(err) { throw err } + ln.on('error', connFailed); + + //Call the close command with the params + ln.disableoffer(offer_id=req.params.offerid).then(data => { + global.logger.log('disableOffer success'); + res.status(202).json(data); + }).catch(err => { + global.logger.warn(err); + res.status(500).json({error: err}); + }); + ln.removeListener('error', connFailed); +} \ No newline at end of file diff --git a/docapp.js b/docapp.js index 65b34be..8e9b9d7 100644 --- a/docapp.js +++ b/docapp.js @@ -2,28 +2,11 @@ const docapp = require('express')(); var swaggerJSDoc = require('swagger-jsdoc'); var swaggerUi = require('swagger-ui-express'); fs = require( 'fs' ); -let configFile = './cl-rest-config.json'; api_version = require('./package.json').version; const cdir = process.env.CL_REST_STATE_DIR ? process.env.CL_REST_STATE_DIR : __dirname; process.chdir(cdir); -if (typeof global.REST_PLUGIN_CONFIG === 'undefined') { - //Read config file when not running as a plugin - global.logger.log("Reading config file"); - let rawconfig = fs.readFileSync (configFile, function (err){ - if (err) - { - global.logger.warn("Failed to read config key"); - global.logger.error( error ); - process.exit(1); - } - }); - global.config = JSON.parse(rawconfig); -} else { - global.config = global.REST_PLUGIN_CONFIG -} - var hostdef = 'localhost:' + config.PORT; var swaggerDefinition = { diff --git a/lightning-client-js.js b/lightning-client-js.js index 10f9518..f161876 100644 --- a/lightning-client-js.js +++ b/lightning-client-js.js @@ -19,33 +19,35 @@ let somedata = ''; class LightningClient extends EventEmitter { constructor(rpcPath=defaultRpcPath) { + global.logger.log("rpcPath -> " + rpcPath); + if (!path.isAbsolute(rpcPath)) { throw new Error('The rpcPath must be an absolute path'); } if (!fExists(rpcPath) || !fStat(rpcPath).isSocket()){ - // network directory provided, use the lightning-rpc within in + // network directory provided, use the lightning-rpc within it if (fExists(rpcPath, 'lightning-rpc')) { rpcPath = path.join(rpcPath, 'lightning-rpc'); } // main data directory provided, default to using the bitcoin mainnet subdirectory else if (fExists(rpcPath, 'bitcoin', 'lightning-rpc')) { - console.error(`WARN: ${rpcPath}/lightning-rpc is missing, using the bitcoin mainnet subdirectory at ${rpcPath}/bitcoin instead.`) + global.logger.error(`WARN: ${rpcPath}/lightning-rpc is missing, using the bitcoin mainnet subdirectory at ${rpcPath}/bitcoin instead.`) rpcPath = path.join(rpcPath, 'bitcoin', 'lightning-rpc') } // or using the bitcoin testnet subdirectory else if (fExists(rpcPath, 'testnet', 'lightning-rpc')) { - console.error(`WARN: ${rpcPath}/lightning-rpc is missing, using the bitcoin testnet subdirectory at ${rpcPath}/testnet instead.`) + global.logger.error(`WARN: ${rpcPath}/lightning-rpc is missing, using the bitcoin testnet subdirectory at ${rpcPath}/testnet instead.`) rpcPath = path.join(rpcPath, 'testnet', 'lightning-rpc') } // or using the bitcoin signet subdirectory else if (fExists(rpcPath, 'signet', 'lightning-rpc')) { - console.error(`WARN: ${rpcPath}/lightning-rpc is missing, using the bitcoin signet subdirectory at ${rpcPath}/signet instead.`) + global.logger.error(`WARN: ${rpcPath}/lightning-rpc is missing, using the bitcoin signet subdirectory at ${rpcPath}/signet instead.`) rpcPath = path.join(rpcPath, 'signet', 'lightning-rpc') } } - debug(`Connecting to ${rpcPath}`); + global.logger.log(`Connecting to ${rpcPath}`); super(); this.rpcPath = rpcPath; diff --git a/methods.json b/methods.json index fa423c2..a67e8ea 100644 --- a/methods.json +++ b/methods.json @@ -50,5 +50,10 @@ "signmessage", "checkmessage", "keysend", - "estimatefees" + "estimatefees", + "decode", + "offer", + "listoffers", + "fetchinvoice", + "disableoffer" ] \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a282114..70baa72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "c-lightning-rest", - "version": "0.4.4", + "version": "0.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "c-lightning-rest", - "version": "0.4.4", + "version": "0.5.0", "license": "MIT", "dependencies": { "atob": "^2.1.2", @@ -1267,9 +1267,9 @@ } }, "node_modules/lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "node_modules/lodash.get": { @@ -3429,9 +3429,9 @@ } }, "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "lodash.get": { diff --git a/package.json b/package.json index 09a3766..d08765c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "c-lightning-rest", - "version": "0.4.4", + "version": "0.5.0", "description": "c-lightning REST API suite", "main": "cl-rest.js", "scripts": { diff --git a/plugin.js b/plugin.js index e688685..afc4a61 100755 --- a/plugin.js +++ b/plugin.js @@ -8,6 +8,7 @@ restPlugin.addOption('rest-docport', 4001, 'rest plugin listens on this port', ' restPlugin.addOption('rest-protocol', 'https', 'rest plugin protocol', 'string'); restPlugin.addOption('rest-execmode', 'production', 'rest exec mode', 'string'); restPlugin.addOption('rest-rpc', ' ', 'allowed rpc commands', 'string'); +restPlugin.addOption('rest-lnrpcpath', ' ', 'path for lightning-rpc', 'string'); restPlugin.onInit = params => { process.env.LN_PATH = `${params.configuration['lightning-dir']}/${params.configuration['rpc-file']}` @@ -18,6 +19,7 @@ restPlugin.onInit = params => { PROTOCOL: params.options['rest-protocol'], EXECMODE: params.options['rest-execmode'], RPCCOMMANDS: params.options['rest-rpc'].trim().split(",").map(s => s.trim()), + LNRPCPATH: params.options['rest-lnrpcpath'], PLUGIN: restPlugin } diff --git a/routes/channel.js b/routes/channel.js index 340e565..bb19ecf 100644 --- a/routes/channel.js +++ b/routes/channel.js @@ -21,4 +21,7 @@ router.get('/localRemoteBal', tasteMacaroon, localRemoteBalController.localRemot //Get the list of htlcs forwarded router.get('/listForwards', tasteMacaroon, channelController.listForwards); +//Get the list of htlcs forwarded, along with starting and ending indices +router.get('/listForwardsFilter', tasteMacaroon, channelController.listForwardsFilter); + module.exports = router; \ No newline at end of file diff --git a/routes/getinfo.js b/routes/getinfo.js index e4a9597..5794515 100644 --- a/routes/getinfo.js +++ b/routes/getinfo.js @@ -11,4 +11,7 @@ router.post('/signMessage/', tasteMacaroon, getinfoController.signMessage); //Check a signature is from a node router.get('/checkMessage/:message/:zbase', tasteMacaroon, getinfoController.checkMessage); +//Decode an invoice string +router.get('/decode/:invoiceString', tasteMacaroon, getinfoController.decode); + module.exports = router; \ No newline at end of file diff --git a/routes/invoice.js b/routes/invoice.js index ee4f48d..48237f3 100644 --- a/routes/invoice.js +++ b/routes/invoice.js @@ -14,4 +14,7 @@ router.delete('/delExpiredInvoice', tasteMacaroon, invoiceController.delExpiredI //Delete invoice router.delete('/delInvoice/:label/:status', tasteMacaroon, invoiceController.delInvoice); +//Wait invoice +router.get('/waitInvoice/:label', tasteMacaroon, invoiceController.waitInvoice); + module.exports = router; \ No newline at end of file diff --git a/routes/offers.js b/routes/offers.js new file mode 100644 index 0000000..df2696b --- /dev/null +++ b/routes/offers.js @@ -0,0 +1,17 @@ +var router = require('express').Router(); +var channelController = require('../controllers/offers'); +var tasteMacaroon = require('../utils/tasteMacaroon'); + +//Create Offer +router.post('/offer', tasteMacaroon, channelController.offer); + +//List Offers +router.get('/listOffers', tasteMacaroon, channelController.listOffers); + +//Fetch Invoice +router.post('/fetchInvoice', tasteMacaroon, channelController.fetchInvoice); + +//Disable Offer +router.delete('/disableOffer/:offerid', tasteMacaroon, channelController.disableOffer); + +module.exports = router; \ No newline at end of file