balanceofsatoshis
Version:
Lightning balance CLI
243 lines (197 loc) • 7.41 kB
JavaScript
const asyncAuto = require('async/auto');
const asyncReflect = require('async/reflect');
const {cancelHodlInvoice} = require('ln-service');
const {getInvoice} = require('ln-service');
const {returnResult} = require('asyncjs-util');
const {openChannel} = require('ln-service');
const {settleHodlInvoice} = require('ln-service');
const {acceptsChannelOpen} = require('ln-sync');
const {orderStateFailed} = require('./lsps1_protocol');
const {orderStateCompleted} = require('./lsps1_protocol');
const {paymentStateHeldPayment} = require('./lsps1_protocol');
const {paymentStatePaid} = require('./lsps1_protocol');
const {paymentStateRefunded} = require('./lsps1_protocol');
const asOutpoint = utxo => `${utxo.transaction_id}:${utxo.transaction_vout}`;
const channelExpiryMs = 1000 * 60 * 60 * 24 * 90;
const expiryDate = ms => new Date(Date.now() + ms).toISOString();
const {parse} = JSON;
const updateOrder = (orders, o, id) => orders.set(id, JSON.stringify(o));
/** Attempt to open a sold channel
{
chain_fee: <Chain Fee Rate Per VByte For Channel Open Number>
invoice_id: <Channel Open Invoice Payment Hash Id Hex String>
lnd: <Authenticated LND API Object>
logger: <Winston Logger Object>
open_to: <Open To Node With Identity Public Key Hex String>
order_id: <Channel Sale Order Id String>
orders: <Open Orders Map Object>
secret: <Invoice Secret Preimage Hex String>
}
@returns via cbk or Promise
*/
module.exports = (args, cbk) => {
return new Promise((resolve, reject) => {
return asyncAuto({
// Check arguments
validate: cbk => {
if (!args.chain_fee) {
return cbk([400, 'ExpectedChainFeesToOpenSoldChannel']);
}
if (!args.invoice_id) {
return cbk([400, 'ExpectedInvoiceIdToOpenSoldChannel']);
}
if (!args.lnd) {
return cbk([400, 'ExpectedAuthenticatedLndToOpenSoldChannel']);
}
if (!args.logger) {
return cbk([400, 'ExpectedLoggerToOpenSoldChannel']);
}
if (!args.open_to) {
return cbk([400, 'ExpectedIdentityPublicKeyToOpenSoldChannelTo']);
}
if (!args.order_id) {
return cbk([400, 'ExpectedOrderIdToOpenSoldChannel']);
}
if (!args.orders) {
return cbk([400, 'ExpectedOrdersMapToOpenSoldChannel']);
}
if (!args.secret) {
return cbk([400, 'ExpectedSecretToOpenSoldChannel']);
}
return cbk();
},
// Get the order
order: ['validate', ({}, cbk) => {
if (!args.orders.get(args.order_id)) {
return cbk([400, 'ExpectedExistingOrderToOpenSoldChannel']);
}
return cbk(null, parse(args.orders.get(args.order_id)));
}],
// Check if a channel open would be accepted
acceptsOpen: ['order', asyncReflect(({order}, cbk) => {
args.logger.info({initial_status: order.result});
// Set payment status to hold
order.result.payment.state = paymentStateHeldPayment;
updateOrder(args.orders, order, args.order_id);
return acceptsChannelOpen({
capacity: order.result.lsp_balance_sat,
is_private: !order.result.announce_channel,
lnd: args.lnd,
partner_public_key: args.open_to,
},
cbk);
})],
// Attempt to open channel
openChannel: [
'acceptsOpen',
'order',
asyncReflect(({acceptsOpen, order}, cbk) =>
{
// Exit early when the channel open is not accepted
if (!!acceptsOpen.error) {
return cbk(acceptsOpen.error);
}
return openChannel({
description: `Sold channel open for invoice ${args.invoice_id}`,
chain_fee_tokens_per_vbyte: args.chain_fee,
is_private: !order.result.announce_channel,
lnd: args.lnd,
local_tokens: order.result.lsp_balance_sat,
partner_public_key: args.open_to,
},
cbk);
})],
// Update the order with the channel information
addChannel: ['openChannel', 'order', ({openChannel, order}, cbk) => {
// Exit early when the open attempt was not successful
if (!!openChannel.error) {
return cbk();
}
order.result.channel = {
expires_at: expiryDate(channelExpiryMs),
funded_at: new Date().toISOString(),
funding_outpoint: asOutpoint(openChannel.value),
};
updateOrder(args.orders, order, args.order_id);
args.logger.info({
channel_opening: order.result.channel,
is_private_chan: !order.result.announce_channel || undefined,
resolve_invoice: args.invoice_id,
reveal_preimage: args.secret,
});
return cbk();
}],
// Prevent the invoice from being paid
cancel: ['openChannel', ({openChannel}, cbk) => {
// Exit early when the open was successful
if (!openChannel.error) {
return cbk();
}
args.logger.error({err: openChannel.error});
// Cancel back the held payment since the channel open failed
return cancelHodlInvoice({lnd: args.lnd, id: args.invoice_id}, cbk);
}],
// Update the order to failed state
failOrder: [
'cancel',
'openChannel',
'order',
({cancel, openChannel, order}, cbk) =>
{
// Exit early when the open was successful
if (!openChannel.error) {
return cbk();
}
order.result.order_state = orderStateFailed;
order.result.payment.state = paymentStateRefunded;
updateOrder(args.orders, order, args.order_id);
args.logger.info({order_failed: args.order_id});
return cbk();
}],
// Now that the delivery of the channel has been made, take the funds
settle: ['addChannel', 'openChannel', ({openChannel}, cbk) => {
// Exit early when the open attempt was not successful
if (!!openChannel.error) {
return cbk();
}
return settleHodlInvoice({lnd: args.lnd, secret: args.secret}, cbk);
}],
// Finalize the payment state in the order records
updateAsPaid: [
'openChannel',
'order',
'settle',
({openChannel, order}, cbk) =>
{
// Exit early when the open attempt was not successful
if (!!openChannel.error) {
return cbk();
}
order.result.order_state = orderStateCompleted;
order.result.payment.state = paymentStatePaid;
updateOrder(args.orders, order, args.order_id);
return cbk();
}],
// Get the updated invoice to see how much was received
getUpdated: ['openChannel', 'updateAsPaid', ({openChannel}, cbk) => {
// Exit early when the open attempt was not successful
if (!!openChannel.error) {
return cbk();
}
return getInvoice({id: args.invoice_id, lnd: args.lnd}, cbk);
}],
// Notify of received payment
received: ['getUpdated', ({getUpdated}, cbk) => {
if (!getUpdated) {
return cbk();
}
args.logger.info({
order_complete: args.order_id,
received_funds: getUpdated.received,
});
return cbk();
}],
},
returnResult({reject, resolve}, cbk));
});
};