balanceofsatoshis 23.1.11 → 23.2.0

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.
Files changed (127) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/bos +8 -1
  3. package/chain/closing_fees.js +4 -3
  4. package/chain/fund_transaction.js +6 -1
  5. package/display/describe_route.js +20 -9
  6. package/network/blinded_path_fee.js +61 -0
  7. package/network/execute_probe.js +45 -21
  8. package/network/pay.js +15 -1
  9. package/network/probe.js +21 -1
  10. package/network/probe_destination.js +101 -8
  11. package/network/push_payment.js +9 -2
  12. package/offchain/create_invoice.js +155 -7
  13. package/offchain/get_invoice_amount.js +10 -4
  14. package/offchain/relay_policy_issue.js +56 -0
  15. package/offchain/select_blinded_path.js +282 -0
  16. package/offchain/select_next_hop.js +224 -0
  17. package/offchain/sign_payment_request.js +46 -4
  18. package/package.json +34 -8
  19. package/.dockerignore +0 -5
  20. package/.gitattributes +0 -1
  21. package/.prettierignore +0 -2
  22. package/.tm_properties +0 -1
  23. package/.travis.yml +0 -15
  24. package/CONTRIBUTING.md +0 -95
  25. package/Dockerfile +0 -22
  26. package/test/arrays/test_shuffle.js +0 -43
  27. package/test/arrays/test_sort_by.js +0 -47
  28. package/test/balances/test_balance_from_tokens.js +0 -63
  29. package/test/balances/test_get_accounting_report.js +0 -127
  30. package/test/balances/test_get_balance.js +0 -66
  31. package/test/balances/test_get_liquidity.js +0 -61
  32. package/test/balances/test_is_matching_filters.js +0 -47
  33. package/test/balances/test_range_for_date.js +0 -57
  34. package/test/balances/test_table_rows_from_csv.js +0 -47
  35. package/test/chain/test_get_chain_fees.js +0 -84
  36. package/test/chain/test_get_channel_closes.js +0 -41
  37. package/test/chain/test_get_deposit_address.js +0 -57
  38. package/test/chain/test_get_mempool_size.js +0 -69
  39. package/test/chain/test_get_raw_transaction.js +0 -90
  40. package/test/chain/test_get_utxos.js +0 -137
  41. package/test/display/test_chart_alias_for_peer.js +0 -43
  42. package/test/display/test_describe_confidence.js +0 -47
  43. package/test/display/test_describe_route.js +0 -101
  44. package/test/display/test_segment_measure.js +0 -30
  45. package/test/encryption/fixtures.json +0 -4
  46. package/test/encryption/test_decrypt_ciphertext.js +0 -70
  47. package/test/encryption/test_der_as_pem.js +0 -32
  48. package/test/encryption/test_encrypt_to_public_keys.js +0 -75
  49. package/test/encryption/test_pem_as_der.js +0 -22
  50. package/test/fixtures/chan_info_response.json +0 -25
  51. package/test/fixtures/describe_graph_response.json +0 -36
  52. package/test/fixtures/get_info_response.json +0 -16
  53. package/test/fixtures/get_node_info_response.json +0 -37
  54. package/test/fixtures/index.js +0 -23
  55. package/test/fixtures/liquidity_channels_response.json +0 -68
  56. package/test/fixtures/list_channels_response.json +0 -47
  57. package/test/fixtures/list_peers_response.json +0 -16
  58. package/test/fixtures/pending_channels_response.json +0 -21
  59. package/test/fixtures/query_routes_response.json +0 -19
  60. package/test/fixtures/version_info_response.json +0 -6
  61. package/test/integration/test_fund_transaction.js +0 -38
  62. package/test/integration/test_open_balanced_channel.js +0 -283
  63. package/test/integration/test_open_channels.js +0 -120
  64. package/test/integration/test_remove_peer.js +0 -36
  65. package/test/integration/test_taproot_channels.js +0 -132
  66. package/test/integration/test_trusted_channels.js +0 -149
  67. package/test/integration.js +0 -18
  68. package/test/lnd/test_credential_restrictions.js +0 -76
  69. package/test/lnd/test_find_record.js +0 -142
  70. package/test/lnd/test_get_cert.js +0 -58
  71. package/test/lnd/test_get_macaroon.js +0 -53
  72. package/test/lnd/test_get_path.js +0 -57
  73. package/test/lnd/test_get_socket.js +0 -109
  74. package/test/lnd/test_lnd_directory.js +0 -81
  75. package/test/network/test_currency_for_network.js +0 -46
  76. package/test/network/test_get_peers.js +0 -311
  77. package/test/network/test_gift_callback_error.js +0 -48
  78. package/test/network/test_is_relevant_forwards.js +0 -80
  79. package/test/network/test_is_relevant_source.js +0 -80
  80. package/test/network/test_multi_probe.js +0 -121
  81. package/test/network/test_peers_with_activity.js +0 -68
  82. package/test/network/test_push_payment.js +0 -84
  83. package/test/network/test_reconnect.js +0 -110
  84. package/test/network/test_send_gift.js +0 -74
  85. package/test/network/test_set_autopilot.js +0 -37
  86. package/test/nodes/test_adjust_tags.js +0 -299
  87. package/test/nodes/test_delete_node_credentials.js +0 -55
  88. package/test/nodes/test_get_saved_credentials.js +0 -95
  89. package/test/nodes/test_get_saved_nodes.js +0 -136
  90. package/test/nodes/test_put_saved_credentials.js +0 -80
  91. package/test/nodes/test_register_node.js +0 -331
  92. package/test/peers/test_channels_from_arguments.js +0 -160
  93. package/test/peers/test_detect_open_rule_violation.js +0 -78
  94. package/test/peers/test_find_tag_match.js +0 -143
  95. package/test/peers/test_open_request_violation.js +0 -70
  96. package/test/responses/test_return_chart.js +0 -66
  97. package/test/responses/test_return_json.js +0 -44
  98. package/test/responses/test_return_number.js +0 -45
  99. package/test/responses/test_return_object.js +0 -107
  100. package/test/responses/test_return_output.js +0 -44
  101. package/test/responses/test_write_json_file.js +0 -59
  102. package/test/routing/test_channel_for_gift.js +0 -119
  103. package/test/routing/test_channels_from_hints.js +0 -76
  104. package/test/routing/test_fees_for_segment.js +0 -47
  105. package/test/routing/test_forwards_via_peer.js +0 -39
  106. package/test/routing/test_get_fees_chart.js +0 -111
  107. package/test/routing/test_get_maximum.js +0 -71
  108. package/test/routing/test_get_past_forwards.js +0 -61
  109. package/test/routing/test_gift_route.js +0 -151
  110. package/test/routing/test_ignore_from_avoid.js +0 -30
  111. package/test/routing/test_parse_fee_rate_formula.js +0 -69
  112. package/test/services/test_accept_balanced_channel.js +0 -186
  113. package/test/services/test_balanced_open_accept_details.js +0 -116
  114. package/test/services/test_get_balanced_opens.js +0 -174
  115. package/test/services/test_initiate_balanced_channel.js +0 -332
  116. package/test/swaps/test_append_failing_edge.js +0 -127
  117. package/test/swaps/test_channel_for_send.js +0 -50
  118. package/test/swaps/test_find_rebalance_peer.js +0 -329
  119. package/test/swaps/test_get_avoid_list.js +0 -62
  120. package/test/swaps/test_get_paid_service.js +0 -61
  121. package/test/swaps/test_get_swap_cost.js +0 -135
  122. package/test/swaps/test_swap_api_key.js +0 -63
  123. package/test/swaps/test_write_avoid_list.js +0 -83
  124. package/test/tags/test_get_tags.js +0 -99
  125. package/test/telegram/test_get_socks_proxy.js +0 -72
  126. package/test/wallets/test_channels_as_report_activity.js +0 -126
  127. package/test/wallets/test_report_overview.js +0 -73
