UNPKG

balanceofsatoshis

Version:
308 lines (271 loc) 10.5 kB
const asyncAuto = require('async/auto'); const asyncReflect = require('async/reflect'); const {chanFormat} = require('bolt07'); const {formatTokens} = require('ln-sync'); const {getChannel} = require('ln-service'); const {getChannels} = require('ln-service'); const {getClosedChannels} = require('ln-service'); const {getHeight} = require('ln-service'); const {getNetworkGraph} = require('ln-service'); const {getNode} = require('ln-service'); const {getPayment} = require('ln-service'); const {getTransactionRecord} = require('ln-sync'); const {gray} = require('colorette'); const moment = require('moment'); const {returnResult} = require('asyncjs-util'); const {findKey} = require('ln-sync'); const asBigUnit = tokens => (tokens / 1e8).toFixed(8); const balance = ({display}) => display.trim() || gray('0.00000000'); const blocksTime = (n, p) => moment.duration(n * 10, 'minutes').humanize(p); const estimateDiskFootprint = n => `${(n.past_states*55/1e6).toFixed(2)}mb`; const {isArray} = Array; const isHash = n => !!n && /^[0-9A-F]{64}$/i.test(n); const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n); const notFound = 404; const standardIdHexLength = Buffer.alloc(32).toString('hex').length; /** Get record Try to find a record by id { lnd: <Authenticated LND API Object> query: <Query String> } @returns via cbk or Promise { [chain_transaction]: { [chain_fee]: <Paid Transaction Fee Tokens Number> [received]: <Received Tokens Number> related_channels: [{ action: <Channel Action String> [balance]: <Channel Balance Tokens Number> [capacity]: <Channel Capacity Value Number> [channel]: <Channel Standard Format Id String> [close_tx]: <Channel Closing Transaction Id Hex String> [open_tx]: <Channel Opening Transaction id Hex String> [timelock]: <Channel Funds Timelocked Until Height Number> with: <Channel Peer Public Key Hex String> }] [sent]: <Sent Tokens Number> [sent_to]: [<Sent to Address String>] [tx]: <Transaction Id Hex String> } [channels]: [<Channel Object>] [nodes]: [<Node Object>] [payment]: <Payment Object> [payment_failed]: <Payment Failed Object> [payment_pending]: <Payment Pending Bool> } */ module.exports = ({lnd, query}, cbk) => { return new Promise((resolve, reject) => { return asyncAuto({ // Check arguments validate: cbk => { if (!lnd) { return cbk([400, 'ExpectedLndObjectToFindRecord']); } if (!query) { return cbk([400, 'QueryExpectedToFindRecord']); } return cbk(); }, // Get channels getChannels: ['validate', ({}, cbk) => getChannels({lnd}, cbk)], // Get closed getClosed: ['validate', ({}, cbk) => getClosedChannels({lnd}, cbk)], // Determine the public key to use getKey: ['validate', asyncReflect(({}, cbk) => { if (query.length === standardIdHexLength) { return cbk(); } return findKey({lnd, query}, cbk); })], // Get graph getGraph: ['getKey', ({getKey}, cbk) => { if (query.length === standardIdHexLength) { return cbk(null, {channels: [], nodes: []}); } if (!!getKey.value) { return getNode({ lnd, public_key: getKey.value.public_key, }, (err, res) => { if (!!err) { return cbk(err); } return cbk(null, { channels: res.channels, nodes: [{ alias: res.alias, capacity: res.capacity, color: res.color, features: res.features, public_key: getKey.value.public_key, sockets: res.sockets.map(n => n.socket), updated_at: res.updated_at, }], }); }); } return getNetworkGraph({lnd}, cbk); }], // Get blockchain height getHeight: ['validate', ({}, cbk) => getHeight({lnd}, cbk)], // Payment getPayment: ['validate', ({}, cbk) => { if (query.length !== standardIdHexLength) { return cbk(null, {}); } return getPayment({lnd, id: query}, (err, payment) => { if (!!isArray(err) && err.slice().shift() === notFound) { return cbk(null, {}); } if (!!err) { return cbk(err); } return cbk(null, payment); }); }], // Transaction getTx: ['validate', ({}, cbk) => { if (!isHash(query)) { return cbk(); } return getTransactionRecord({lnd, id: query}, cbk); }], // Records records: [ 'getChannels', 'getClosed', 'getGraph', 'getHeight', 'getPayment', 'getTx', ({ getChannels, getClosed, getGraph, getHeight, getPayment, getTx, }, cbk) => { const nodes = getGraph.nodes .filter(node => { if (node.alias.toLowerCase().includes(query.toLowerCase())) { return true; } if (node.public_key.startsWith(query.toLowerCase())) { return true; } return node.public_key === query; }) .map(node => { const hasLargeChannels = !!node.features .find(n => n.type === 'large_channels'); return { alias: node.alias, capacity: asBigUnit(node.capacity || getGraph.channels.reduce( (sum, {capacity, policies}) => { if (!policies.find(n => n.public_key === node.public_key)) { return sum; } return sum + capacity; }, Number() )), is_accepting_large_channels: hasLargeChannels || undefined, public_key: node.public_key, updated: moment(node.updated_at).fromNow(), urls: node.sockets.map(socket => `${node.public_key}@${socket}`), past_channels: getClosed.channels .filter(n => n.partner_public_key === node.public_key) .filter(n => !!n.id) .filter(n => !!n.close_confirm_height) .map(chan => { const currentHeight = getHeight.current_block_height; const [height] = chan.id.split('x'); const isCoopClose = chan.is_cooperative_close; const removed = chan.close_confirm_height; const coopClose = isCoopClose && !chan.is_partner_closed; const peerCoopClose = isCoopClose && chan.is_partner_closed; return { age: blocksTime(removed - height), closed: blocksTime(removed - currentHeight, true), capacity: formatTokens({tokens: chan.capacity}).display, breach_closed: chan.is_breach_close || undefined, cooperative_closed: coopClose || undefined, force_closed: chan.is_local_force_close || undefined, peer_cooperatively_closed: peerCoopClose || undefined, peer_force_closed: chan.is_remote_force_close || undefined, }; }), connected_channels: getChannels.channels .filter(n => n.partner_public_key === node.public_key) .filter(n => !!n.id) .map(chan => { const [height] = chan.id.split('x'); const local = formatTokens({tokens: chan.local_balance}); const pending = chan.pending_payments; const remote = formatTokens({tokens: chan.remote_balance}); const inbound = `in: ${balance(remote)}`; const outbound = `out: ${balance(local)}`; return { age: blocksTime(getHeight.current_block_height - height), est_disk_usage: estimateDiskFootprint(chan), liquidity: `${inbound} | ${outbound}`, capacity: formatTokens({tokens: chan.capacity}).display, funding: `${chan.transaction_id} ${chan.transaction_vout}`, peer_created: chan.is_partner_initiated || undefined, pending_payments: !!pending.length ? pending : undefined, }; }), } }) .filter(node => node.capacity !== asBigUnit(0)); const channels = getGraph.channels .filter(channel => { try { if (channel.id === chanFormat({number: query}).channel) { return true; } } catch (err) {} return channel.id === query; }) .map(channel => { return { capacity: channel.capacity, id: channel.id, policies: channel.policies.map(policy => { const node = getGraph.nodes .find(n => n.public_key === policy.public_key); return { alias: !node ? undefined : node.alias, base_fee_mtokens: policy.base_fee_mtokens, cltv_delta: policy.cltv_delta, fee_rate: policy.fee_rate, is_disabled: policy.is_disabled, max_htlc_mtokens: policy.max_htlc_mtokens, min_htlc_mtokens: policy.min_htlc_mtokens, public_key: policy.public_key, }; }), transaction_id: channel.transaction_id, transaction_vout: channel.transaction_vout, updated_at: channel.updated_at, }; }); return cbk(null, { chain_transaction: !!getTx && !!getTx.tx ? getTx : undefined, channels: !!channels.length ? channels : undefined, nodes: !!nodes.length ? nodes : undefined, payment: getPayment.payment || undefined, payment_failed: getPayment.failed || undefined, payment_pending: getPayment.is_pending || undefined, }); }], }, returnResult({reject, resolve, of :'records'}, cbk)); }); };