lightning
Version:
Lightning Network client library
206 lines (169 loc) • 6.16 kB
JavaScript
const EventEmitter = require('events');
const {featureFlagDetails} = require('bolt09');
const getNode = require('./get_node');
const {isLnd} = require('./../../lnd_requests');
const {rpcChannelClosedAsClosed} = require('./../../lnd_responses');
const {rpcChannelUpdateAsUpdate} = require('./../../lnd_responses');
const events = ['channel_closed', 'channel_updated', 'node_updated'];
const {isArray} = Array;
const {keys} = Object;
const method = 'subscribeChannelGraph';
const sumOf = arr => arr.reduce((sum, n) => sum + n, Number());
const type = 'default';
/** Subscribe to graph updates
Requires `info:read` permission
`inbound_base_discount_mtokens` is not supported on LND 0.17.5 and below
`inbound_rate_discount` is not supported on LND 0.17.5 and below
{
lnd: <Authenticated LND API Object>
}
@throws
<Error>
@returns
<EventEmitter Object>
@event 'channel_closed'
{
[capacity]: <Channel Capacity Tokens Number>
close_height: <Channel Close Confirmed Block Height Number>
id: <Standard Format Channel Id String>
[transaction_id]: <Channel Transaction Id String>
[transaction_vout]: <Channel Transaction Output Index Number>
updated_at: <Update Received At ISO 8601 Date String>
}
@event 'channel_updated'
{
base_fee_mtokens: <Channel Base Fee Millitokens String>
[capacity]: <Channel Capacity Tokens Number>
cltv_delta: <Channel CLTV Delta Number>
fee_rate: <Channel Fee Rate In Millitokens Per Million Number>
id: <Standard Format Channel Id String>
inbound_base_discount_mtokens: <Source Specific Base Fee Reduction String>
inbound_rate_discount: <Source Specific Per Million Rate Reduction Number>
is_disabled: <Channel Is Disabled Bool>
[max_htlc_mtokens]: <Channel Maximum HTLC Millitokens String>
min_htlc_mtokens: <Channel Minimum HTLC Millitokens String>
public_keys: [<Announcing Public Key>, <Target Public Key String>]
[transaction_id]: <Channel Transaction Id String>
[transaction_vout]: <Channel Transaction Output Index Number>
updated_at: <Update Received At ISO 8601 Date String>
}
@event 'error'
<Subscription Error>
@event 'node_updated'
{
alias: <Node Alias String>
color: <Node Color String>
features: [{
bit: <BOLT 09 Feature Bit Number>
is_known: <Feature is Known Bool>
is_required: <Feature Support is Required Bool>
type: <Feature Type String>
}]
public_key: <Node Public Key Hex String>
[sockets]: [<Network Host And Port String>]
updated_at: <Update Received At ISO 8601 Date String>
}
*/
module.exports = ({lnd}) => {
if (!isLnd({lnd, method, type})) {
throw new Error('ExpectedAuthenticatedLndToSubscribeToChannelGraph');
}
const eventEmitter = new EventEmitter();
const subscription = lnd[type][method]({});
// Notify listeners of an error and cancel the subscription if it's active
const emitError = err => {
subscription.cancel();
if (!eventEmitter.listenerCount('error')) {
return;
}
return eventEmitter.emit('error', err);
};
// Cancel the subscription when all listeners are removed
eventEmitter.on('removeListener', () => {
const listenerCounts = events.map(n => eventEmitter.listenerCount(n));
// Exit early when there are still listeners
if (!!sumOf(listenerCounts)) {
return;
}
subscription.cancel();
return;
});
subscription.on('data', update => {
if (!isArray(update.channel_updates)) {
return emitError(new Error('ExpectedChannelUpdates'));
}
if (!isArray(update.closed_chans)) {
return emitError(new Error('ExpectedClosedChans'));
}
if (!isArray(update.node_updates)) {
return emitError(new Error('ExpectedNodeUpdates'));
}
// Emit channel updates
update.channel_updates.forEach(update => {
try {
const channelUpdated = rpcChannelUpdateAsUpdate(update);
return eventEmitter.emit('channel_updated', channelUpdated);
} catch (err) {
return emitError(err);
}
});
// Emit closed channel updates
update.closed_chans.forEach(update => {
try {
const channelClosed = rpcChannelClosedAsClosed(update);
return eventEmitter.emit('channel_closed', channelClosed);
} catch (err) {
return emitError(err);
}
});
// Emit node updates
update.node_updates.forEach(node => {
if (!node) {
return emitError(new Error('ExpectedDetailsInNodeUpdateAnnouncement'));
}
if (!node.identity_key) {
return emitError(new Error('ExpectedPubKeyInNodeUpdateAnnouncement'));
}
// LND 0.11.1 and below don't support features, but above do: exit early
if (!!node.features && !!keys(node.features).length) {
return eventEmitter.emit('node_updated', {
alias: node.alias,
color: node.color,
features: keys(node.features).map(bit => ({
bit: Number(bit),
is_known: node.features[bit].is_known,
is_required: node.features[bit].is_required,
type: featureFlagDetails({bit: Number(bit)}).type,
})),
public_key: node.identity_key,
sockets: node.addresses,
updated_at: new Date().toISOString(),
});
}
// Lookup features for LND 0.11.1 and below
return getNode({
lnd,
is_omitting_channels: true,
public_key: node.identity_key,
},
(err, res) => {
if (!!err) {
return emitError(err);
}
return eventEmitter.emit('node_updated', {
alias: res.alias,
color: res.color,
features: res.features,
public_key: node.identity_key,
sockets: res.sockets.map(n => n.socket),
updated_at: res.updated_at || new Date().toISOString(),
});
});
});
return;
});
subscription.on('end', () => eventEmitter.emit('end'));
subscription.on('error', err => emitError(err));
subscription.on('status', status => eventEmitter.emit('status', status));
return eventEmitter;
};