package/CHANGELOG.md CHANGED
@@ -1,6 +1,16 @@
1
1
  # Versions
2
2
 
3
- ## 23.1.11
3
+ ## 23.2.0
4
+
5
+ - `invoice`: Add `--encrypted-hints` to hide the node behind blinded paths in
6
+ the payment request
7
+ - `invoice`: Add `--select-hints` with `--encrypted-hints` to choose the
8
+ blinded path outwards from a peer to an introduction node
9
+ - `invoice`: Add `--virtual-fee-rate` with selected `--encrypted-hints` to
10
+ charge a receiver fee on the blinded path
11
+ - `pay`, `probe`: Add support for payment requests with blinded paths
12
+
13
+ ## 23.1.12
4
14
 
5
15
  - Add support for LND 0.21.3
6
16
  - Add support for LND 0.20.4
package/bos CHANGED
@@ -1147,7 +1147,11 @@ prog
1147
1147
  .help('--virtual invoices cannot be used with payers who probe before pay')
1148
1148
  .help('Only one --virtual invoice can be active at a time')
1149
1149
  .help('--reject-on-amount-increase can only be used with --virtual invoices')
1150
+ .help('--encrypted-hints hides the node with blinded paths, needs an amount')
1151
+ .help('--encrypted-hints with --select-hints picks the path outwards by hop')
1152
+ .help('--virtual-fee-rate with selected --encrypted-hints is a receiver fee')
1150
1153
  .argument('[amount]', 'Amount for invoice', STRING, '0')
1154
+ .option('--encrypted-hints', 'Use blinded paths instead of hop hints')
1151
1155
  .option('--for <description>', 'What is the invoice requesting payment for')
1152
1156
  .option('--hours <expires_in>', 'Hours invoice is valid for', INT)
1153
1157
  .option('--include-hints', 'Include the default set of hop hint channels')
@@ -1157,7 +1161,7 @@ prog
1157
1161
  .option('--reject-on-amount-increase', 'Reject if required amount increases')
1158
1162
  .option('--select-hints', 'Select hop hints to be added to the request')
1159
1163
  .option('--virtual', 'Request payment over a virtual channel')
