mirror of
https://github.com/Ride-The-Lightning/RTL.git
synced 2026-08-13 12:33:07 +02:00
LND's DELETE /v1/channels/{channelPoint} streams until the closing tx
confirms, routinely longer than the wrapper's 10-minute bound. The
close call in closeChannel is fire-and-forget (202 returned
immediately, no .catch), so the timeout rejection would have become an
unhandled promise rejection and crashed the process ~10 minutes after
any close that had not yet confirmed. request-promise returned
Bluebird promises whose unhandled rejections only warned, which is why
this never crashed before.
The close now uses a copy of the options with timeout: 0 (same
treatment as the invoice/payment subscriptions) and a .catch that logs
through handleError - errors were never surfaced to the HTTP response
anyway, but logging beats Bluebird's silent warning. This was the only
call site without a rejection handler.
Verified on the fixture: opened a disposable 200k alice->bob channel
via RTL, closed it (202, gone from open and listed in closed after
mining), then requested a close for a bogus channel point - LND
rejects the stream, the catch logs the error (no auth headers in it),
and the process stays up. Read suite re-passes.
240 lines
14 KiB
JavaScript
240 lines
14 KiB
JavaScript
import request from '../../utils/request.js';
|
|
import { Logger } from '../../utils/logger.js';
|
|
import { Common } from '../../utils/common.js';
|
|
let options = null;
|
|
const logger = Logger;
|
|
const common = Common;
|
|
export const getAliasForChannel = (selNode, channel) => {
|
|
const pubkey = (channel.remote_pubkey) ? channel.remote_pubkey : (channel.remote_node_pub) ? channel.remote_node_pub : '';
|
|
options.url = selNode.settings.lnServerUrl + '/v1/graph/node/' + pubkey;
|
|
return request(options).then((aliasBody) => {
|
|
logger.log({ selectedNode: selNode, level: 'DEBUG', fileName: 'Channels', msg: 'Alias Received', data: aliasBody.node.alias });
|
|
channel.remote_alias = aliasBody.node.alias && aliasBody.node.alias !== '' ? aliasBody.node.alias : aliasBody.node.pub_key.slice(0, 20);
|
|
return channel;
|
|
}).catch((err) => {
|
|
channel.remote_alias = pubkey.slice(0, 20);
|
|
return channel;
|
|
});
|
|
};
|
|
export const getAllChannels = (req, res, next) => {
|
|
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting Channels..' });
|
|
options = common.getOptions(req);
|
|
if (options.error) {
|
|
return res.status(options.statusCode).json({ message: options.message, error: options.error });
|
|
}
|
|
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/channels';
|
|
options.qs = req.query;
|
|
let local = 0;
|
|
let remote = 0;
|
|
let total = 0;
|
|
request(options).then((body) => {
|
|
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Channels List Received', data: body });
|
|
if (body.channels) {
|
|
return Promise.all(body.channels?.map((channel) => {
|
|
local = (channel.local_balance) ? +channel.local_balance : 0;
|
|
remote = (channel.remote_balance) ? +channel.remote_balance : 0;
|
|
total = local + remote;
|
|
channel.balancedness = (total === 0) ? 1 : (1 - Math.abs((local - remote) / total)).toFixed(3);
|
|
return getAliasForChannel(req.session.selectedNode, channel);
|
|
})).then((values) => {
|
|
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Sorted Channels List Received', data: body });
|
|
return res.status(200).json(body);
|
|
}).catch((errRes) => {
|
|
const err = common.handleError(errRes, 'Channels', 'Get All Channel Aliases Error', req.session.selectedNode);
|
|
return res.status(err.statusCode).json({ message: err.message, error: err.error });
|
|
});
|
|
}
|
|
else {
|
|
body.channels = [];
|
|
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Empty Channels List Received' });
|
|
return res.status(200).json(body);
|
|
}
|
|
}).catch((errRes) => {
|
|
const err = common.handleError(errRes, 'Channels', 'List Channels Error', req.session.selectedNode);
|
|
return res.status(err.statusCode).json({ message: err.message, error: err.error });
|
|
});
|
|
};
|
|
export const getPendingChannels = (req, res, next) => {
|
|
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting Pending Channels..' });
|
|
options = common.getOptions(req);
|
|
if (options.error) {
|
|
return res.status(options.statusCode).json({ message: options.message, error: options.error });
|
|
}
|
|
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/channels/pending';
|
|
options.qs = req.query;
|
|
request(options).then((body) => {
|
|
if (!body.total_limbo_balance) {
|
|
body.total_limbo_balance = 0;
|
|
}
|
|
const promises = [];
|
|
if (body.pending_open_channels && body.pending_open_channels.length > 0) {
|
|
body.pending_open_channels?.map((channel) => promises.push(getAliasForChannel(req.session.selectedNode, channel.channel)));
|
|
}
|
|
if (body.pending_force_closing_channels && body.pending_force_closing_channels.length > 0) {
|
|
body.pending_force_closing_channels?.map((channel) => promises.push(getAliasForChannel(req.session.selectedNode, channel.channel)));
|
|
}
|
|
if (body.pending_closing_channels && body.pending_closing_channels.length > 0) {
|
|
body.pending_closing_channels?.map((channel) => promises.push(getAliasForChannel(req.session.selectedNode, channel.channel)));
|
|
}
|
|
if (body.waiting_close_channels && body.waiting_close_channels.length > 0) {
|
|
body.waiting_close_channels?.map((channel) => promises.push(getAliasForChannel(req.session.selectedNode, channel.channel)));
|
|
}
|
|
return Promise.all(promises).then((values) => {
|
|
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Pending Channels List Received', data: body });
|
|
return res.status(200).json(body);
|
|
}).
|
|
catch((errRes) => {
|
|
const err = common.handleError(errRes, 'Channels', 'Get Pending Channel Aliases Error', req.session.selectedNode);
|
|
return res.status(err.statusCode).json({ message: err.message, error: err.error });
|
|
});
|
|
}).catch((errRes) => {
|
|
const err = common.handleError(errRes, 'Channels', 'List Pending Channels Error', req.session.selectedNode);
|
|
return res.status(err.statusCode).json({ message: err.message, error: err.error });
|
|
});
|
|
};
|
|
export const getClosedChannels = (req, res, next) => {
|
|
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting Closed Channels..' });
|
|
options = common.getOptions(req);
|
|
if (options.error) {
|
|
return res.status(options.statusCode).json({ message: options.message, error: options.error });
|
|
}
|
|
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/channels/closed';
|
|
options.qs = req.query;
|
|
request(options).then((body) => {
|
|
if (body.channels && body.channels.length > 0) {
|
|
return Promise.all(body.channels?.map((channel) => {
|
|
channel.close_type = (!channel.close_type) ? 'COOPERATIVE_CLOSE' : channel.close_type;
|
|
return getAliasForChannel(req.session.selectedNode, channel);
|
|
})).then((values) => {
|
|
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Closed Channels List Received', data: body });
|
|
return res.status(200).json(body);
|
|
}).catch((errRes) => {
|
|
const err = common.handleError(errRes, 'Channels', 'Get Closed Channel Aliases Error', req.session.selectedNode);
|
|
return res.status(err.statusCode).json({ message: err.message, error: err.error });
|
|
});
|
|
}
|
|
else {
|
|
body.channels = [];
|
|
return res.status(200).json(body);
|
|
}
|
|
}).catch((errRes) => {
|
|
const err = common.handleError(errRes, 'Channels', 'List Closed Channels Error', req.session.selectedNode);
|
|
return res.status(err.statusCode).json({ message: err.message, error: err.error });
|
|
});
|
|
};
|
|
export const postChannel = (req, res, next) => {
|
|
const { node_pubkey, private: privateChannel, spend_unconfirmed, local_funding_amount, trans_type, trans_type_value, commitment_type } = req.body;
|
|
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Opening Channel..' });
|
|
options = common.getOptions(req);
|
|
if (options.error) {
|
|
return res.status(options.statusCode).json({ message: options.message, error: options.error });
|
|
}
|
|
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/channels';
|
|
options.form = {
|
|
node_pubkey_string: node_pubkey,
|
|
local_funding_amount: local_funding_amount,
|
|
private: privateChannel,
|
|
spend_unconfirmed: spend_unconfirmed
|
|
};
|
|
if (trans_type === '1') {
|
|
options.form.target_conf = trans_type_value;
|
|
}
|
|
else if (trans_type === '2') {
|
|
options.form.sat_per_vbyte = trans_type_value;
|
|
}
|
|
if (commitment_type) {
|
|
options.form.commitment_type = commitment_type;
|
|
}
|
|
options.form = JSON.stringify(options.form);
|
|
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Channel Open Options', data: options.form });
|
|
request.post(options).then((body) => {
|
|
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Opened', data: body });
|
|
res.status(201).json(body);
|
|
}).catch((errRes) => {
|
|
const err = common.handleError(errRes, 'Channels', 'Open Channel Error', req.session.selectedNode);
|
|
return res.status(err.statusCode).json({ message: err.message, error: err.error });
|
|
});
|
|
};
|
|
export const closeChannel = (req, res, next) => {
|
|
try {
|
|
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Closing Channel..' });
|
|
if (!req.session.selectedNode) {
|
|
const err = common.handleError({ message: 'Session Expired after a day\'s inactivity.', statusCode: 401 }, 'Session Expired', 'Session Expiry Error', null);
|
|
return res.status(err.statusCode).json({ message: err.message, error: err.error });
|
|
}
|
|
options = common.getOptions(req);
|
|
if (options.error) {
|
|
return res.status(options.statusCode).json({ message: options.message, error: options.error });
|
|
}
|
|
const channelpoint = req.params.channelPoint?.replace(':', '/');
|
|
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/channels/' + channelpoint + '?force=' + req.query.force;
|
|
if (req.query.target_conf) {
|
|
options.url = options.url + '&target_conf=' + req.query.target_conf;
|
|
}
|
|
if (req.query.sat_per_vbyte) {
|
|
options.url = options.url + '&sat_per_vbyte=' + req.query.sat_per_vbyte;
|
|
}
|
|
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Closing Channel Options URL', data: options.url });
|
|
// Fire-and-forget: LND keeps the close stream open until the closing tx
|
|
// confirms, so exempt it from the request timeout; the 202 is already sent,
|
|
// so log a rejection instead of letting it crash the process.
|
|
request.delete({ ...options, timeout: 0 }).catch((errRes) => {
|
|
const err = common.handleError(errRes, 'Channels', 'Close Channel Error', req.session.selectedNode);
|
|
logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Channels', msg: 'Close Channel Error', error: err });
|
|
});
|
|
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Close Requested' });
|
|
res.status(202).json({ message: 'Close channel request has been submitted.' });
|
|
}
|
|
catch (error) {
|
|
logger.log({ selectedNode: req.session.selectedNode, level: 'ERROR', fileName: 'Channels', msg: 'Close Channel Error', error: error.message });
|
|
return res.status(500).json({ message: 'Close Channel Error', error: error.message });
|
|
}
|
|
};
|
|
export const postChanPolicy = (req, res, next) => {
|
|
const { chanPoint, baseFeeMsat, feeRate, timeLockDelta, max_htlc_msat, min_htlc_msat } = req.body;
|
|
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Updating Channel Policy..' });
|
|
options = common.getOptions(req);
|
|
if (options.error) {
|
|
return res.status(options.statusCode).json({ message: options.message, error: options.error });
|
|
}
|
|
options.url = req.session.selectedNode.settings.lnServerUrl + '/v1/chanpolicy';
|
|
if (chanPoint === 'all') {
|
|
options.form = JSON.stringify({
|
|
global: true,
|
|
base_fee_msat: baseFeeMsat,
|
|
fee_rate: parseFloat((feeRate / 1000000).toString()),
|
|
time_lock_delta: parseInt(timeLockDelta)
|
|
});
|
|
}
|
|
else {
|
|
const breakPoint = chanPoint.indexOf(':');
|
|
const txid_str = chanPoint.substring(0, breakPoint);
|
|
const output_idx = chanPoint.substring(breakPoint + 1, chanPoint.length);
|
|
const optionsBody = {
|
|
base_fee_msat: baseFeeMsat,
|
|
fee_rate: parseFloat((feeRate / 1000000).toString()),
|
|
time_lock_delta: parseInt(timeLockDelta),
|
|
chan_point: { funding_txid_str: txid_str, output_index: parseInt(output_idx) }
|
|
};
|
|
if (max_htlc_msat) {
|
|
optionsBody['max_htlc_msat'] = max_htlc_msat;
|
|
}
|
|
if (min_htlc_msat) {
|
|
optionsBody['min_htlc_msat'] = min_htlc_msat;
|
|
optionsBody['min_htlc_msat_specified'] = true;
|
|
}
|
|
options.form = JSON.stringify(optionsBody);
|
|
}
|
|
logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Update Channel Policy Options', data: options.form });
|
|
request.post(options).then((body) => {
|
|
logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Channel Policy Updated', data: body });
|
|
if (body.failed_updates && body.failed_updates.length && body.failed_updates[0].update_error) {
|
|
const err = common.handleError({ error: body.failed_updates[0].update_error }, 'Channels', 'Update Channel Policy Error', req.session.selectedNode);
|
|
return res.status(500).json({ message: err.message, error: err.error });
|
|
}
|
|
res.status(201).json(body);
|
|
}).catch((errRes) => {
|
|
const err = common.handleError(errRes, 'Channels', 'Update Channel Policy Error', req.session.selectedNode);
|
|
return res.status(err.statusCode).json({ message: err.message, error: err.error });
|
|
});
|
|
};
|