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;