1160
- .option('--virtual-fee-rate <pm>', 'Fee rate to use on virtual channel', INT)
1164
+ .option('--virtual-fee-rate <pm>', 'Receiver fee rate on virtual/path', INT)
1161
1165
  .action((args, options, logger) => {
1162
1166
  return new Promise(async (resolve, reject) => {
1163
1167
  try {
@@ -1167,6 +1171,7 @@ prog
1167
1171
  ask: await commands.interrogate({}),
1168
1172
  description: options.for,
1169
1173
  expires_in: options.hours,
1174
+ is_encrypting_hints: options.encryptedHints || undefined,
1170
1175
  is_hinting: options.includeHints || undefined,
1171
1176
  is_including_qr: !!options.qr || undefined,
1172
1177
  is_rejecting_option: !!options.rejectOnAmountIncrease || undefined,
@@ -1551,6 +1556,7 @@ prog
1551
1556
 
1552
1557
  // Pay a payment request, probing first
1553
1558
  .command('pay', 'Pay a payment request, probing first')
1559
+ .help('Blinded path requests are probed to the introduction node, 1 path')
1554
1560
  .argument('<request>', 'Payment Request')
1555
1561
  .option('--avoid <avoid>', 'Avoid forwarding via node/chan/tag', REPEATABLE)
1556
1562
  .option('--avoid-high-fee-routes', 'Avoid evaluating too-high fee routes')
@@ -1670,6 +1676,7 @@ prog
1670
1676
  // Determine if a payment request is sendable
1671
1677
  .command('probe', 'Check if a payment request is sendable')
1672
1678
  .help('Simulate paying a payment request without actually paying it')
1679
+ .help('Blinded path requests are probed to the introduction node, 1 path')
1673
1680
  .argument('<to>', 'Payment request or node public key')
1674
1681
  .argument('[amount]', 'Amount to probe, default: request amount')
1675
1682
  .option('--avoid <avoid>', 'Avoid forwarding via node/chan/tag', REPEATABLE)
@@ -1,6 +1,5 @@
1
- const {Transaction} = require('bitcoinjs-lib');
1
+ const {componentsOfTransaction} = require('@alexbosworth/blockchain');
2
2
 
3
- const {fromHex} = Transaction;
4
3
  const sumOf = arr => arr.reduce((sum, n) => sum + n, Number());
5
4
 
6
5
  /** Calculate channel closing fees paid
@@ -40,7 +39,9 @@ module.exports = args => {
40
39
  }
41
40
 
42
41
  // Calculate the value of the outputs in the tx that spends the capacity
43
- const outValue = sumOf(fromHex(closer.transaction).outs.map(n => n.value));
42
+ const {outputs} = componentsOfTransaction({transaction: closer.transaction});
43
+
44
+ const outValue = sumOf(outputs.map(n => n.tokens));
44
45
 
45
46
  const fees = []
46
47
  .concat(args.capacity - outValue)
@@ -246,7 +246,12 @@ module.exports = (args, cbk) => {
246
246
  try {
247
247
  const outputs = args.addresses.map((address, i) => {
248
248
  const amount = args.amounts[i];
249
- const variables = {max: getMax.max_tokens};
249
+ const variables = {};
250
+
251
+ // The max variable is only defined when a maximum is calculated
252
+ if (getMax.max_tokens !== undefined) {
253
+ variables.max = getMax.max_tokens;
254
+ }
250
255
 
251
256
  return {address, tokens: parseAmount({amount, variables}).tokens};
252
257
  });
@@ -10,8 +10,10 @@ const describeConfidence = require('./describe_confidence');
10
10
  const formatFeeRate = require('./format_fee_rate');
11
11
 
12
12
  const aliasColor = n => !!n ? greenBright(n) : '';
13
+ const blindedChannel = 'blinded';
13
14
  const effectiveFeeRate = (n, m) => Number(BigInt(1e6) * BigInt(n) / BigInt(m));
14
15
  const flatten = arr => [].concat(...arr);
16
+ const isBlindedHop = hop => !!hop.encrypted_data && !hop.path_key;
15
17
  const isEmoji = /(?:[\u261D\u26F9\u270A-\u270D]|\uD83C[\uDF85\uDFC2-\uDFC4\uDFC7\uDFCA-\uDFCC]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66-\uDC69\uDC6E\uDC70-\uDC78\uDC7C\uDC81-\uDC83\uDC85-\uDC87\uDCAA\uDD74\uDD75\uDD7A\uDD90\uDD95\uDD96\uDE45-\uDE47\uDE4B-\uDE4F\uDEA3\uDEB4-\uDEB6\uDEC0\uDECC]|\uD83E[\uDD18-\uDD1C\uDD1E\uDD1F\uDD26\uDD30-\uDD39\uDD3D\uDD3E\uDDD1-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])?|(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDEEB\uDEEC\uDEF4-\uDEF8]|\uD83E[\uDD10-\uDD3A\uDD3C-\uDD3E\uDD40-\uDD45\uDD47-\uDD4C\uDD50-\uDD6B\uDD80-\uDD97\uDDC0\uDDD0-\uDDE6])|(?:[#\*0-9\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267B\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDCCF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE1A\uDE2F\uDE32-\uDE3A\uDE50\uDE51\uDF00-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFF]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDEE0-\uDEE5\uDEE9\uDEEB\uDEEC\uDEF0\uDEF3-\uDEF8]|\uD83E[\uDD10-\uDD3A\uDD3C-\uDD3E\uDD40-\uDD45\uDD47-\uDD4C\uDD50-\uDD6B\uDD80-\uDD97\uDDC0\uDDD0-\uDDE6])\uFE0F/g;
16
18
  const pairEdgeIndex = (pair, key) => `x${Number(!pair.indexOf(key))}`;
17
19
 
@@ -26,10 +28,12 @@ const pairEdgeIndex = (pair, key) => `x${Number(!pair.indexOf(key))}`;
26
28
  hops: [{
27
29
  channel: <Standard Format Channel Id String>
28
30
  channel_capacity: <Channel Capacity Tokens Number>
31
+ [encrypted_data]: <Blinded Path Encrypted Data Hex String>
29
32
  fee: <Fee Number>
30
33
  fee_mtokens: <Fee Millitokens String>
31
34
  forward: <Forward Tokens Number>
32
35
  forward_mtokens: <Forward Millitokens String>
36
+ [path_key]: <Blinded Path Key Hex String>
33
37
  public_key: <Public Key Hex String>
34
38
  timeout: <Timeout Block Height Number>
35
39
  }]
@@ -109,28 +113,35 @@ module.exports = ({lnd, route, tagged}, cbk) => {
109
113
 
110
114
  const path = route.hops.map((hop, i, hops) => {
111
115
  const {alias} = getAliases.find(n => n.id === hop.public_key);
112
- const {channel} = hop;
113
- const delta = !i ? null : hops[i - 1].timeout - hops[i].timeout;
116
+ const isBlinded = isBlindedHop(hop);
114
117
  const isFinal = i === hops.length - 1 && hops.length !== 1;
115
118
  const pair = [(hops[i - 1] || {}).public_key, hop.public_key].sort();
116
119
 
117
- const edgeIndex = pairEdgeIndex(pair, hop.public_key);
118
- const expiry = delta > 240 ? redBright(` Expiry +${delta}`) : '';
120
+ // Blinded hops have no channel, timeout or amount of their own
121
+ const channel = !isBlinded ? hop.channel : blindedChannel;
122
+ const edge = isBlinded ? '' : pairEdgeIndex(pair, hop.public_key);
119
123
  const feeMtokens = isFinal ? hops[i-1].fee_mtokens : hop.fee_mtokens;
120
124
  const forwarder = `${aliasColor(alias)} ${hop.public_key}`.trim();
125
+ const hasForward = !!BigInt(hop.forward_mtokens || Number());
126
+ const prior = hops[i - 1];
121
127
 
122
- const feeRate = effectiveFeeRate(feeMtokens, hop.forward_mtokens);
128
+ const delta = !i || isBlinded ? null : prior.timeout - hop.timeout;
129
+ const expiry = delta > 240 ? redBright(` Expiry +${delta}`) : '';
123
130
 
124
- const rate = formatFeeRate({rate: feeRate}).display;
131
+ // The rate is unknown when the hop forward amount is unknown
132
+ const rate = !hasForward ? null : formatFeeRate({
133
+ rate: effectiveFeeRate(feeMtokens, hop.forward_mtokens),
134
+ }).display;
125
135
 
126
- const forward = `${forwarder}. Fee rate: ${rate}${expiry}`;
136
+ const forward = rate === null ? forwarder :
137
+ `${forwarder}. Fee rate: ${rate}${expiry}`;
127
138
 
128
139
  if (!i) {
129
140
  return [`${gray(channel)} ${description || String()}`, forward];
130
141
  } else if (i === hops.length - [i].length) {
131
- return [`${gray(channel + edgeIndex)}`];
142
+ return [`${gray(channel + edge)}`];
132
143
  } else {
133
- return [`${gray(channel + edgeIndex)}`, forward];
144
+ return [`${gray(channel + edge)}`, forward];
134
145
  }
135
146
  });
136
147
 
@@ -0,0 +1,61 @@
1
+ const feeRateDenominator = BigInt(1e6);
2
+ const {isArray} = Array;
3
+ const isMtokens = n => typeof n === 'string' && /^\d+$/.test(n);
4
+ const isNumber = n => typeof n === 'number' && !isNaN(n);
5
+ const noFee = '0';
6
+
7
+ /** Calculate the fee to deliver an amount through a blinded path
8
+
9
+ {
10
+ mtokens: <Millitokens To Deliver To The Destination String>
11
+ path: {
12
+ base_fee_mtokens: <Accumulated Base Fee Millitokens String>
13
+ fee_rate: <Accumulated Fee Rate Millitokens Per Million Number>
14
+ hops: [{
15
+ encrypted_data: <Encrypted Recipient Data Hex String>
16
+ relay_key: <Relaying Node Public Key Hex String>
17
+ }]
18
+ }
19
+ }
20
+
21
+ @throws
22
+ <Error>
23
+
24
+ @returns
25
+ {
26
+ fee_mtokens: <Blinded Path Fee Millitokens String>
27
+ }
28
+ */
29
+ module.exports = ({mtokens, path}) => {
30
+ if (!isMtokens(mtokens)) {
31
+ throw new Error('ExpectedMillitokensToCalculateBlindedPathFee');
32
+ }
33
+
34
+ if (!path) {
35
+ throw new Error('ExpectedBlindedPathToCalculateBlindedPathFee');
36
+ }
37
+
38
+ if (!isMtokens(path.base_fee_mtokens)) {
39
+ throw new Error('ExpectedBaseFeeMillitokensToCalculateBlindedPathFee');
40
+ }
41
+
42
+ if (!isNumber(path.fee_rate)) {
43
+ throw new Error('ExpectedFeeRateToCalculateBlindedPathFee');
44
+ }
45
+
46
+ if (!isArray(path.hops) || !path.hops.length) {
47
+ throw new Error('ExpectedPathHopsToCalculateBlindedPathFee');
48
+ }
49
+
50
+ // Exit early when the path is just the destination, there is no relaying
51
+ if (path.hops.length === [path].length) {
52
+ return {fee_mtokens: noFee};
53
+ }
54
+
55
+ const baseFee = BigInt(path.base_fee_mtokens);
56
+ const proportional = BigInt(mtokens) * BigInt(path.fee_rate);
57
+
58
+ const fee = baseFee + proportional / feeRateDenominator;
59
+
60
+ return {fee_mtokens: fee.toString()};
61
+ };
@@ -16,9 +16,11 @@ const tokensAsMillitokens = tok => (BigInt(tok) * BigInt(1e3)).toString();
16
16
 
17
17
  /** Execute a probe
18
18
 
19
+ `destination` is not required when probing to blinded `paths`
20
+
19
21
  {
20
22
  cltv_delta: <Final Cltv Delta Number>
21
- destination: <Final Destination Public Key Hex String>
23
+ [destination]: <Final Destination Public Key Hex String>
22
24
  [features]: [{
23
25
  bit: <Feature Bit Number>
24
26
  }]
@@ -39,6 +41,19 @@ const tokensAsMillitokens = tok => (BigInt(tok) * BigInt(1e3)).toString();
39
41
  value: <Message To Final Destination Raw Value Hex Encoded String>
40
42
  }]
41
43
  [outgoing_channel]: <Outgoing Channel Id String>
44
+ [paths]: [{
45
+ base_fee_mtokens: <Accumulated Base Fee Millitokens String>
46
+ cltv_delta: <Accumulated CLTV Expiry Delta Number>
47
+ fee_rate: <Accumulated Fee Rate Millitokens Per Million Number>
48
+ hops: [{
49
+ encrypted_data: <Encrypted Recipient Data Hex String>
50
+ relay_key: <Relaying Node Public Key Hex String>
51
+ }]
52
+ [introduction_node]: <Introduction Node Public Key Hex String>
53
+ key: <First Hop Path Key Public Key Hex String>
54
+ [max_htlc_mtokens]: <Maximum HTLC Millitokens String>
55
+ [min_htlc_mtokens]: <Minimum HTLC Millitokens String>
56
+ }]
42
57
  [payment]: <Payment Identifier Hex String>
43
58
  [routes]: [[{
44
59
  [base_fee_mtokens]: <Base Fee Millitokens String>
@@ -67,10 +82,12 @@ const tokensAsMillitokens = tok => (BigInt(tok) * BigInt(1e3)).toString();
67
82
  hops: [{
68
83
  channel: <Standard Format Channel Id String>
69
84
  channel_capacity: <Channel Capacity Tokens Number>
85
+ [encrypted_data]: <Blinded Path Encrypted Data Hex String>
70
86
  fee: <Fee Number>
71
87
  fee_mtokens: <Fee Millitokens String>
72
88
  forward: <Forward Tokens Number>
73
89
  forward_mtokens: <Forward Millitokens String>
90
+ [path_key]: <Blinded Path Key Hex String>
74
91
  public_key: <Public Key Hex String>
75
92
  timeout: <Timeout Block Height Number>
76
93
  }]
@@ -89,7 +106,7 @@ module.exports = (args, cbk) => {
89
106
  return cbk([400, 'ExpectedFinalCltvDeltaToExecuteProbe']);
90
107
  }
91
108
 
92
- if (!args.destination) {
109
+ if (!args.destination && !args.paths) {
93
110
  return cbk([400, 'ExpectedDestinationToExecuteProbe']);
94
111
  }
95
112
 
@@ -145,25 +162,32 @@ module.exports = (args, cbk) => {
145
162
 
146
163
  const timeoutMinutes = minutesAsMs((args.timeout_minutes || Number()));
147
164
 
148
- const sub = subscribeToProbeForRoute({
149
- mtokens,
150
- cltv_delta: args.cltv_delta,
151
- destination: args.destination,
152
- features: args.features,
153
- ignore: args.ignore,
154
- incoming_peer: args.in_through,
155
- lnd: args.lnd,
156
- max_fee_mtokens: strictMaxFee || undefined,
157
- max_timeout_height: args.max_timeout_height,
158
- messages: args.messages,
159
- outgoing_channel: args.outgoing_channel,
160
- path_timeout_ms: pathTimeoutMs,
161
- payment: args.payment,
162
- probe_timeout_ms: timeoutMinutes || undefined,
163
- routes: args.routes,
164
- tokens: args.tokens,
165
- total_mtokens: !!args.payment ? mtokens : undefined,
166
- });
165
+ let sub;
166
+
167
+ try {
168
+ sub = subscribeToProbeForRoute({
169
+ mtokens,
170
+ cltv_delta: args.cltv_delta,
171
+ destination: args.destination,
172
+ features: args.features,
173
+ ignore: args.ignore,
174
+ incoming_peer: args.in_through,
175
+ lnd: args.lnd,
176
+ max_fee_mtokens: strictMaxFee || undefined,
177
+ max_timeout_height: args.max_timeout_height,
178
+ messages: args.messages,
179
+ outgoing_channel: args.outgoing_channel,
180
+ path_timeout_ms: pathTimeoutMs,
181
+ paths: args.paths,
182
+ payment: args.payment,
183
+ probe_timeout_ms: timeoutMinutes || undefined,
184
+ routes: args.routes,
185
+ tokens: args.tokens,
186
+ total_mtokens: !!args.payment ? mtokens : undefined,
187
+ });
188
+ } catch (err) {
189
+ return cbk([400, 'FailedToStartProbeForRoute', {err}]);
190
+ }
167
191
 
168
192
  const finished = (err, res) => {
169
193
  sub.removeAllListeners();
package/network/pay.js CHANGED
@@ -21,6 +21,7 @@ const {isArray} = Array;
21
21
  const mtokensAsTokens = mtokens => Number(mtokens / BigInt(1e3));
22
22
  const pathTimeoutMs = 1000 * 60 * 5;
23
23
  const singlePath = 1;
24
+ const withTotalFee = ({fee, ...res}) => ({total_fee: fee, ...res});
24
25
 
25
26
  /** Make a payment
26
27
 
@@ -229,6 +230,11 @@ module.exports = (args, cbk) => {
229
230
  const paths = [];
230
231
  const request = parsePaymentRequest({request: args.request});
231
232
 
233
+ // Multi-path payments look for paths to a destination, not to paths
234
+ if (!!request.paths) {
235
+ return cbk([501, 'MultiPathPayNotSupportedWithBlindedPaths']);
236
+ }
237
+
232
238
  const sub = subscribeToMultiPathProbe({
233
239
  cltv_delta: request.cltv_delta,
234
240
  destination: request.destination,
@@ -399,7 +405,15 @@ module.exports = (args, cbk) => {
399
405
  'singlePathPay',
400
406
  ({multiPathPay, singlePathPay}, cbk) =>
401
407
  {
402
- return cbk(null, singlePathPay || multiPathPay);
408
+ const payment = singlePathPay || multiPathPay;
409
+
410
+ // Exit early when the payment was not through a blinded path
411
+ if (!payment || payment.blinded_path_fee === undefined) {
412
+ return cbk(null, payment);
413
+ }
414
+
415
+ // The fee of a blinded path payment is a total including the path fee
416
+ return cbk(null, withTotalFee(payment));
403
417
  }],
404
418
  },
405
419
  returnResult({reject, resolve, of: 'payment'}, cbk));
package/network/probe.js CHANGED
@@ -30,6 +30,7 @@ const pathTimeoutMs = 1000 * 60 * 90;
30
30
  const singlePath = 1;
31
31
  const uniq = arr => Array.from(new Set(arr));
32
32
  const unsupported = 501;
33
+ const withTotalFee = ({fee, ...res}) => ({total_fee: fee, ...res});
33
34
 
34
35
  /** Probe a destination, looking for multiple non-overlapping paths
35
36
 
@@ -54,10 +55,12 @@ const unsupported = 501;
54
55
 
55
56
  @returns via cbk or Promise
56
57
  {
58
+ [blinded_path_fee]: <Blinded Path Fee Tokens Number>
57
59
  [fee]: <Total Fee Tokens To Destination Number>
58
60
  [latency_ms]: <Latency Milliseconds Number>
59
61
  [relays]: [[<Relaying Public Key Hex String>]]
60
62
  [routes_maximum]: <Maximum Sendable Tokens on Paths Number>
63
+ [total_fee]: <Total Fee Tokens Including Blinded Path Fee Number>
61
64
  }
62
65
  */
63
66
  module.exports = (args, cbk) => {
@@ -109,6 +112,7 @@ module.exports = (args, cbk) => {
109
112
  cltv_delta: decoded.cltv_delta,
110
113
  destination: decoded.destination,
111
114
  features: decoded.features,
115
+ paths: decoded.paths,
112
116
  routes: decoded.routes,
113
117
  });
114
118
  }],
@@ -319,6 +323,11 @@ module.exports = (args, cbk) => {
319
323
  return cbk([501, 'StrictMaxFeeNotSupportedWithMultiPathProbes']);
320
324
  }
321
325
 
326
+ // Multi-path probing looks for paths to a destination, not to paths
327
+ if (!!decodeRequest.paths) {
328
+ return cbk([501, 'MultiPathProbeNotSupportedWithBlindedPaths']);
329
+ }
330
+
322
331
  // Exit with error when the backing LND is below 0.10.0
323
332
  if (!!isLegacy) {
324
333
  return cbk([501, 'BackingLndDoesNotSupportMultiPathPayments']);
@@ -433,7 +442,18 @@ module.exports = (args, cbk) => {
433
442
 
434
443
  return cbk();
435
444
  }],
445
+
446
+ // Final probe result
447
+ result: ['checkFee', 'probe', ({probe}, cbk) => {
448
+ // Exit early when the probe was not through a blinded path
449
+ if (!probe || probe.blinded_path_fee === undefined) {
450
+ return cbk(null, probe);
451
+ }
452
+
453
+ // The fee of a blinded path probe is a total that includes the path fee
454
+ return cbk(null, withTotalFee(probe));
455
+ }],
436
456
  },
437
- returnResult({reject, resolve, of: 'probe'}, cbk));
457
+ returnResult({reject, resolve, of: 'result'}, cbk));
438
458
  });
