lightning
Version:
Lightning Network client library
229 lines (197 loc) • 7.83 kB
JavaScript
const EventEmitter = require('events');
const {randomBytes} = require('crypto');
const asyncAuto = require('async/auto');
const {returnResult} = require('asyncjs-util');
const {emitSubscriptionError} = require('./../../grpc');
const {handleRemoveListener} = require('./../../grpc');
const handleRpcRequestUpdate = require('./handle_rpc_request_update');
const {isLnd} = require('./../../lnd_requests');
const makeId = () => randomBytes(32).toString('hex');
const method = 'RegisterRPCMiddleware';
const minIdLength = 5;
const type = 'default';
/** Subscribe to RPC requests and their responses
`accept` and `reject` methods can be used with cbk or Promise syntax
Requires `macaroon:write` permission
LND must be running with "RPC middleware" enabled: `rpcmiddleware.enable=1`
This method is not supported in LND 0.13.4 and below
`description` is not supported on LND 0.16.4 and below
{
[id]: <RPC Middleware Interception Name String>
[is_intercepting_close_channel_requests]: <Intercept Channel Closes Bool>
[is_intercepting_open_channel_requests]: <Intercept Channel Opens Bool>
[is_intercepting_pay_via_routes_requests]: <Intercept Pay Via Route Bool>
lnd: <Authenticated LND API Object>
}
@returns via cbk or Promise
{
subscription: <RPC Request Subscription EventEmitter Object>
}
// A channel close request was intercepted: by default it will be rejected
@event 'close_channel_request'
{
accept: ({}, [cbk]) => {}
id: <Message Id Number>
macaroon: <Base64 Encoded Macaroon String>
reject: ({message: <Rejection String>}, [cbk]) => {}
request: {
[address]: <Request Sending Local Channel Funds To Address String>
[is_force_close]: <Is Force Close Bool>
[max_tokens_per_vbyte]: <Max Tokens Per VByte Number>
[target_confirmations]: <Confirmation Target Number>
[tokens_per_vbyte]: <Tokens Per Virtual Byte Number>
transaction_id: <Transaction Id Hex String>
transaction_vout: <Transaction Output Index Number>
}
uri: <RPC URI String>
}
// A channel open request was intercepted: by default it will be rejected
@event 'open_channel_request'
{
accept: ({}, [cbk]) => {}
id: <Message Id Number>
macaroon: <Base64 Encoded Macaroon String>
reject: ({message: <Rejection String>}, [cbk]) => {}
request: {
[base_fee_mtokens]: <Routing Base Fee Millitokens Charged String>
[chain_fee_tokens_per_vbyte]: <Chain Fee Tokens Per VByte Number>
[cooperative_close_address]: <Prefer Cooperative Close To Address String>
[description]: <Immutable Channel Description String>
[fee_rate]: <Routing Fee Rate In Millitokens Per Million Number>
[give_tokens]: <Tokens to Gift To Partner Number>
[is_max_funding]: <Use Maximal Chain Funds For Local Funding Bool>
[is_private]: <Channel is Private Bool>
[local_tokens]: <Local Tokens Number>
[min_confirmations]: <Spend UTXOs With Minimum Confirmations Number>
[min_htlc_mtokens]: <Minimum HTLC Millitokens String>
partner_public_key: <Public Key Hex String>
[partner_csv_delay]: <Peer Output CSV Delay Number>
}
uri: <RPC URI String>
}
// A pay to route request was intercepted: by default it will be rejected
@event 'pay_via_route_request'
{
accept: ({}, [cbk]) => {}
id: <Message Id Number>
macaroon: <Base64 Encoded Macaroon String>
reject: ({message: <Rejection String>}, [cbk]) => {}
request: {
id: <Payment Hash Hex String>
route: {
fee: <Route Fee Tokens Number>
fee_mtokens: <Route Fee Millitokens String>
hops: [{
channel: <Standard Format Channel Id String>
channel_capacity: <Channel Capacity Tokens Number>
fee: <Fee Tokens Number>
fee_mtokens: <Fee Millitokens String>
forward: <Forward Tokens Number>
forward_mtokens: <Forward Millitokens String>
public_key: <Forward Edge Public Key Hex String>
[timeout]: <Timeout Block Height Number>
}]
mtokens: <Total Fee-Inclusive Millitokens String>
[payment]: <Payment Identifier Hex String>
[timeout]: <Timeout Block Height Number>
tokens: <Total Fee-Inclusive Tokens Number>
[total_mtokens]: <Total Payment Millitokens String>
}
}
uri: <RPC URI String>
}
@event 'request'
{
call: <Call Identifier Number>
id: <Message Id Number>
[macaroon]: <Base64 Encoded Macaroon String>
[uri]: <RPC URI String>
}
@event 'response'
{
call: <Call Identifier Number>
id: <Message Id Number>
[macaroon]: <Base64 Encoded Macaroon String>
[uri]: <RPC URI String>
}
*/
module.exports = (args, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!!args.id && args.id.length < minIdLength) {
return cbk([400, 'ExpectedLongerIdLengthToSpecifyMiddlewareName']);
}
if (!isLnd({method, type, lnd: args.lnd})) {
return cbk([400, 'ExpectedLndToSubscribeToRpcRequests']);
}
return cbk();
},
// Initialize subscription
init: ['validate', ({}, cbk) => {
const emitter = new EventEmitter();
const isInterceptCloses = args.is_intercepting_close_channel_requests;
const isInterceptOpens = args.is_intercepting_open_channel_requests;
const isInterceptRoute = args.is_intercepting_pay_via_routes_requests;
const subscription = args.lnd[type][method]({});
const events = [
'close_channel_request',
'open_channel_request',
'pay_via_route_request',
'request',
'response',
];
const errored = emitSubscriptionError({emitter, subscription});
// Terminate subscription when all listeners are removed
handleRemoveListener({subscription, emitter, events});
subscription.on('end', () => emitter.emit('end'));
subscription.on('error', err => errored(err));
subscription.on('status', n => emitter.emit('status', n));
subscription.on('data', update => {
// Exit early on notification of registration complete
if (update.intercept_type === 'reg_complete') {
return;
}
try {
const {accept, data, event} = handleRpcRequestUpdate({
subscription,
update,
is_intercepting_close_channel_requests: isInterceptCloses,
is_intercepting_open_channel_requests: isInterceptOpens,
is_intercepting_pay_via_routes_requests: isInterceptRoute,
lnd: args.lnd,
});
// Notify listeners of the RPC update
emitter.emit(event, data);
// Exit early when accepting/rejecting is left to the caller
if (!accept) {
return;
}
// Accept traffic by default
return accept({}, err => !!err ? errored(err) : undefined);
} catch (err) {
return errored([503, err.message]);
}
});
return cbk(null, {emitter, subscription});
}],
// Start the interception of RPC requests
intercept: ['init', ({init}, cbk) => {
return init.subscription.write({
register: {
middleware_name: args.id || makeId(),
read_only_mode: true,
},
},
err => {
if (!!err) {
return cbk([503, 'UnexpectedErrInterceptingRpcRequests', {err}]);
}
return cbk(null, {subscription: init.emitter});
});
}],
},
returnResult({reject, resolve, of: 'intercept'}, cbk));
});
};