clightningjs
Version:
Plugins for C-lightning, a Lightning Network implementation. Also contains an RPC wrapper.
235 lines (218 loc) • 6.95 kB
JavaScript
const EventEmitter = require('events');
const path = require('path');
const RpcMethod = require('./method.js');
const RpcWrapper = require('./rpc.js');
class Notification extends EventEmitter {};
class Plugin {
constructor (params) {
// name: { type: "", default: "", description: "" }
this.options = {};
// RpcMethods
this.methods = [];
// name: Notification()
this.notifications = {};
// name: callback
this.hooks = {};
this.rpc = undefined;
// Plugins are dynamic by default
this.dynamic = true;
if (typeof params != 'undefined') {
// Backward compat
// TODO: Make sure nobody relies on it anymore
if (typeof params === 'boolean') {
this.dynamic = params;
}
// New behaviour
if (params.hasOwnProperty('dynamic')) {
this.dynamic = params.dynamic;
}
}
}
// Beware with writing on stdout !
// https://nodejs.org/api/process.html#process_a_note_on_process_i_o
async _write (content) {
// We append \n\n, not that is still mandatory but it's way more
// readable to a human debugger !
content += '\n';
if (!process.stdout.write(content)) {
return new Promise((resolve, reject) => {
process.stdout.once('drain', resolve());
process.stdout.once('error', reject());
});
}
return Promise.resolve();
}
// The getmanifest call, all about us !
_getmanifest (params) {
let opts = [];
for (let name in this.options) {
opts.push({
name: name,
type: this.options[name].type,
default: this.options[name].default,
description: this.options[name].description
});
}
let notifs = [];
for (let name in this.notifications) {
notifs.push(name);
}
let hooks = [];
for (let name in this.hooks) {
hooks.push(name);
}
return {
options: opts,
rpcmethods: this.methods.map(function (method) {
return {
name: method.name,
usage: method.usage,
description: method.description,
long_description: method.longDescription
}
}),
subscriptions: notifs,
hooks: hooks,
dynamic: this.dynamic,
}
}
// We are almost done ! Lightningd sends this once it receives our manifest.
_init (params) {
const socketPath = path.join(params.configuration['lightning-dir'],
params.configuration['rpc-file']);
this.rpc = new RpcWrapper(socketPath);
for (let opt in params.options) {
this.options[opt].value = params.options[opt];
}
this.startup = params.configuration['startup'];
this.onInit(params);
// It's not interpreted by lightningd for now.
return {};
}
async _writeJsonrpcNotification (method, params) {
const payload = {
jsonrpc: '2.0',
method: method,
params: params,
}
const notif = JSON.stringify(payload);
await this._write(notif);
}
async _writeJsonrpcResponse (result, id, isError=false) {
const payload = {
jsonrpc: '2.0',
id: id
};
if (isError) payload.error = result;
else payload.result = result;
const response = JSON.stringify(payload);
await this._write(response);
}
// Add a fresh JSONRPC method accessible from lightningd
addMethod (name, callback, usage, description, longDescription) {
if (!name || !callback) {
throw new Error('You need to pass at least a name and a callback to register a method');
}
const method = new RpcMethod(name, usage, description, longDescription);
method.main = callback;
this.methods.push(method);
}
// Add a startup option to lightningd
addOption (name, defaultValue, description, type) {
if (!name || !defaultValue || !description) {
throw new Error('You need to pass at least a name, default value and description for the option');
}
this.options[name] = {
default: defaultValue,
description: description,
type: type || 'string',
value: defaultValue
};
}
// A hook is a notification which needs a response from our (plugin) side
addHook (name, callback) {
this.hooks[name] = callback;
}
// Notifications are emitted as events
subscribe (name) {
this.notifications[name] = new Notification();
}
// To be overriden to do something special at startup
onInit (params) {
}
// Send logs to lightningd's log
log (message, level) {
level = level || 'info';
if (!message || typeof message !== 'string') {
throw new Error('You need to specify a string to write on lightningd\'s logs.');
}
message.split('\n').forEach((line) => {
if (line) {
this._writeJsonrpcNotification('log', {level: level, message: line});
}
});
}
// Read from stdin and do what master (not Satoshi, lightningd!!) tells until
// we die
async _mainLoop () {
let chunk;
let msg;
while (chunk = process.stdin.read()) {
// Ok so process.stdin.read() can actually return a chunk with multiple
// lines.
// FIXME: don't rely on lightningd's \n\n !!
const lines = chunk.split('\n\n');
for (const i in lines) {
if (!lines[i]) continue;
try {
msg = JSON.parse(lines[i]);
} catch (e) {
this.log(e.message, 'error');
throw e;
}
// JSONRPC2 sanity checks
if (!msg || !msg.method || msg.jsonrpc !== '2.0') {
this.log('Got bad JSONRPC2', 'error');
throw new Error('Bad JSONRPC(2)!');
}
if (!msg.id && msg.method in this.notifications) {
this.notifications[msg.method].emit(msg.method, msg.params);
}
if (msg.method === 'getmanifest') {
await this._writeJsonrpcResponse(this._getmanifest(msg.params),
msg.id);
continue;
}
if (msg.method === 'init') {
await this._writeJsonrpcResponse(this._init(msg.params),
msg.id);
continue;
}
if (msg.method in this.hooks) {
Promise.resolve(this.hooks[msg.method](msg.params)).then(async (response) => {
await this._writeJsonrpcResponse(response, msg.id);
});
continue;
}
this.methods.forEach(async (m) => {
if (m.name === msg.method) {
try {
const response = await m.main(msg.params)
await this._writeJsonrpcResponse(response, msg.id);
} catch (error) {
await this._writeJsonrpcResponse({code: -32603, message: error.message}, msg.id, true);
}
}
});
}
}
}
// Start plugining !
start () {
process.stdin.setEncoding('utf8');
process.stdin.on('readable', () => {
this._mainLoop();
});
}
}
module.exports = Plugin;