439
459
  };
@@ -2,6 +2,7 @@ const {createHash} = require('crypto');
2
2
  const {randomBytes} = require('crypto');
3
3
 
4
4
  const asyncAuto = require('async/auto');
5
+ const asyncMap = require('async/map');
5
6
  const {decodePaymentRequest} = require('ln-service');
6
7
  const {getChannels} = require('ln-service');
7
8
  const {getIdentity} = require('ln-service');
@@ -14,10 +15,12 @@ const {returnResult} = require('asyncjs-util');
14
15
  const {signBytes} = require('ln-service');
15
16
  const {subscribeToFindMaxPayable} = require('probing');
16
17
 
18
+ const blindedPathFee = require('./blinded_path_fee');
17
19
  const executeProbe = require('./execute_probe');
18
20
  const {getIcons} = require('./../display');
19
21
  const {sortBy} = require('./../arrays');
20
22
 
23
+ const blinded = 'blinded';
21
24
  const bufFromHex = hex => Buffer.from(hex, 'hex');
22
25
  const channelReserve = n => n.local_reserve || Math.floor(n.capacity * 0.01);
23
26
  const cltvBuffer = 3;
@@ -33,14 +36,17 @@ const feeBuffer = fees => fees * 2;
33
36
  const {floor} = Math;
