/ src / server / lightning-client.js
lightning-client.js
  1  import path from 'path'
  2  import net from 'net'
  3  import chalk from 'chalk'
  4  import { EventEmitter } from 'node:events'
  5  const methods = [
  6    "autocleaninvoice",
  7    "check",
  8    "checkmessage",
  9    "close",
 10    "connect",
 11    "createonion",
 12    "decodepay",
 13    "delexpiredinvoice",
 14    "delinvoice",
 15    "delpay",
 16    "dev-sendcustommsg",
 17    "disconnect",
 18    "feerates",
 19    "fundchannel",
 20    "fundchannel_cancel",
 21    "fundchannel_complete",
 22    "fundchannel_start",
 23    "fundpsbt",
 24    "getinfo",
 25    "getlog",
 26    "getroute",
 27    "getsharedsecret",
 28    "help",
 29    "hsmtool",
 30    "invoice",
 31    "keysend",
 32    "listchannels",
 33    "listconfigs",
 34    "listforwards",
 35    "listfunds",
 36    "listinvoices",
 37    "listnodes",
 38    "listpays",
 39    "listpeers",
 40    "listsendpays",
 41    "listtransactions",
 42    "multifundchannel",
 43    "multiwithdraw",
 44    "newaddr",
 45    "notifications",
 46    "openchannel_init",
 47    "openchannel_signed",
 48    "openchannel_update",
 49    "ping",
 50    "plugin",
 51    "reserveinputs",
 52    "sendonion",
 53    "sendpay",
 54    "sendpsbt",
 55    "setchannelfee",
 56    "signmessage",
 57    "signpsbt",
 58    "stop",
 59    "txdiscard",
 60    "txprepare",
 61    "txsend",
 62    "unreserveinputs",
 63    "utxopsbt",
 64    "waitanyinvoice",
 65    "waitblockheight",
 66    "waitinvoice",
 67    "waitsendpay",
 68    // ## -- "withdraw"
 69    "stormwallet", 
 70    "stormnetwork", 
 71    "stormpaths"
 72  ]
 73  
 74  const debug = console.log
 75  
 76  class LightningClient extends EventEmitter {
 77      constructor(rpcPath, debugFlag = false) {
 78          if (!path.isAbsolute(rpcPath)) {
 79              throw new Error('The rpcPath must be an absolute path');
 80          }
 81  
 82          rpcPath = path.join(rpcPath, '/lightning-rpc');
 83  
 84          super();
 85          this.rpcPath = rpcPath;
 86          this.reconnectWait = 0.5;
 87          this.reconnectTimeout = null;
 88          this.reqcount = 0;
 89  
 90          this.debug = debugFlag;
 91  
 92          const _self = this;
 93  
 94          this.client = net.createConnection(rpcPath);
 95          this.clientConnectionPromise = new Promise(resolve => {
 96              _self.client.on('connect', () => {
 97                  _self.reconnectWait = 1;
 98                  resolve();
 99              });
100  
101              _self.client.on('end', () => {
102                  _self.increaseWaitTime();
103                  _self.reconnect();
104              });
105  
106              _self.client.on('error', error => {
107                  _self.increaseWaitTime();
108                  _self.reconnect();
109              });
110          });
111  
112          let buffer = Buffer.from('');
113          let openCount = 0;
114          this.client.on('data', data => {
115              LightningClient
116              .splitJSON(Buffer.concat([buffer, data]), buffer.length, openCount)
117              .forEach(partObj => {
118                  if (partObj.partial) {
119                      buffer = partObj.string;
120                      openCount = partObj.openCount;
121                      return;
122                  }
123  
124                  buffer = Buffer.from('');
125                  openCount = 0;
126  
127                  try {
128                      let dataObject = JSON.parse(partObj.string.toString());
129                      _self.emit('res:' + dataObject.id, dataObject);
130                  } catch (err) {
131                      return;
132                  }
133              });
134          });
135  
136      }
137  
138      static splitJSON(str, startFrom = 0, openCount = 0) {
139          const parts = [];
140  
141          let lastSplit = 0;
142  
143          for (let i = startFrom; i < str.length; i++) {
144              if (i > 0 && str[i - 1] === 115) { // 115 => backslash, ignore this character
145                  continue;
146              }
147  
148              if (str[i] === 123) { // '{'
149                  openCount++;
150              } else if (str[i] === 125) { // '}'
151                  openCount--;
152  
153                  if (openCount === 0) {
154                      const start = lastSplit;
155                      const end = (i + 1 === str.length) ? undefined : i + 1;
156  
157                      parts.push({partial: false, string: str.slice(start, end), openCount: 0});
158  
159                      lastSplit = end;
160                  }
161              }
162          }
163  
164          if (lastSplit !== undefined) {
165              parts.push({partial: true, string: str.slice(lastSplit), openCount});
166          }
167  
168          return parts;
169      }
170  
171      increaseWaitTime() {
172          if (this.reconnectWait >= 128) {
173              this.reconnectWait = 128;
174          } else {
175              this.reconnectWait *= 2;
176          }
177      }
178  
179      reconnect() {
180          const _self = this;
181  
182          if (this.reconnectTimeout) {
183              return;
184          }
185  
186          this.reconnectTimeout = setTimeout(() => {
187              debug('Trying to reconnect...');
188              _self.client.connect(_self.rpcPath);
189              _self.reconnectTimeout = null;
190          }, this.reconnectWait * 1000);
191      }
192  
193      call(method, args = {}) {
194          if (!typeof(method) === 'string') {
195              return Promise.reject(new Error('invalid_call'));
196          }
197  
198          let stackTrace = null;
199          if (this.debug === true) { 
200              const error = new Error();
201              stackTrace = error.stack;
202          }
203  
204          const _self = this;
205  
206          const callInt = ++this.reqcount;
207          const sendObj = {
208              jsonrpc:"2.0",
209              method,
210              params:args,
211              id: callInt
212          };
213          // Wait for the client to connect
214          return this.clientConnectionPromise
215              .then(() => new Promise((resolve, reject) => {
216                  // Wait for a response
217                  this.once('res:' + callInt, response => {
218                      if (!response.error) {
219                          return resolve(response.result);
220                      }
221                      reject({error: response.error, stack: stackTrace});
222                  });
223                  _self.client.write(JSON.stringify(sendObj));
224  
225              }));
226      }
227  }
228  
229  const protify = s => s.replace(/-([a-z])/g, m => m[1].toUpperCase());
230  
231  methods.forEach(k => {
232      LightningClient.prototype[protify(k)] = function (args={}) {
233          return this.call(k, args);
234      };
235  });
236  
237  export default LightningClient;