mirror of
https://github.com/alexbosworth/balanceofsatoshis.git
synced 2026-08-13 12:33:37 +02:00
834 lines
32 KiB
JavaScript
Executable file
834 lines
32 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
const {lstat} = require('fs');
|
|
const {mkdir} = require('fs');
|
|
const {readdir} = require('fs');
|
|
const {readFile} = require('fs');
|
|
const {rmdir} = require('fs');
|
|
const {spawn} = require('child_process');
|
|
const {unlink} = require('fs');
|
|
const {writeFile} = require('fs');
|
|
|
|
const inquirer = require('inquirer');
|
|
const prog = require('caporal');
|
|
const {rateProviders} = require('ln-accounting');
|
|
const request = require('request');
|
|
|
|
const {accountingCategories} = require('./balances');
|
|
const {adjustSavedNodes} = require('./nodes');
|
|
const {authenticatedLnd} = require('./lnd');
|
|
const {exchanges} = require('./fiat');
|
|
const {findRecord} = require('./lnd');
|
|
const {fundDev} = require('./wallets');
|
|
const {getAccountingReport} = require('./balances');
|
|
const {getBalance} = require('./balances');
|
|
const {getCertValidityDays} = require('./lnd');
|
|
const {getChainFees} = require('./chain');
|
|
const {getChannelCloses} = require('./chain');
|
|
const {getCredentials} = require('./lnd');
|
|
const {getDepositAddress} = require('./chain');
|
|
const {getExchangeRates} = require('./fiat');
|
|
const {getFeesChart} = require('./routing');
|
|
const {getForwards} = require('./network');
|
|
const {getLiquidity} = require('./balances');
|
|
const {getPeers} = require('./network');
|
|
const {getPriceChart} = require('./fiat');
|
|
const {getReport} = require('./wallets');
|
|
const {getSwapCost} = require('./swaps');
|
|
const {getSwapService} = require('./swaps');
|
|
const {getUtxos} = require('./chain');
|
|
const {ignoreFromAvoid} = require('./routing');
|
|
const marketPairs = require('./fiat').pairs;
|
|
const {openChannel} = require('./network');
|
|
const {probeDestination} = require('./network');
|
|
const {rebalance} = require('./swaps');
|
|
const {returnChart} = require('./responses');
|
|
const {returnNumber} = require('./responses');
|
|
const {returnObject} = require('./responses');
|
|
const {returnOutput} = require('./responses');
|
|
const {sendGift} = require('./network');
|
|
const {setAutopilot} = require('./network');
|
|
const {splitUtxos} = require('./chain');
|
|
const {startTelegramBot} = require('./telegram');
|
|
const {swapIn} = require('./swaps');
|
|
const {swapOut} = require('./swaps');
|
|
const {swapTypes} = require('./swaps');
|
|
const {unlockWallet} = require('./wallets');
|
|
const {version} = require('./package');
|
|
|
|
const {exit} = process;
|
|
const flatten = arr => [].concat(...arr);
|
|
const {FLOAT} = prog;
|
|
const {INT} = prog;
|
|
const {isArray} = Array;
|
|
const {keys} = Object;
|
|
const {REPEATABLE} = prog;
|
|
|
|
prog
|
|
.version(version)
|
|
|
|
// Get accounting information
|
|
.command('accounting', 'Get an accounting rundown')
|
|
.argument('<category>', 'Report category', keys(accountingCategories))
|
|
.help(`Categories: ${keys(accountingCategories).join(', ')}`)
|
|
.help(`Rate providers: ${rateProviders.join(', ')}`)
|
|
.option('--csv', 'Output a CSV')
|
|
.option('--month <month>', 'Show only records for specific month')
|
|
.option('--node <node_name>', 'Get details from named node')
|
|
.option('--rate-provider <rate_provider>', 'Rate provider', rateProviders)
|
|
.option('--year <year>', 'Show only records for specified year')
|
|
.action((args, options, logger) => {
|
|
const table = !!options.csv ? null : 'rows';
|
|
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
return getAccountingReport({
|
|
category: args.category,
|
|
is_csv: !!options.csv,
|
|
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
|
|
month: options.month,
|
|
node: options.node,
|
|
rate_provider: options.rateProvider,
|
|
year: options.year,
|
|
},
|
|
returnObject({logger, reject, resolve, table}));
|
|
} catch (err) {
|
|
return reject(err);
|
|
}
|
|
});
|
|
})
|
|
|
|
// Direct autopilot to mirror one or more nodes on the network
|
|
.command('autopilot', 'Enable autopilot')
|
|
.argument('<status>', 'Status of autopilot', ['off', 'on'])
|
|
.help('Autopilot status is either on or off')
|
|
.help('Mirroring and urls require lnd --autopilot.heuristic=externalscore:1')
|
|
.option('--dryrun', 'Show scoring without changing autopilot settings')
|
|
.option('--mirror <pubkey>', 'Mirror channels of node', REPEATABLE)
|
|
.option('--no-color', 'Mute all colors')
|
|
.option('--node <node_name>', 'Set autopilot on saved node')
|
|
.option('--url <url>', 'Follow nodes from a scoring URL', REPEATABLE)
|
|
.action((args, options, logger) => {
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
return setAutopilot({
|
|
request,
|
|
is_dry_run: !!options.dryrun,
|
|
is_enabled: args.status === 'on',
|
|
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
|
|
mirrors: flatten([options.mirror].filter(n => !!n)),
|
|
node: options.node,
|
|
urls: flatten([options.url].filter(n => !!n)),
|
|
},
|
|
returnObject({logger, reject, resolve}));
|
|
} catch (err) {
|
|
return reject(err);
|
|
}
|
|
});
|
|
})
|
|
|
|
// Get local balance information
|
|
.command('balance', 'Get total tokens')
|
|
.help('Sums balances on-chain, in channels, and pending, minus commit fees')
|
|
.option('--above <tokens>', 'Return tokens above watermark', INT)
|
|
.option('--below <tokens>', 'Return tokens below watermark', INT)
|
|
.option('--confirmed', 'Return confirmed funds only')
|
|
.option('--node <node_name>', 'Node to get balance for')
|
|
.option('--offchain', 'List only off-chain tokens')
|
|
.option('--onchain', 'List only on-chain tokens')
|
|
.action((args, options, logger) => {
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
return getBalance({
|
|
above: options.above,
|
|
below: options.below,
|
|
is_confirmed: !!options.confirmed,
|
|
is_offchain_only: !!options.offchain,
|
|
is_onchain_only: !!options.onchain,
|
|
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
|
|
node: options.node,
|
|
},
|
|
returnNumber({logger, reject, resolve, number: 'balance'}));
|
|
} catch (err) {
|
|
return reject(err);
|
|
}
|
|
});
|
|
})
|
|
|
|
// Get the number of days until the RPC cert expires
|
|
.command('cert-validity-days', 'Number of days until the cert is invalid')
|
|
.option('--below <number_of_days>', 'Return number of days below mark', INT)
|
|
.option('--node <node_name>', 'Node to check cert on')
|
|
.action((args, options, logger) => {
|
|
return new Promise(async (resolve, reject) => {
|
|
return getCertValidityDays({
|
|
logger,
|
|
below: options.below,
|
|
node: options.node,
|
|
},
|
|
returnNumber({logger, reject, resolve, number: 'days'}));
|
|
});
|
|
})
|
|
|
|
// Deposit coins
|
|
.command('chain-deposit', 'Deposit coins in the on-chain wallet')
|
|
.argument('[amount]', 'Amount to receive', INT)
|
|
.option('--node <node_name>', 'Node to deposit coins to')
|
|
.action((args, options, logger) => {
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
return getDepositAddress({
|
|
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
|
|
tokens: args.amount,
|
|
},
|
|
returnObject({logger, reject, resolve}));
|
|
} catch (err) {
|
|
return reject(err);
|
|
}
|
|
});
|
|
})
|
|
|
|
// Create on-chain submarine swap
|
|
.command('chain-receive', 'Receive funds on-chain via submarine swap')
|
|
.argument('[amount]', 'Amount to receive', INT, 1e6)
|
|
.option('--max-fee <max_fee>', 'Max fee in tokens to pay', INT, 4000)
|
|
.option('--no-color', 'Mute all colors')
|
|
.option('--node <node_name>', 'Node to receive funds on')
|
|
.option('--recovery <refund_recovery>', 'Attempt refund of swap')
|
|
.option('--refund-address <refund_address>', 'Refund address for swap')
|
|
.option('--test-refund', 'Reduce refund timeout height to test refund')
|
|
.action((args, options, logger) => {
|
|
return new Promise((resolve, reject) => {
|
|
return swapIn({
|
|
logger,
|
|
is_refund_test: options.testRefund,
|
|
max_fee: options.maxFee,
|
|
node: options.node,
|
|
recovery: options.recovery,
|
|
refund_address: options.refundAddress,
|
|
tokens: args.amount,
|
|
},
|
|
returnObject({exit, logger, reject, resolve}));
|
|
});
|
|
})
|
|
|
|
// Get the current chain fee rates
|
|
.command('chainfees', 'Get the current chain fee estimates')
|
|
.help('Lookup chain fee estimates at various confirm targets')
|
|
.option('--blocks <depth>', 'Blocks confirm target depth to estimate to')
|
|
.option('--file <path>', 'Write the output to a JSON file at desired path')
|
|
.option('--no-color', 'Mute all colors')
|
|
.option('--node <node_name>', 'Node to get chain fees view from')
|
|
.action((args, options, logger) => {
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
return getChainFees({
|
|
blocks: options.blocks,
|
|
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
|
|
},
|
|
returnObject({
|
|
logger,
|
|
reject,
|
|
resolve,
|
|
file: options.file,
|
|
write: writeFile,
|
|
}));
|
|
} catch (err) {
|
|
return reject(err);
|
|
}
|
|
});
|
|
})
|
|
|
|
// Show a chart of fees earned
|
|
.command('chart-fees-earned', 'Get a chart of earned routing fees')
|
|
.argument('[via_peer]', 'Routing fees earned via a specified peer')
|
|
.help('Show the routing fees earned')
|
|
.option('--days <days>', 'Chart fees over the past number of days', INT, 60)
|
|
.option('--node <node_name>', 'Get fee earnings chart for saved node')
|
|
.action((args, options, logger) => {
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
return getFeesChart({
|
|
days: options.days,
|
|
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
|
|
via: args.viaPeer,
|
|
},
|
|
returnChart({logger, reject, resolve, data: 'fees'}));
|
|
} catch (err) {
|
|
return reject(err);
|
|
}
|
|
});
|
|
})
|
|
|
|
// Determine the outcomes of channel closings
|
|
.command('closed', 'Get the status of a channel closings')
|
|
.help('Channel closes with chain-transaction derived resolution details')
|
|
.option('--limit [limit]', 'Limit of closings to get', INT, 20)
|
|
.option('--no-color', 'Mute all colors')
|
|
.option('--node <node_name>', 'Get channel closes from saved node')
|
|
.action((args, options, logger) => {
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
return getChannelCloses({
|
|
limit: options.limit,
|
|
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
|
|
},
|
|
returnObject({logger, reject, resolve}));
|
|
} catch (err) {
|
|
return reject(err);
|
|
}
|
|
});
|
|
})
|
|
|
|
// Export LND credentials from the standard
|
|
.command('credentials', 'Export local credentials')
|
|
.help('Output encrypted remote access credentials. Use with "nodes --add"')
|
|
.option('--cleartext', 'Output remote access credentials without encryption')
|
|
.option('--node <node_name>', 'Get credentials for a saved node')
|
|
.action((args, options, logger) => {
|
|
return new Promise((resolve, reject) => {
|
|
return getCredentials({
|
|
logger,
|
|
ask: (n, cbk) => inquirer.prompt([n]).then(res => cbk(res)),
|
|
is_cleartext: options.cleartext,
|
|
node: options.node,
|
|
},
|
|
returnObject({logger, reject, resolve}));
|
|
});
|
|
})
|
|
|
|
// Fan out utxos
|
|
.command('fanout', 'Fan out utxos')
|
|
.argument('<size>', 'UTXO minimum size', INT)
|
|
.argument('<count>', 'Desired number of total utxos', INT)
|
|
.help('Make a bunch of utxos by making a tx with a bunch of outputs')
|
|
.option('--confirmed', 'Only consider confirmed existing utxos')
|
|
.option('--dryrun', 'Execute a fan-out dry run')
|
|
.option('--feerate <feerate>', 'Feerate in per vbyte rate', INT)
|
|
.option('--no-color', 'Mute all colors')
|
|
.option('--node <node_name>', 'Node to do fan out for')
|
|
.action((args, options, logger) => {
|
|
return new Promise((resolve, reject) => {
|
|
return splitUtxos({
|
|
count: args.count,
|
|
is_confirmed: !!options.confirmed,
|
|
is_dry_run: !!options.dryrun,
|
|
node: options.node,
|
|
size: args.size,
|
|
tokens_per_vbyte: options.feerate,
|
|
},
|
|
returnObject({logger, reject, resolve}));
|
|
});
|
|
})
|
|
|
|
// Query the node to search for something
|
|
.command('find', 'Find a record')
|
|
.help('Look for something in the node db that matches a query')
|
|
.argument('<query>', 'Query for a record')
|
|
.option('--no-color', 'Mute all colors')
|
|
.option('--node <node_name>', 'Node to find record on')
|
|
.action((args, options, logger) => {
|
|
try {
|
|
return new Promise(async (resolve, reject) => {
|
|
return findRecord({
|
|
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
|
|
query: args.query,
|
|
},
|
|
returnObject({logger, reject, resolve}));
|
|
});
|
|
} catch (err) {
|
|
return reject(err);
|
|
}
|
|
})
|
|
|
|
// Get forwards
|
|
.command('forwards', 'Get forwards')
|
|
.help('Peers where routing has taken place from inbound and outbound sides')
|
|
.option('--days <days>', 'Number of past days to evaluate', INT)
|
|
.option('--no-color', 'Mute all colors')
|
|
.option('--node <node_name>', 'Node to get forwards for')
|
|
.action((args, options, logger) => {
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
return getForwards({
|
|
days: options.days,
|
|
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
|
|
},
|
|
returnObject({logger, reject, resolve}));
|
|
} catch (err) {
|
|
return reject(err);
|
|
}
|
|
});
|
|
})
|
|
|
|
// Help with funding
|
|
.command('fund-dev', 'Fund people related to Bitcoin and Lightning efforts')
|
|
.help('Send some sats (805 default) to a worthy individual. For the cause!')
|
|
.help('Someone will be automatically chosen if no @handle specified')
|
|
.help('Nominate worthiness via GitHub issues, include proof of your support')
|
|
.option('--amount <amount>', 'Choose amount to send', INT, 805)
|
|
.option('--dryrun', 'Show what would happen without actually sending funds')
|
|
.option('--node <node_name>', 'Saved node to give funds from')
|
|
.option('--twitter-handle <nick>', 'Specifically support someone')
|
|
.action(async (args, options, logger) => {
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
return fundDev({
|
|
logger,
|
|
request,
|
|
is_dry_run: options.dryrun,
|
|
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
|
|
to_twitter_account: options.twitterHandle,
|
|
tokens: options.amount,
|
|
},
|
|
returnObject({logger, reject, resolve}));
|
|
} catch (err) {
|
|
return reject(err);
|
|
}
|
|
});
|
|
})
|
|
|
|
// Give a peer some tokens
|
|
.command('gift', 'Give a direct peer some free funds off-chain')
|
|
.help('Send some funds to a connected peer')
|
|
.argument('<target>', 'Peer to give funds to')
|
|
.argument('<amount>', 'Tokens to give', INT)
|
|
.option('--node <node_name>', 'Source node to use to pay gift')
|
|
.action((args, options, logger) => {
|
|
try {
|
|
return new Promise(async (resolve, reject) => {
|
|
return sendGift({
|
|
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
|
|
to: args.target,
|
|
tokens: args.amount,
|
|
},
|
|
returnNumber({logger, reject, resolve, number: 'gave_tokens'}));
|
|
});
|
|
} catch (err) {
|
|
return reject(err);
|
|
}
|
|
})
|
|
|
|
// Get inbound liquidity information: available inbound off-chain tokens
|
|
.command('inbound-liquidity', 'Get inbound liquidity size')
|
|
.option('--above <tokens>', 'Return amount above watermark', INT)
|
|
.option('--below <tokens>', 'Return amount above watermark', INT)
|
|
.option('--node <node_name>', 'Node to get inbound liquidity')
|
|
.option('--top', 'Top percentile inbound liquidity in an individual channel')
|
|
.option('--with', 'Liquidity with a specific node')
|
|
.action((args, options, logger) => {
|
|
try {
|
|
return new Promise(async (resolve, reject) => {
|
|
return getLiquidity({
|
|
above: options.above || undefined,
|
|
below: options.below || undefined,
|
|
is_top: options.top || undefined,
|
|
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
|
|
with: options.with || undefined,
|
|
},
|
|
returnNumber({logger, reject, resolve, number: 'balance'}));
|
|
});
|
|
} catch (err) {
|
|
return reject(err);
|
|
}
|
|
})
|
|
|
|
// Increase inbound liquidity
|
|
.command('increase-inbound-liquidity', 'Increase node inbound liquidity')
|
|
.help('Spend down a channel to get inbound. Fee is an estimate, may be more')
|
|
.option('--address <out_address>', 'Out chain address to send funds out to')
|
|
.option('--avoid <pubkey>', 'Avoid forwarding through node', REPEATABLE)
|
|
.option('--amount <amount>', 'Amount to increase liquidity', INT, 5e5)
|
|
.option('--confs <confs>', 'Confs to consider reorg safe', INT, 1)
|
|
.option('--dryrun', 'Only show cost estimate for increase')
|
|
.option('--fast', 'Request swap server avoid batching delay')
|
|
.option('--max-fee <max_fee>', 'Maximum estimated fee tokens', INT, 3000)
|
|
.option('--max-hours <max_hours>', 'Maximum hours to wait', INT, 24)
|
|
.option('--no-color', 'Mute all colors')
|
|
.option('--node <node_name>', 'Increase inbound liquidity on saved node')
|
|
.option('--recovery <recovery>', 'Recover in-progress swap')
|
|
.option('--show-raw-recovery', 'Show raw recovery transactions')
|
|
.option('--with <peer>', 'Public key of peer to increase liquidity from')
|
|
.action((args, options, logger) => {
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
return swapOut({
|
|
logger,
|
|
avoid: flatten([options.avoid].filter(n => !!n)),
|
|
confs: options.confs,
|
|
is_fast: options.fast || false,
|
|
is_raw_recovery_shown: options.showRawRecovery || undefined,
|
|
is_dry_run: options.dryrun || false,
|
|
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
|
|
max_fee: options.maxFee,
|
|
max_wait_blocks: Math.ceil((options.maxHours) * 60 / 10),
|
|
node: options.node || undefined,
|
|
out_address: options.address || undefined,
|
|
peer: options.with || undefined,
|
|
recovery: options.recovery,
|
|
timeout: 1000 * 60 * 60 * 10,
|
|
tokens: options.amount,
|
|
},
|
|
returnObject({exit, logger, reject, resolve}));
|
|
} catch (err) {
|
|
return reject(err);
|
|
}
|
|
});
|
|
})
|
|
|
|
// Increase outbound liquidity
|
|
.command('increase-outbound-liquidity')
|
|
.help('Open a new channel to add more off-chain liquidity')
|
|
.option('--amount <amount>', 'Amount to assign to new channel capacity', INT)
|
|
.option('--dryrun', 'Avoid actually opening a channel')
|
|
.option('--fee-rate <fee_rate>', 'Use specific fee rate (per vbyte)', FLOAT)
|
|
.option('--node <node_name>', 'Increase outbound liquidity on saved node')
|
|
.option('--with <peer_public_key>', 'Select a specific peer to open with')
|
|
.action((args, options, logger) => {
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
return openChannel({
|
|
logger,
|
|
request,
|
|
chain_fee_rate: options.feeRate || undefined,
|
|
is_dry_run: options.dryrun,
|
|
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
|
|
peer: options.with || undefined,
|
|
tokens: options.amount,
|
|
},
|
|
returnObject({logger, reject, resolve}));
|
|
} catch (err) {
|
|
return reject(err);
|
|
}
|
|
});
|
|
})
|
|
|
|
// Get the price for liquidity
|
|
.command('liquidity-cost', 'Get the price of liquidity')
|
|
.argument('<type>', 'Liquidity direction', swapTypes)
|
|
.argument('<amount>', 'Amount of liquidity to get quote for', INT)
|
|
.option('--above <tokens>', 'Return amount above watermark', INT)
|
|
.option('--node <node_name>', 'Node to get liquidity cost')
|
|
.action((args, options, logger) => {
|
|
return new Promise(async (resolve, reject) => {
|
|
return getSwapCost({
|
|
above: options.above,
|
|
service: (await getSwapService({logger, node: options.node})).service,
|
|
tokens: args.amount,
|
|
type: args.type,
|
|
},
|
|
returnNumber({logger, reject, resolve, number: 'cost'}));
|
|
});
|
|
})
|
|
|
|
// Get the history of prices
|
|
.command('market', 'Get the history of prices on a market')
|
|
.help('A chart of historic prices. Note: SuredBits price history has a fee!')
|
|
.help(`Pairs: ${Array.from(new Set(marketPairs.map(n => n.toUpperCase())))}`)
|
|
.help(`Markets: ${Array.from(new Set(exchanges.map(n => n.toLowerCase())))}`)
|
|
.argument('[pair]', 'Market pair', marketPairs, 'BTCUSD')
|
|
.argument('[exchange]', 'Exchange', exchanges, 'kraken')
|
|
.option('--max-fee <max_fee>', 'Maximum amount to pay for data', INT, 5)
|
|
.option('--node <node_name>', 'Node to use to pay for price data')
|
|
.action((args, options, logger) => {
|
|
return new Promise((resolve, reject) => {
|
|
return getPriceChart({
|
|
exchange: args.exchange.toLowerCase(),
|
|
fee: options.maxFee,
|
|
node: options.node,
|
|
pair: args.pair.toLowerCase(),
|
|
},
|
|
returnChart({logger, reject, resolve, data: 'prices'}));
|
|
});
|
|
})
|
|
|
|
// Saved nodes
|
|
.command('nodes', 'List and edit saved nodes')
|
|
.help('Locking and unlocking requires existing installation of GPG')
|
|
.argument('[node]', 'Specify a saved node')
|
|
.option('--add', 'Add a new node')
|
|
.option('--lock <id>', 'Encrypt node authentication to GPG key>', REPEATABLE)
|
|
.option('--remove', 'Remove saved node')
|
|
.option('--unlock', 'Remove encryption from auth macaroon')
|
|
.action((args, options, logger) => {
|
|
return new Promise((resolve, reject) => {
|
|
return adjustSavedNodes({
|
|
logger,
|
|
spawn,
|
|
ask: (n, cbk) => inquirer.prompt([n]).then(res => cbk(res)),
|
|
fs: {
|
|
writeFile,
|
|
getDirectoryFiles: readdir,
|
|
getFile: readFile,
|
|
getFileStatus: lstat,
|
|
makeDirectory: mkdir,
|
|
removeDirectory: rmdir,
|
|
removeFile: unlink,
|
|
},
|
|
is_registering: options.add || undefined,
|
|
is_removing: options.remove || undefined,
|
|
is_unlocking: options.unlock || undefined,
|
|
lock_credentials_to: flatten([options.lock].filter(n => !!n)),
|
|
node: args.node || undefined,
|
|
},
|
|
returnObject({logger, reject, resolve}));
|
|
});
|
|
})
|
|
|
|
// Get outbound liquidity information: available outbound off-chain tokens
|
|
.command('outbound-liquidity', 'Get outbound liquidity size')
|
|
.option('--above <tokens>', 'Return amount above watermark', INT)
|
|
.option('--below <tokens>', 'Return amount above watermark', INT)
|
|
.option('--node <node_name>', 'Node to get outbound liquidity amount')
|
|
.option('--top', 'Top percentile inbound liquidity in an individual channel')
|
|
.option('--with', 'Liquidity with a specific node')
|
|
.action((args, options, logger) => {
|
|
try {
|
|
return new Promise(async (resolve, reject) => {
|
|
return getLiquidity({
|
|
above: options.above || undefined,
|
|
below: options.below || undefined,
|
|
is_outbound: true,
|
|
is_top: options.top || undefined,
|
|
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
|
|
with: options.with || undefined,
|
|
},
|
|
returnNumber({logger, reject, resolve, number: 'balance'}));
|
|
});
|
|
} catch (err) {
|
|
return reject(err);
|
|
}
|
|
})
|
|
|
|
// Pay a payment request, probing first
|
|
.command('pay', 'Pay a payment request, probing first')
|
|
.argument('<request>', 'Payment Request')
|
|
.option('--avoid <pubkey>', 'Avoid forwarding through node', REPEATABLE)
|
|
.option('--in <public_key>', 'Route through specific peer of destination')
|
|
.option('--max-fee <max_fee>', 'Maximum fee to pay', INT, 1337)
|
|
.option('--no-color', 'Mute all colors')
|
|
.option('--node <node_name>', 'Node to use to pay payment request')
|
|
.option('--out <public_key>', 'Make first hop through specific peer')
|
|
.action((args, options, logger) => {
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
return probeDestination({
|
|
logger,
|
|
ignore: ignoreFromAvoid({avoid: options.avoid}).ignore,
|
|
in_through: options.in,
|
|
is_real_payment: true,
|
|
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
|
|
max_fee: options.maxFee,
|
|
node: options.node,
|
|
out_through: options.out,
|
|
request: args.request,
|
|
},
|
|
returnObject({logger, reject, resolve}));
|
|
} catch (err) {
|
|
return reject(err);
|
|
}
|
|
});
|
|
})
|
|
|
|
// Get a list of channel-connected peers
|
|
.command('peers', 'Get a list of channel-connected peers')
|
|
.option('--active', 'Only active peer channels')
|
|
.option('--fee-days <past_days>', 'Include fees earned over n days', INT)
|
|
.option('--inbound-below <amount>', 'Inbound liquidity below amount')
|
|
.option('--node <node_name>', 'Node to get peers for')
|
|
.option('--offline', 'Only offline peer channels')
|
|
.option('--outbound-below <amount>', 'Outbound liquidity below amount', INT)
|
|
.option('--no-color', 'Mute all colors')
|
|
.option('--private', 'Only private channels')
|
|
.option('--public', 'Only peers with public channels')
|
|
.option('--sort', 'Sort results by peer attribute')
|
|
.action((args, options, logger) => {
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
return getPeers({
|
|
earnings_days: options.feeDays,
|
|
inbound_liquidity_below: options.inboundBelow,
|
|
is_active: !!options.active,
|
|
is_offline: !!options.offline,
|
|
is_private: !!options.private,
|
|
is_public: !!options.public,
|
|
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
|
|
node: options.node,
|
|
outbound_liquidity_below: options.outboundBelow,
|
|
sort_by: options.sort,
|
|
},
|
|
returnObject({logger, reject, resolve}));
|
|
} catch (err) {
|
|
return reject(err);
|
|
}
|
|
});
|
|
})
|
|
|
|
// Get the current fiat exchange price
|
|
.command('price', 'Get the price')
|
|
.argument('[symbols...]', 'Desired fiat tickers')
|
|
.option('--file <path>', 'Write the output to a JSON file at desired path')
|
|
.option('--no-color', 'Mute all colors')
|
|
.action((args, options, logger) => {
|
|
return new Promise((resolve, reject) => {
|
|
return getExchangeRates({
|
|
symbols: args.symbols.map(n => n.toUpperCase()),
|
|
},
|
|
returnObject({
|
|
logger,
|
|
reject,
|
|
resolve,
|
|
file: options.file,
|
|
write: writeFile,
|
|
}));
|
|
});
|
|
})
|
|
|
|
// Determine if a payment request is sendable
|
|
.command('probe', 'Check if a payment request is sendable')
|
|
.help('Simulate paying a payment request without actually paying it')
|
|
.argument('<to>', 'Payment request or node public key')
|
|
.argument('[amount]', 'Amount to probe, default: request amount', INT)
|
|
.option('--avoid <pubkey>', 'Avoid forwarding through node', REPEATABLE)
|
|
.option('--find-max', 'Find the maximum routeable amount on success route')
|
|
.option('--in <public_key>', 'Route through specific peer of destination')
|
|
.option('--no-color', 'Mute all colors')
|
|
.option('--node <node_name>', 'Node to use for payment request check')
|
|
.option('--out <public_key>', 'Make first hop through specific peer')
|
|
.action((args, options, logger) => {
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
return probeDestination({
|
|
logger,
|
|
destination: args.to.length === 66 ? args.to : undefined,
|
|
find_max: !!options.findMax ? 1e7 : undefined,
|
|
ignore: ignoreFromAvoid({avoid: options.avoid}).ignore,
|
|
in_through: options.in || undefined,
|
|
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
|
|
node: options.node || undefined,
|
|
out_through: options.out || undefined,
|
|
request: args.to.length !== 66 ? args.to : undefined,
|
|
tokens: args.amount || undefined,
|
|
},
|
|
returnObject({logger, reject, resolve}));
|
|
} catch (err) {
|
|
return reject(err);
|
|
}
|
|
});
|
|
})
|
|
|
|
// Rebalance between peers
|
|
.command('rebalance', 'Rebalance funds between peers')
|
|
.help('Change the liquidity profile of two peers')
|
|
.option('--avoid <pubkey>', 'Avoid forwarding through node', REPEATABLE)
|
|
.option('--in <public_key>', 'Route through specific peer of destination')
|
|
.option('--max-fee <max_fee>', 'Maximum fee to pay', INT)
|
|
.option('--max-fee-rate <max_fee_rate>', 'Max fee rate to pay', INT)
|
|
.option('--no-color', 'Mute all colors')
|
|
.option('--node <node_name>', 'Node to use for payment request check')
|
|
.option('--out <public_key>', 'Make first hop through specific peer')
|
|
.action((args, options, logger) => {
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
return rebalance({
|
|
logger,
|
|
avoid: flatten([options.avoid].filter(n => !!n)),
|
|
in_through: options.in || undefined,
|
|
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
|
|
max_fee: options.maxFee,
|
|
max_fee_rate: options.maxFeeRate,
|
|
node: options.node || undefined,
|
|
out_through: options.out || undefined,
|
|
},
|
|
returnObject({exit, logger, reject, resolve}));
|
|
} catch (err) {
|
|
return reject(err);
|
|
}
|
|
});
|
|
})
|
|
|
|
// See a general report about the node
|
|
.command('report', 'Report about the node')
|
|
.option('--node <node_name>', 'Node to get report for')
|
|
.option('--styled', 'Add unicode styling to report')
|
|
.action((args, options, logger) => {
|
|
return new Promise((resolve, reject) => {
|
|
return getReport({
|
|
node: options.node,
|
|
style: !!options.styled ? 'styled' : undefined,
|
|
},
|
|
returnOutput({logger, reject, resolve}));
|
|
});
|
|
})
|
|
|
|
// Link up Telegram bot
|
|
.command('telegram', 'Post updates to a Telegram bot')
|
|
.help('Connect to a Telegram bot. Create bot: tg://resolve?domain=botfather')
|
|
.help('Supported updates: forwards, received payments')
|
|
.option('--budget <amount>', 'Spending amount to allow', INT, 0)
|
|
.option('--connect <connect_code>', 'Connection code', INT)
|
|
.option('--node <node_name>', 'Node to connect to Telegram', REPEATABLE)
|
|
.action((args, options, logger) => {
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
const nodes = flatten([options.node].filter(n => !!n)).map(node => {
|
|
return authenticatedLnd({logger, node});
|
|
});
|
|
|
|
return startTelegramBot({
|
|
logger,
|
|
request,
|
|
fs: {writeFile, getFile: readFile, makeDirectory: mkdir},
|
|
id: options.connect,
|
|
lnds: (await Promise.all(nodes)).map(({lnd}) => lnd),
|
|
payments: {limit: options.budget}
|
|
},
|
|
returnObject({logger, reject, resolve}));
|
|
} catch (err) {
|
|
return reject(err);
|
|
}
|
|
});
|
|
})
|
|
|
|
// Unlock wallet
|
|
.command('unlock', 'Unlock wallet if locked')
|
|
.help('Check if the wallet is locked, if so use a password file to unlock')
|
|
.argument('<path_to_password_file>', 'Path to password file')
|
|
.option('--no-color', 'Mute all colors')
|
|
.option('--node <node_name>', 'Node to unlock')
|
|
.action((args, options, logger) => {
|
|
return new Promise((resolve, reject) => {
|
|
return unlockWallet({
|
|
node: options.node,
|
|
path_to_password_file: args.pathToPasswordFile,
|
|
},
|
|
returnObject({logger, reject, resolve}));
|
|
});
|
|
})
|
|
|
|
// Get utxos
|
|
.command('utxos', 'Get a list of utxos')
|
|
.option('--confirmed', 'Return only confirmed utxos')
|
|
.option('--count', 'Return the count of utxos')
|
|
.option('--count-below <below>', 'Return only count below number', INT)
|
|
.option('--no-color', 'Mute all colors')
|
|
.option('--node <node_name>', 'Node to get utxos for')
|
|
.option('--size', 'Utxos of size greater than or equal to specified amount')
|
|
.action((args, options, logger) => {
|
|
return new Promise(async (resolve, reject) => {
|
|
try {
|
|
return getUtxos({
|
|
count_below: options.countBelow,
|
|
is_confirmed: !!options.confirmed,
|
|
is_count: !!options.count,
|
|
lnd: (await authenticatedLnd({logger, node: options.node})).lnd,
|
|
min_tokens: options.size || undefined,
|
|
node: options.node,
|
|
},
|
|
returnObject({logger, reject, resolve}));
|
|
} catch (err) {
|
|
return reject(err);
|
|
}
|
|
});
|
|
});
|
|
|
|
prog.parse(process.argv);
|