34
37
  const fromKeyType = '34349339';
35
38
  const htlcOutputSize = 43;
39
+ const isBlindedHop = hop => !!hop.encrypted_data && !hop.path_key;
36
40
  const keySendPreimageType = '5482373484';
37
41
  const makeNonce = () => randomBytes(32).toString('hex');
38
42
  const {max} = Math;
39
43
  const messageType = '34349334';
40
44
  const {min} = Math;
45
+ const mtokensAsTokens = mtokens => Number(BigInt(mtokens) / BigInt(1e3));
41
46
  const nodeKeyFamily = 6;
42
47
  const preimageByteLength = 32;
43
48
  const {now} = Date;
49
+ const pathChannel = hop => isBlindedHop(hop) ? blinded : hop.channel;
44
50
  const rate = n => n.commit_transaction_fee / (n.commit_transaction_weight / 4);
45
51
  const signatureType = '34349337';
46
52
  const tokAsMtok = tokens => (BigInt(tokens || 0) * BigInt(1e3)).toString();
@@ -173,15 +179,17 @@ module.exports = (args, cbk) => {
173
179
  features: details.features,
174
180
  id: details.id,
175
181
  mtokens: tokAsMtok(args.tokens),
182
+ paths: details.paths,
176
183
  payment: details.payment,
177
184
  routes: details.routes,
178
185
  tokens: args.tokens,
179
186
  });
