balanceofsatoshis 24.2.0 → 24.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Versions
2
2
 
3
+ ## 24.2.1
4
+
5
+ - `open`: Adhere to BIP 69 when ordering funding outputs with equal amounts
6
+
3
7
  ## 24.2.0
4
8
 
5
9
  - Add support for LND 0.21.4 and LND 0.20.5
package/package.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "url": "https://github.com/alexbosworth/balanceofsatoshis/issues"
16
16
  },
17
17
  "dependencies": {
18
- "@alexbosworth/blockchain": "4.9.0",
18
+ "@alexbosworth/blockchain": "4.10.0",
19
19
  "@alexbosworth/caporal": "2.0.0",
20
20
  "@alexbosworth/fiat": "2.0.0",
21
21
  "@alexbosworth/formulas": "1.5.0",
@@ -38,7 +38,7 @@
38
38
  "ini": "7.0.0",
39
39
  "inquirer": "14.2.2",
40
40
  "ln-accounting": "10.0.7",
41
- "ln-service": "60.1.1",
41
+ "ln-service": "60.1.2",
42
42
  "ln-sync": "8.0.6",
43
43
  "ln-telegram": "8.0.6",
44
44
  "moment": "2.31.0",
@@ -54,7 +54,7 @@
54
54
  "description": "Lightning balance CLI",
55
55
  "devDependencies": {
56
56
  "invoices": "6.2.0",
57
- "ln-docker-daemons": "8.0.5"
57
+ "ln-docker-daemons": "8.1.0"
58
58
  },
59
59
  "engines": {
60
60
  "node": ">=22"
@@ -105,5 +105,5 @@
105
105
  "postpublish": "docker buildx build --platform linux/amd64,linux/arm64 -t alexbosworth/balanceofsatoshis -t alexbosworth/balanceofsatoshis:$npm_package_version --push .",
106
106
  "test": "node --experimental-test-coverage --test test/arrays/*.js test/balances/*.js test/chain/*.js test/display/*.js test/encryption/*.js test/lnd/*.js test/network/*.js test/nodes/*.js test/offchain/*.js test/peers/*.js test/responses/*.js test/routing/*.js test/services/*.js test/swaps/*.js test/tags/*.js test/telegram/*.js test/wallets/*.js"
107
107
  },
108
- "version": "24.2.0"
108
+ "version": "24.2.1"
109
109
  }
@@ -2,7 +2,6 @@ const {randomBytes} = require('crypto');
2
2
 
3
3
  const {acceptsChannelOpen} = require('ln-sync');
4
4
  const {addPeer} = require('ln-service');
5
- const {address} = require('bitcoinjs-lib');
6
5
  const {askForFeeRate} = require('ln-sync');
7
6
  const asyncAuto = require('async/auto');
8
7
  const asyncEach = require('async/each');
@@ -14,6 +13,7 @@ const asyncReflect = require('async/reflect');
14
13
  const asyncRetry = require('async/retry');
15
14
  const {broadcastChainTransaction} = require('ln-service');
16
15
  const {cancelPendingChannel} = require('ln-service');
16
+ const {decodeBech32Address} = require('@alexbosworth/blockchain');
17
17
  const {fundPendingChannels} = require('ln-service');
18
18
  const {getChainBalance} = require('ln-service');
19
19
  const {getChannels} = require('ln-service');
@@ -23,12 +23,13 @@ const {getNode} = require('ln-service');
23
23
  const {getPeers} = require('ln-service');
24
24
  const {getPsbtFromTransaction} = require('goldengate');
25
25
  const {getWalletVersion} = require('ln-service');
26
+ const {idForTransaction} = require('@alexbosworth/blockchain');
26
27
  const {openChannels} = require('ln-service');
27
28
  const {maintainUtxoLocks} = require('ln-sync');
28
29
  const moment = require('moment');
29
30
  const {parseAmount} = require('ln-accounting');
30
31
  const {returnResult} = require('asyncjs-util');
31
- const {Transaction} = require('bitcoinjs-lib');
32
+ const {scriptElementsAsScript} = require('@alexbosworth/blockchain');
32
33
  const {unlockUtxo} = require('ln-service');
33
34
 