180
187
  }
181
188
 
189
+ // The destination of a blinded paths request is not known
182
190
  args.logger.info({
183
191
  description: details.description || undefined,
184
- destination: details.destination,
192
+ destination: !details.paths ? details.destination : blinded,
185
193
  expires: moment(details.expires_at).fromNow(),
186
194
  id: details.id,
187
195
  tokens: details.tokens,
@@ -193,6 +201,7 @@ module.exports = (args, cbk) => {
193
201
  features: details.features,
194
202
  id: details.id,
195
203
  mtokens: details.mtokens || '0',
204
+ paths: details.paths,
196
205
  payment: details.payment,
197
206
  routes: details.routes,
198
207
  tokens: details.tokens,
@@ -209,6 +218,11 @@ module.exports = (args, cbk) => {
209
218
 
210
219
  // Lookup node destination details
211
220
  getDestinationNode: ['to', ({to}, cbk) => {
221
+ // Exit early when the destination is hidden behind blinded paths
222
+ if (!!to.paths) {
223
+ return cbk(null, {alias: String()});
224
+ }
225
+
212
226
  return getNode({
213
227
  is_omitting_channels: true,
214
228
  lnd: args.lnd,
@@ -224,6 +238,33 @@ module.exports = (args, cbk) => {
224
238
  });
225
239
  }],
226
240
 
241
+ // Lookup the introduction nodes of blinded paths
242
+ getIntroductionNodes: ['to', ({to}, cbk) => {
243
+ // Exit early when there are no blinded paths
244
+ if (!to.paths) {
245
+ return cbk();
246
+ }
247
+
248
+ const ids = to.paths.map(n => n.introduction_node);
249
+
250
+ return asyncMap(ids, (id, cbk) => {
251
+ return getNode({
252
+ is_omitting_channels: true,
253
+ lnd: args.lnd,
254
+ public_key: id,
255
+ },
256
+ (err, res) => {
257
+ // Suppress errors when the node is not found
258
+ if (!!err) {
259
+ return cbk(null, id);
260
+ }
261
+
262
+ return cbk(null, `${res.alias} ${id}`.trim());
263
+ });
264
+ },
265
+ cbk);
266
+ }],
267
+
227
268
  // For destinations that are peers, get features directly
228
269
  getPeerFeatures: [
229
270
  'getDestinationNode',
@@ -243,7 +284,7 @@ module.exports = (args, cbk) => {
243
284
  const peer = res.peers.find(n => n.public_key === to.destination);
244
285
 
245
286
  // Exit early when there are no peer features
246
- if (!peer || !peers.features.length) {
287
+ if (!peer || !peer.features.length) {
247
288
  return cbk();
248
289
  }
249
290
 
@@ -277,8 +318,24 @@ module.exports = (args, cbk) => {
277
318
  return cbk(null, {features});
278
319
  }],
279
320
 
321
+ // Check that messages can be attached to the payment
322
+ checkMessages: ['to', ({to}, cbk) => {
323
+ // Exit early when there are no messages to attach
324
+ if (!args.message && !args.messages) {
325
+ return cbk();
326
+ }
327
+
328
+ // Blinded path hops only accept encrypted data in their payloads
329
+ if (!!to.paths) {
330
+ return cbk([501, 'MessagesNotSupportedWithBlindedPaths']);
331
+ }
332
+
333
+ return cbk();
334
+ }],
335
+
280
336
  // Determine messages to attach
281
337
  messages: [
338
+ 'checkMessages',
282
339
  'getFeatures',
283
340
  'getIdentity',
284
341
  'to',
@@ -379,9 +436,37 @@ module.exports = (args, cbk) => {
379
436
  checkPath: [
380
437
  'getDestinationNode',
381
438
  'getIdentity',
439
+ 'getIntroductionNodes',
382
440
  'to',
383
- ({getDestinationNode, getIdentity, to}, cbk) =>
441
+ ({getDestinationNode, getIdentity, getIntroductionNodes, to}, cbk) =>
384
442
  {
443
+ // Exit early when the path is found to a blinded path introduction node
444
+ if (!!to.paths) {
445
+ const [node] = getIntroductionNodes;
446
+ const isSingle = getIntroductionNodes.length === [node].length;
447
+
448
+ // The probe amount is what the paths must deliver to the destination
449
+ const mtokens = !BigInt(to.mtokens) ? tokAsMtok(defaultTokens) :
450
+ to.mtokens;
451
+
452
+ // The fee of a blinded path is what relaying through it costs
453
+ const fees = to.paths.map(path => {
454
+ return mtokensAsTokens(blindedPathFee({mtokens, path}).fee_mtokens);
455
+ });
456
+
457
+ const [fee] = fees;
458
+
459
+ const nodes = getIntroductionNodes;
460
+
461
+ // A request may have a single path or multiple paths to choose from
462
+ args.logger.info({
463
+ blinded_path_fee: isSingle ? fee : fees,
464
+ checking_for_path_to_introduction: isSingle ? node : nodes,
465
+ });
466
+
467
+ return cbk();
468
+ }
469
+
385
470
  const sendingTo = `${getDestinationNode.alias} ${to.destination}`;
386
471
 
387
472
  if (to.destination === getIdentity.public_key) {
@@ -406,7 +491,7 @@ module.exports = (args, cbk) => {
406
491
  return executeProbe({
407
492
  messages,
408
493
  cltv_delta: (to.cltv_delta || defaultCltvDelta) + cltvBuffer,
409
- destination: to.destination,
494
+ destination: !to.paths ? to.destination : undefined,
410
495
  features: getFeatures.features,
411
496
  ignore: args.ignore,
412
497
  in_through: args.in_through,
@@ -418,6 +503,7 @@ module.exports = (args, cbk) => {
418
503
  max_fee_mtokens: args.max_fee_mtokens,
419
504
  mtokens: !BigInt(to.mtokens) ? tokAsMtok(defaultTokens) : to.mtokens,
420
505
  outgoing_channel: !!outId ? outId.id : undefined,
506
+ paths: to.paths || undefined,
421
507
  payment: to.payment,
422
508
  routes: to.routes,
423
509
  tagged: !!getIcons ? getIcons.nodes : undefined,
@@ -433,6 +519,11 @@ module.exports = (args, cbk) => {
433
519
  return cbk(null, {});
434
520
  }
435
521
 
522
+ // Finding the maximum needs to probe the hops which are hidden in paths
523
+ if (!!to.paths) {
524
+ return cbk([501, 'FindMaxNotSupportedWithBlindedPaths']);
525
+ }
526
+
436
527
  const sub = subscribeToFindMaxPayable({
437
528
  cltv: (to.cltv_delta || defaultCltvDelta) + cltvBuffer,
438
529
  hops: probe.route.hops,
@@ -476,9 +567,7 @@ module.exports = (args, cbk) => {
476
567
  return cbk([400, 'MaxFeeTooLow', {required_fee: probe.route.fee}]);
477
568
  }
478
569
 
479
- args.logger.info({
480
- paying: probe.route.hops.map(({channel}) => channel),
481
- });
570
+ args.logger.info({paying: probe.route.hops.map(pathChannel)});
482
571
 
483
572
  return payViaRoutes({
484
573
  id: to.id,
@@ -499,7 +588,11 @@ module.exports = (args, cbk) => {
499
588
 
500
589
  const {route} = probe;
501
590
 
591
+ // The introduction node hop of a blinded path takes the path fee
592
+ const introduction = route.hops.find(hop => !!hop.path_key);
593
+
502
594
  return cbk(null, {
595
+ blinded_path_fee: !introduction ? undefined : introduction.fee,
503
596
  fee: !route ? undefined : route.fee,
504
597
  id: !pay ? undefined : pay.id,
505
598
  latency_ms: !route ? undefined : probe.latency_ms,
@@ -508,7 +601,7 @@ module.exports = (args, cbk) => {
508
601
  preimage: !pay ? undefined : pay.secret,
509
602
  probed: !!pay ? undefined : route.tokens - route.fee,
510
603
  relays: !route ? undefined : route.hops.map(n => n.public_key),
511
- success: !route ? undefined : route.hops.map(({channel}) => channel),
604
+ success: !route ? undefined : route.hops.map(pathChannel),
512
605
  });
513
606
  }],
514
607
  },