34
35
  const adjustFees = require('./../routing/adjust_fees');
@@ -38,13 +39,14 @@ const {getAddressUtxo} = require('./../chain');
38
39
  const getChannelOutpoints = require('./get_channel_outpoints');
39
40
  const getPeersForNodes = require('./get_peers_for_nodes');
40
41
 
41
- const bech32AsData = bech32 => address.fromBech32(bech32).data;
42
42
  const description = 'bos open';
43
43
  const detectNetworks = ['btc', 'btctestnet'];
44
+ const encodeVersion = version => 80 + version;
44
45
  const featureAnchors = 'anchor_zero_fee_htlc_tx';
45
46
  const flatten = arr => [].concat(...arr);
46
47
  const format = 'p2wpkh';
47
- const {fromHex} = Transaction;
48
+ const hexAsBuffer = hex => Buffer.from(hex, 'hex');
49
+ const idOf = transaction => idForTransaction({transaction}).id;
48
50
  const interval = 1000;
49
51
  const {isArray} = Array;
50
52
  const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
@@ -623,15 +625,28 @@ module.exports = (args, cbk) => {
623
625
  return pending.map(n => ({address: n.address, tokens: n.tokens}));
624
626
  }));
625
627
 
628
+ // Map a bech32 address to its output script: OP_n then a data push
629
+ const scriptOf = address => {
630
+ const {program, version} = decodeBech32Address({address});
631
+
632
+ // Encode the version as a script number
633
+ const op = !version ? version : encodeVersion(version);
634
+
635
+ const {script} = scriptElementsAsScript({elements: [op, program]});
636
+
637
+ return hexAsBuffer(script);
638
+ };
639
+
626
640
  // Sort all the outputs using BIP 69
627
641
  try {
628
- pending.sort((a, b) => {
642
+ pending.sort((a, b) => {
629
643
  // Sort by tokens ascending when no tie breaker needed
630
644
  if (a.tokens !== b.tokens) {
631
645
  return a.tokens - b.tokens;
632
646
  }
633
647
 
634
- return bech32AsData(a.address).compare(bech32AsData(b.address));
648
+ // Break ties by comparing the output scripts as bytes
649
+ return scriptOf(a.address).compare(scriptOf(b.address));
635
650
  });
636
651
  } catch (err) {}
637
652
 
@@ -690,9 +705,7 @@ module.exports = (args, cbk) => {
690
705
  return cbk();
691
706
  }
692
707
 
693
- args.logger.info({
694
- funding_detected: Transaction.fromHex(foundTx).getId(),
695
- });
708
+ args.logger.info({funding_detected: idOf(foundTx)});
696
709
 
697
710
  return asyncEach(openChannels, ({lnd, node, pending}, cbk) => {
698
711
  return fundPendingChannels({
@@ -817,7 +830,7 @@ module.exports = (args, cbk) => {
817
830
  }
818
831
 
819
832
  const toOpen = flatten(openChannels.map(n => n.pending));
820
- const txId = fromHex(getFunding.value.transaction).getId();
833
+ const txId = idOf(getFunding.value.transaction);
821
834
 
822
835
  args.logger.info({confirming_pending_open: true});
823
836
 
@@ -0,0 +1,91 @@
1
+ const {sortBy} = require('./../arrays');
2
+
3
+ const attribute = 'receivable';
4
+ const feeBuffer = fees => fees * 2;
5
+ const {floor} = Math;
6
+ const htlcOutputSize = 43;
7
+ const {min} = Math;
8
+ const mtokensAsTokens = mtokens => Number(BigInt(mtokens) / BigInt(1e3));
9
+ const rate = n => n.commit_transaction_fee / (n.commit_transaction_weight / 4);
10
+
11
+ /** Choose a channel with a peer to receive a payment through
12
+
13
+ The peer forwards the payment to us over the channel, so the peer needs the
14
+ balance on their side to send it and their policy towards us must allow it.
15
+
16
+ The preferred channel is kept when it can receive the full amount, otherwise
17
+ the channel that can receive the most is chosen.
18
+
19
+ {
20
+ channels: [{
21
+ commit_transaction_fee: <Commit Transaction Fee Number>
22
+ commit_transaction_weight: <Commit Transaction Weight Number>
23
+ id: <Standard Format Channel Id String>
24
+ is_active: <Channel Active Bool>
25
+ is_partner_initiated: <Channel Partner Opened Channel Bool>
26
+ partner_public_key: <Peer Public Key Hex String>
27
+ policies: [{
28
+ [is_disabled]: <Channel Is Disabled Bool>
29
+ [max_htlc_mtokens]: <Maximum HTLC Millitokens Value String>
30
+ public_key: <Node Public Key Hex String>
31
+ }]
32
+ remote_balance: <Remote Balance Tokens Number>
33
+ remote_reserve: <Remote Reserved Tokens Number>
34
+ }]
35
+ [id]: <Preferred Channel Id String>
36
+ peer: <Peer Public Key Hex String>
37
+ tokens: <Tokens To Receive Number>
38
+ }
39
+
40
+ @returns
41
+ {
42
+ [id]: <Channel Id String>
43
+ [receivable]: <Receivable Tokens Number>
44
+ }
45
+ */
46
+ module.exports = ({channels, id, peer, tokens}) => {
47
+ const candidates = channels
48
+ .filter(n => n.partner_public_key === peer)
49
+ .filter(n => !!n.is_active)
50
+ .map(channel => {
51
+ const policy = channel.policies.find(n => n.public_key === peer);
52
+
53
+ // Exit early when the peer does not forward to us over this channel
54
+ if (!policy || !!policy.is_disabled) {
55
+ return;
56
+ }
57
+
58
+ // The partner needs to keep a buffer for fees when they pay for commits
59
+ const fees = !channel.is_partner_initiated ? Number() :
60
+ channel.commit_transaction_fee + htlcOutputSize * rate(channel);
61
+
62
+ const balance = channel.remote_balance - channel.remote_reserve;
63
+
64
+ const spendable = balance - feeBuffer(fees);
65
+
66
+ // The peer limits the size of HTLCs that they will forward to us
67
+ const maxHtlc = !policy.max_htlc_mtokens ? spendable :
68
+ mtokensAsTokens(policy.max_htlc_mtokens);
69
+
70
+ return {id: channel.id, receivable: floor(min(spendable, maxHtlc))};
71
+ })
72
+ .filter(n => !!n && n.receivable > Number());
73
+
74
+ const preferred = candidates.find(n => n.id === id);
75
+
76
+ // Exit early when the preferred channel can receive the full amount
77
+ if (!!preferred && preferred.receivable >= tokens) {
78
+ return {id, receivable: preferred.receivable};
79
+ }
80
+
81
+ // Exit early when no channel with the peer can receive anything
82
+ if (!candidates.length) {
83
+ return {};
84
+ }
85
+
86
+ const {sorted} = sortBy({attribute, array: candidates});
87
+
88
+ const [best] = sorted.slice().reverse();
89
+
90
+ return {id: best.id, receivable: best.receivable};
91
+ };
@@ -16,6 +16,7 @@ const {payViaRoutes} = require('ln-service');
16
16
  const {returnResult} = require('asyncjs-util');
17
17
  const {routeFromChannels} = require('ln-service');
18
18
 
19
+ const channelForReceive = require('./channel_for_receive');
19
20
  const findRebalancePeer = require('./find_rebalance_peer');
20
21
  const {formatFeeRate} = require('./../display');
21
22
  const {getIgnores} = require('./../routing');
@@ -599,10 +600,9 @@ module.exports = (args, cbk) => {
599
600
  cbk);
600
601
  }],
601
602
 
602
- // Get channels for the rebalance route
603
- channels: [
603
+ // Get channels for the probed rebalance route
604
+ getRouteChannels: [
604
605
  'findRoute',
605
- 'getInitialLiquidity',
606
606
  'getPublicKey',
607
607
  'lnd',
608
608
  ({findRoute, getPublicKey, lnd}, cbk) =>
@@ -633,13 +633,116 @@ module.exports = (args, cbk) => {
633
633
  cbk);
634
634
  }],
635
635
 
636
+ // Get the current channels with the inbound peer
637
+ getInboundChannels: [
638
+ 'getInbound',
639
+ 'getRouteChannels',
640
+ 'lnd',
641
+ ({getInbound, lnd}, cbk) =>
642
+ {
643
+ return getChannels({
644
+ lnd,
645
+ is_active: true,
646
+ partner_public_key: getInbound.public_key,
647
+ },
648
+ cbk);
649
+ }],
650
+
651
+ // Get the inbound peer channels together with their routing policies
652
+ inboundChannels: [
653
+ 'getInboundChannels',
654
+ 'lnd',
655
+ ({getInboundChannels, lnd}, cbk) =>
656
+ {
657
+ return asyncMap(getInboundChannels.channels, (channel, cbk) => {
658
+ return getChannel({lnd, id: channel.id}, (err, res) => {
659
+ // Policies are unknown when the channel is not found in the graph
660
+ const policies = !!err ? [] : res.policies;
661
+
662
+ return cbk(null, {
663
+ policies,
664
+ capacity: channel.capacity,
665
+ commit_transaction_fee: channel.commit_transaction_fee,
666
+ commit_transaction_weight: channel.commit_transaction_weight,
667
+ id: channel.id,
668
+ is_active: channel.is_active,
669
+ is_partner_initiated: channel.is_partner_initiated,
670
+ partner_public_key: channel.partner_public_key,
671
+ remote_balance: channel.remote_balance,
672
+ remote_reserve: channel.remote_reserve,
673
+ });
674
+ });
675
+ },
676
+ cbk);
677
+ }],
678
+
679
+ // Choose the final hop channel that has the inbound to receive the amount
680
+ finalHop: [
681
+ 'findRoute',
682
+ 'getInbound',
683
+ 'getPublicKey',
684
+ 'getRouteChannels',
685
+ 'inboundChannels',
686
+ ({
687
+ findRoute,
688
+ getInbound,
689
+ getPublicKey,
690
+ getRouteChannels,
691
+ inboundChannels,
692
+ },
693
+ cbk) =>
694
+ {
695
+ const [probed] = getRouteChannels.slice().reverse();
696
+ const inPeer = getInbound.public_key;
697
+ const maximum = findRoute.route_maximum;
698
+
699
+ const fromInPeer = probed.policies.find(n => n.public_key === inPeer);
700
+
701
+ // Exit early when the route does not end with the inbound peer to self
702
+ if (!fromInPeer || probed.destination !== getPublicKey.public_key) {
703
+ return cbk(null, {maximum, channel: probed});
704
+ }
705
+
706
+ // LND picks the final hop channel without regard to the peer balance
707
+ const {id, receivable} = channelForReceive({
708
+ channels: inboundChannels,
709
+ id: probed.id,
710
+ peer: inPeer,
711
+ tokens: maximum,
712
+ });
713
+
714
+ // Exit early when the probed final hop channel is used to receive
715
+ if (!id || id === probed.id) {
716
+ return cbk(null, {maximum, channel: probed});
717
+ }
718
+
719
+ const {capacity, policies} = inboundChannels.find(n => n.id === id);
720
+
721
+ // The amount is limited to what the switched channel can receive
722
+ return cbk(null, {
723
+ channel: {capacity, id, policies, destination: probed.destination},
724
+ maximum: min(maximum, receivable),
725
+ });
726
+ }],
727
+
728
+ // Channels for the rebalance route, ending with the chosen final hop
729
+ channels: [
730
+ 'finalHop',
731
+ 'getRouteChannels',
732
+ ({finalHop, getRouteChannels}, cbk) =>
733
+ {
734
+ const hops = getRouteChannels.slice(Number(), -1);
735
+
736
+ return cbk(null, hops.concat(finalHop.channel));
737
+ }],
738
+
636
739
  // Create local invoice
637
- invoice: ['channels', 'findRoute', 'lnd', ({findRoute, lnd}, cbk) => {
740
+ invoice: ['channels', 'finalHop', 'lnd', ({finalHop, lnd}, cbk) => {
638
741
  return createInvoice({
639
742
  lnd,
640
743
  cltv_delta: defaultCltvDelta,
641
744
  description: 'Rebalance',
642
- tokens: findRoute.route_maximum,
745
+ tokens: finalHop.maximum,
643
746
  },
644
747
  cbk);
645
748
  }],