balanceofsatoshis 25.5.0 → 25.6.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Versions
2
2
 
3
+ ## 25.6.0
4
+
5
+ - `stream`: Add command to stream a payment to a lnurl or lightning address
6
+
3
7
  ## 25.5.0
4
8
 
5
9
  - `probe`: Add `--ignore-expiry` to probe expired payment requests
package/README.md CHANGED
@@ -168,6 +168,9 @@ bos send
168
168
  # Run a server of paid services
169
169
  bos server
170
170
 
171
+ # Stream a payment to a lnurl or lightning address in a series of payments
172
+ bos stream "amount" --lnurl "lnurl/lightning_address"
173
+
171
174
  # Connect up to a Telegram bot
172
175
  bos telegram
173
176
 
package/bos CHANGED
@@ -1954,6 +1954,53 @@ prog
1954
1954
  });
1955
1955
  })
1956
1956
 
1957
+ // Stream a payment in a series of payments
1958
+ .command('stream', 'Stream a payment in a series of payments')
1959
+ .help('Each payment is sized to the most a single route can carry')
1960
+ .help('--lnurl is required, it can stream above the max the service takes')
1961
+ .help('--min-payment defaults to 100*k, a smaller remainder is not sent')
1962
+ .help('--out can take a tag to make the first hop through any tagged peer')
1963
+ .help('--out-filter is checked before every payment, skipping failing peers')
1964
+ .help('--out-filter vars: CAPACITY/HEIGHTS/INBOUND_LIQUIDITY')
1965
+ .help('--out-filter vars: OUTBOUND_LIQUIDITY/PENDING_PAYMENTS')
1966
+ .help('--out-filter vars: INBOUND_BASE_FEE/INBOUND_FEE_RATE')
1967
+ .help('Amounts can take m/k variables: 5*m for 5 million, 250*k = 0.0025')
1968
+ .argument('<amount>', 'Total amount to stream')
1969
+ .option('--avoid <avoid>', 'Avoid forwarding via node/chan/tag', REPEATABLE)
1970
+ .option('--lnurl <url>', 'Lnurl/lightning address', STRING, undefined, true)
1971
+ .option('--max-fee <max_fee>', 'Maximum total fee to pay', STRING, '1337')
1972
+ .option('--max-fee-rate <ppm>', 'Max fee rate for all payments', INT, 1000)
1973
+ .option('--max-payment <amount>', 'Largest single payment to make', STRING)
1974
+ .option('--min-payment <amount>', 'Smallest single payment to make', STRING)
1975
+ .option('--no-color', 'Mute all colors')
1976
+ .option('--node <node_name>', 'Node to stream from')
1977
+ .option('--out <peer_or_tag>', 'Make first hop through peer/tag', REPEATABLE)
1978
+ .option('--out-filter <out_filter>', 'Filter outbound peer/tag', REPEATABLE)
1979
+ .action((args, options, logger) => {
1980
+ return new Promise(async (resolve, reject) => {
1981
+ try {
1982
+ return network.streamPayment({
1983
+ logger,
1984
+ amount: args.amount,
1985
+ avoid: flatten([options.avoid].filter(n => !!n)),
1986
+ fs: {getFile: readFile},
1987
+ lnd: (await lndForNode(logger, options.node)).lnd,
1988
+ lnurl: options.lnurl,
1989
+ max_fee: options.maxFee,
1990
+ max_fee_rate: options.maxFeeRate,
1991
+ max_payment: options.maxPayment,
1992
+ min_payment: options.minPayment,
1993
+ out: flatten([options.out].filter(n => !!n)),
1994
+ out_filters: flatten([options.outFilter].filter(n => !!n)),
1995
+ request: commands.simpleRequest,
1996
+ },
1997
+ responses.returnObject({logger, reject, resolve}));
1998
+ } catch (err) {
1999
+ return logger.error({err}) && reject();
2000
+ }
2001
+ });
2002
+ })
2003
+
1957
2004
  // Get a swap API key or inspect an existing one
1958
2005
  .command('swap-api-key', 'Purchase a swap API key or inspect a swap API key')
1959
2006
  .visible(false)
@@ -108,6 +108,7 @@ module.exports = ({index, lnd, reason, route, tagged}, cbk) => {
108
108
 
109
109
  const {display} = chartAliasForPeer({
110
110
  alias: getTo.alias,
111
+ icons: !!tag ? tag.icons : undefined,
111
112
  public_key: getTo.id,
112
113
  });
113
114
 
@@ -120,11 +121,14 @@ module.exports = ({index, lnd, reason, route, tagged}, cbk) => {
120
121
  'toName',
121
122
  ({fromName, toName}, cbk) =>
122
123
  {
123
- const at = `on ${route.hops[index].channel}`;
124
+ const hop = route.hops[index];
125
+
126
+ // A failure at the destination is not on a channel out of it
127
+ const at = !hop ? '' : ` on ${hop.channel}`;
124
128
  const from = !fromName ? '' : `from ${fromName}`;
125
129
  const to = !toName ? '' : ` forwarding to ${toName}`;
126
130
 
127
- return cbk(null, {description: `${reason} ${from} ${at}${to}`});
131
+ return cbk(null, {description: `${reason} ${from}${at}${to}`});
128
132
  }],
129
133
  },
130
134
  returnResult({reject, resolve, of: 'description'}, cbk));
@@ -3,6 +3,7 @@ const {parsePaymentRequest} = require('ln-service');
3
3
  const {returnResult} = require('asyncjs-util');
4
4
 
5
5
  const errorStatus = 'ERROR';
6
+ const timeoutMs = 1000 * 60;
6
7
 
7
8
  /** Get a payment request for a LNURL
8
9
 
@@ -45,9 +46,13 @@ module.exports = ({hash, mtokens, request, url}, cbk) => {
45
46
 
46
47
  // Get the payment request
47
48
  getRequest: ['validate', ({}, cbk) => {
48
- const qs = {amount: mtokens};
49
-
50
- return request({qs, url, json: true}, (err, r, json) => {
49
+ return request({
50
+ url,
51
+ json: true,
52
+ qs: {amount: mtokens},
53
+ timeout: timeoutMs,
54
+ },
55
+ (err, r, json) => {
51
56
  if (!!err) {
52
57
  return cbk([503, 'FailedToGetPaymentRequestFromService', {err}]);
53
58
  }
@@ -15,6 +15,7 @@ const payRequestTag = 'payRequest';
15
15
  const sha256 = n => createHash('sha256').update(n).digest().toString('hex');
16
16
  const sslProtocol = 'https:';
17
17
  const textPlain = 'text/plain';
18
+ const timeoutMs = 1000 * 60;
18
19
  const utf8AsBuffer = utf8 => Buffer.from(utf8, 'utf8');
19
20
 
20
21
  /** Get payment terms
@@ -51,7 +52,12 @@ module.exports = ({request, url}, cbk) => {
51
52
 
52
53
  // Get payment terms
53
54
  getTerms: ['validate', ({}, cbk) => {
54
- return request({url, json: true}, (err, r, json) => {
55
+ return request({
56
+ url,
57
+ json: true,
58
+ timeout: timeoutMs,
59
+ },
60
+ (err, r, json) => {
55
61
  if (!!err) {
56
62
  return cbk([503, 'FailureGettingLnUrlDataFromUrl', {err}]);
57
63
  }
@@ -100,14 +106,17 @@ module.exports = ({request, url}, cbk) => {
100
106
  return cbk([503, 'ExpectedMetadataArrayInLnurlResponse', json]);
101
107
  }
102
108
 
103
- const [, description] = parse(json.metadata)
109
+ const textEntry = parse(json.metadata)
104
110
  .filter(isArray)
105
111
  .find(([entry, text]) => entry === textPlain && !!text);
106
112
 
107
- if (!description) {
113
+ // Exit early when there is no text/plain entry to describe the terms
114
+ if (!textEntry) {
108
115
  return cbk([503, 'ExpectedTextPlainEntryInLnurlResponse']);
109
116
  }
110
117
 
118
+ const [, description] = textEntry;
119
+
111
120
  if (!isNumber(json.minSendable)) {
112
121
  return cbk([503, 'ExpectedNumericValueForMinSendable']);
113
122
  }
@@ -3,9 +3,9 @@ const {bech32} = require('bech32');
3
3
  const asLnurl = n => n.substring(n.startsWith('lightning:') ? 10 : 0);
4
4
  const bech32CharLimit = 2000;
5
5
  const {decode} = bech32;
6
- const isEmail = n => /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/.test(n);
6
+ const isEmail = n => /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(n);
7
7
  const isOnion = n => /.onion$/.test(n);
8
- const isUsername = n => /^[a-z0-9_.]*$/.test(n);
8
+ const isUsername = n => /^[a-z0-9_.-]*$/.test(n);
9
9
  const join = arr => arr.join('');
10
10
  const parseEmail = email => email.split('@');
11
11
  const prefix = 'lnurl';
@@ -46,7 +46,14 @@ module.exports = ({url}) => {
46
46
  throw new Error('LnurlOnionUrlsCurrentlyUnsupported');
47
47
  }
48
48
 
49
- return {url: join([sslProtocol, domain, urlString, username])};
49
+ const addressUrl = join([sslProtocol, domain, urlString, username]);
50
+
51
+ // Exit with error when the lightning address does not make a valid URL
52
+ if (!URL.canParse(addressUrl)) {
53
+ throw new Error('ExpectedValidUrlForLightningAddress');
54
+ }
55
+
56
+ return {url: addressUrl};
50
57
  }
51
58
 
52
59
  if (decode(asLnurl(url), bech32CharLimit).prefix !== prefix) {
@@ -55,5 +62,12 @@ module.exports = ({url}) => {
55
62
 
56
63
  const {words} = decode(asLnurl(url), bech32CharLimit);
57
64
 
58
- return {url: wordsAsUtf8(words)};
65
+ const decoded = wordsAsUtf8(words);
66
+
67
+ // Exit with error when the lnurl does not hold a valid URL
68
+ if (!URL.canParse(decoded)) {
69
+ throw new Error('ExpectedValidUrlInLnurl');
70
+ }
71
+
72
+ return {url: decoded};
59
73
  };
@@ -2,6 +2,7 @@
2
2
  "emojiIcons": {
3
3
  "disabled": "🚫"
4
4
  },
5
+ "minStreamPaymentTokens": 100000,
5
6
  "peerSortOptions": [
6
7
  "alias",
7
8
  "fee_earnings",
@@ -1,13 +1,10 @@
1
1
  const asyncAuto = require('async/auto');
2
- const asyncMapSeries = require('async/mapSeries');
3
- const {getNode} = require('ln-service');
4
2
  const {returnResult} = require('asyncjs-util');
5
- const {subscribeToProbe} = require('ln-service');
6
3
  const {subscribeToProbeForRoute} = require('ln-service');
7
4
 
8
- const {describeConfidence} = require('./../display');
9
5
  const {describeRoute} = require('./../display');
10
6
  const {describeRoutingFailure} = require('./../display');
7
+ const lineWriter = require('./line_writer');
11
8
 
12
9
  const {now} = Date;
13
10
  const minutesAsMs = minutes => 1000 * 60 * minutes;
@@ -189,10 +186,14 @@ module.exports = (args, cbk) => {
189
186
  return cbk([400, 'FailedToStartProbeForRoute', {err}]);
190
187
  }
191
188
 
189
+ // Lines are written in the order of events, and all before finishing
190
+ const lines = lineWriter({logger: args.logger});
191
+
192
192
  const finished = (err, res) => {
193
193
  sub.removeAllListeners();
194
194
 
195
- return cbk(err, res);
195
+ // Wait for lines that are being written so that they are not lost
196
+ return lines.wait().then(() => cbk(err, res));
196
197
  };
197
198
 
198
199
  // Finish without success
@@ -204,12 +205,12 @@ module.exports = (args, cbk) => {
204
205
  sub.on('error', err => finished(err));
205
206
 
206
207
  // Log failures encountered while trying to find a route
207
- sub.on('routing_failure', async failure => {
208
+ sub.on('routing_failure', failure => {
208
209
  if (!!args.log_failure) {
209
210
  args.log_failure(null, failure);
210
211
  }
211
212
 
212
- const {description} = await describeRoutingFailure({
213
+ const described = describeRoutingFailure({
213
214
  index: failure.index,
214
215
  lnd: args.lnd,
215
216
  reason: failure.reason,
@@ -217,7 +218,7 @@ module.exports = (args, cbk) => {
217
218
  tagged: args.tagged,
218
219
  });
219
220
 
220
- return args.logger.info({failure: description});
221
+ return lines.write(described.then(n => ({failure: n.description})));
221
222
  });
222
223
 
223
224
  // Finish with successful probe
@@ -233,16 +234,18 @@ module.exports = (args, cbk) => {
233
234
  });
234
235
 
235
236
  // Log probing attempts
236
- sub.on('probing', async ({route}) => {
237
+ sub.on('probing', ({route}) => {
237
238
  attemptedPaths.push(route);
238
239
 
239
- const {description} = await describeRoute({
240
+ const described = describeRoute({
240
241
  route,
241
242
  lnd: args.lnd,
242
243
  tagged: args.tagged || undefined,
243
244
  });
244
245
 
245
- return args.logger.info({evaluating: description});
246
+ const line = described.then(n => ({evaluating: n.description}));
247
+
248
+ return lines.write(line);
246
249
  });
247
250
 
248
251
  return;
@@ -0,0 +1,128 @@
1
+ const asyncAuto = require('async/auto');
2
+ const asyncMapSeries = require('async/mapSeries');
3
+ const asyncReflect = require('async/reflect');
4
+ const {getChannels} = require('ln-service');
5
+ const {returnResult} = require('asyncjs-util');
6
+
7
+ const getOutPeers = require('./get_out_peers');
8
+
9
+ const flatten = arr => [].concat(...arr);
10
+ const inactiveTagError = 'NoActivePeersFoundForOutTag';
11
+ const {isArray} = Array;
12
+ const noMatchError = 'NoPeerMatchesFoundToSatisfyOutboundFilter';
13
+ const uniq = arr => Array.from(new Set(arr));
14
+
15
+ /** Get the channels to route out through, applying out filters
16
+
17
+ Out peers that fail the filters or have no active channel are left out
18
+
19
+ {
20
+ [filters]: [<Outbound Peer Filter Formula String>]
21
+ [fs]: {
22
+ getFile: <Read File Contents Function> (path, cbk) => {}
23
+ }
24
+ lnd: <Authenticated LND API Object>
25
+ out: [<Out Through Peer With Public Key, Alias, or Tag String>]
26
+ }
27
+
28
+ @returns via cbk or Promise
29
+ {
30
+ [channels]: [<Out Through Standard Format Channel Id String>]
31
+ public_keys: [<Out Through Peer Public Key Hex String>]
32
+ }
33
+ */
34
+ module.exports = ({filters, fs, lnd, out}, cbk) => {
35
+ return new Promise((resolve, reject) => {
36
+ return asyncAuto({
37
+ // Check arguments
38
+ validate: cbk => {
39
+ if (!lnd) {
40
+ return cbk([400, 'ExpectedAuthenticatedLndToGetOutChannels']);
41
+ }
42
+
43
+ if (!isArray(out)) {
44
+ return cbk([400, 'ExpectedArrayOfOutPeersToGetOutChannels']);
45
+ }
46
+
47
+ // Filters narrow down out peers so there must be out peers to filter
48
+ if (!out.length && !!filters && !!filters.length) {
49
+ return cbk([400, noMatchError]);
50
+ }
51
+
52
+ return cbk();
53
+ },
54
+
55
+ // Find public keys to pay out through for each out peer or tag
56
+ getOuts: ['validate', ({}, cbk) => {
57
+ return asyncMapSeries(out, (query, cbk) => {
58
+ return asyncReflect(getOutPeers)({
59
+ filters,
60
+ fs,
61
+ lnd,
62
+ out: [query],
63
+ },
64
+ cbk);
65
+ },
66
+ cbk);
67
+ }],
68
+
69
+ // Out peers that pass the filters, leaving out the ones that do not and
70
+ // tags with no active peers
71
+ peers: ['getOuts', ({getOuts}, cbk) => {
72
+ const errors = getOuts.filter(n => !!n.error).map(n => n.error);
73
+ const leftOut = [inactiveTagError, noMatchError];
74
+
75
+ // An error other than an out peer being left out is passed back
76
+ const [err] = errors.filter(([, msg]) => !leftOut.includes(msg));
77
+
78
+ if (!!err) {
79
+ return cbk(err);
80
+ }
81
+
82
+ const found = getOuts.filter(n => !n.error).map(n => n.value);
83
+
84
+ return cbk(null, uniq(flatten(found.map(n => n.public_keys))));
85
+ }],
86
+
87
+ // Get channels with the out peers
88
+ getChannels: ['peers', ({}, cbk) => {
89
+ // Exit early when there is no outbound restriction
90
+ if (!out.length) {
91
+ return cbk();
92
+ }
93
+
94
+ return getChannels({lnd}, cbk);
95
+ }],
96
+
97
+ // Channels to go out through, with the out peers that have them
98
+ channels: ['getChannels', 'peers', ({getChannels, peers}, cbk) => {
99
+ // Exit early when there is no outbound restriction
100
+ if (!out.length) {
101
+ return cbk(null, {public_keys: []});
102
+ }
103
+
104
+ const withPeer = getChannels.channels
105
+ .filter(n => !!n.is_active)
106
+ .filter(n => peers.includes(n.partner_public_key));
107
+
108
+ // Exit with error when the filters left no out peer to go through
109
+ if (!withPeer.length && !!filters && !!filters.length) {
110
+ return cbk([400, noMatchError]);
111
+ }
112
+
113
+ // Exit with error when no out peer has an active channel
114
+ if (!withPeer.length) {
115
+ return cbk([404, 'NoActiveChannelWithOutgoingPeer']);
116
+ }
117
+
118
+ const keys = withPeer.map(n => n.partner_public_key);
119
+
120
+ return cbk(null, {
121
+ channels: withPeer.map(n => n.id),
122
+ public_keys: peers.filter(key => keys.includes(key)),
123
+ });
124
+ }],
125
+ },
126
+ returnResult({reject, resolve, of: 'channels'}, cbk));
127
+ });
128
+ };
@@ -0,0 +1,102 @@
1
+ const asyncAuto = require('async/auto');
2
+ const asyncMapSeries = require('async/mapSeries');
3
+ const {findKey} = require('ln-sync');
4
+ const {returnResult} = require('asyncjs-util');
5
+
6
+ const asKey = n => !n.public_key ? undefined : n.public_key.toLowerCase();
7
+ const flatten = arr => [].concat(...arr);
8
+ const {isArray} = Array;
9
+ const uniq = arr => Array.from(new Set(arr));
10
+
11
+ /** Get out queries with the aliases of out peers resolved to public keys
12
+
13
+ Tag queries are kept so that the peers of tags can be found again later
14
+
15
+ {
16
+ channels: [{
17
+ partner_public_key: <Peer Public Key Hex String>
18
+ }]
19
+ lnd: <Authenticated LND API Object>
20
+ out: [<Out Through Peer With Public Key, Alias, or Tag String>]
21
+ tags: [{
22
+ [alias]: <Tag Alias String>
23
+ id: <Tag Id Hex String>
24
+ nodes: [<Public Key Hex String>]
25
+ }]
26
+ }
27
+
28
+ @returns via cbk or Promise
29
+ {
30
+ public_keys: [<Out Peer Public Key Hex String>]
31
+ queries: [<Out Through Peer Public Key or Tag String>]
32
+ }
33
+ */
34
+ module.exports = ({channels, lnd, out, tags}, cbk) => {
35
+ return new Promise((resolve, reject) => {
36
+ return asyncAuto({
37
+ // Check arguments
38
+ validate: cbk => {
39
+ if (!isArray(channels)) {
40
+ return cbk([400, 'ExpectedArrayOfChannelsToGetOutQueries']);
41
+ }
42
+
43
+ if (!lnd) {
44
+ return cbk([400, 'ExpectedAuthenticatedLndToGetOutQueries']);
45
+ }
46
+
47
+ if (!isArray(out)) {
48
+ return cbk([400, 'ExpectedArrayOfOutPeersToGetOutQueries']);
49
+ }
50
+
51
+ if (!isArray(tags)) {
52
+ return cbk([400, 'ExpectedArrayOfTagsToGetOutQueries']);
53
+ }
54
+
55
+ return cbk();
56
+ },
57
+
58
+ // Out queries with the tags that they match on alias or on id
59
+ outQueries: ['validate', ({}, cbk) => {
60
+ return cbk(null, out.map(n => String(n)).map(query => ({
61
+ query,
62
+ tags: tags.filter(tag => {
63
+ const alias = tag.alias || String();
64
+
65
+ const isAliasMatch = alias.toLowerCase() === query.toLowerCase();
66
+
67
+ return isAliasMatch || tag.id.startsWith(query);
68
+ }),
69
+ })));
70
+ }],
71
+
72
+ // Find the public keys of out peers, other than peers of tags
73
+ getKeys: ['outQueries', ({outQueries}, cbk) => {
74
+ return asyncMapSeries(outQueries, (outQuery, cbk) => {
75
+ // Exit early when the query is a tag, its peers are found later
76
+ if (!!outQuery.tags.length) {
77
+ return cbk(null, {});
78
+ }
79
+
80
+ return findKey({channels, lnd, query: outQuery.query}, cbk);
81
+ },
82
+ cbk);
83
+ }],
84
+
85
+ // Out queries with peer aliases replaced by peer public keys
86
+ queries: ['getKeys', 'outQueries', ({getKeys, outQueries}, cbk) => {
87
+ const keys = getKeys.map(asKey);
88
+ const peers = channels.map(n => n.partner_public_key);
89
+ const tagged = flatten(outQueries.map(n => n.tags));
90
+
91
+ // Out peers are the peers that are named and the peers in out tags
92
+ const outPeers = keys.concat(flatten(tagged.map(n => n.nodes)));
93
+
94
+ return cbk(null, {
95
+ public_keys: uniq(outPeers.filter(n => peers.includes(n))),
96
+ queries: outQueries.map((n, i) => keys[i] || n.query),
97
+ });
98
+ }],
99
+ },
100
+ returnResult({reject, resolve, of: 'queries'}, cbk));
101
+ });
102
+ };
@@ -0,0 +1,95 @@
1
+ const asyncAuto = require('async/auto');
2
+ const asyncReflect = require('async/reflect');
3
+ const {formatTokens} = require('ln-sync');
4
+ const {returnResult} = require('asyncjs-util');
5
+
6
+ const getPayRequest = require('./../lnurl/get_pay_request');
7
+
8
+ const {isArray} = Array;
9
+ const mtokensAsTokens = n => Number(BigInt(n) / BigInt(1e3));
10
+ const serviceDeclinedError = 'ServiceReturnedError';
11
+
12
+ /** Get an invoice from a LNURL service for a payment of a stream
13
+
14
+ {
15
+ hash: <Description Hash Hex String>
16
+ logger: <Winston Logger Object>
17
+ mtokens: <Invoice Amount Millitokens String>
18
+ request: <Request Function>
19
+ url: <Service Callback URL String>
20
+ }
21
+
22
+ @returns via cbk or Promise
23
+ {
24
+ [is_amount_too_high]: <Service Turned Down The Amount Bool>
25
+ [request]: <BOLT 11 Payment Request String>
26
+ }
27
+ */
28
+ module.exports = ({hash, logger, mtokens, request, url}, cbk) => {
29
+ return new Promise((resolve, reject) => {
30
+ return asyncAuto({
31
+ // Check arguments
32
+ validate: cbk => {
33
+ if (!hash) {
34
+ return cbk([400, 'ExpectedDescriptionHashToGetStreamInvoice']);
35
+ }
36
+
37
+ if (!logger) {
38
+ return cbk([400, 'ExpectedLoggerToGetStreamInvoice']);
39
+ }
40
+
41
+ if (!mtokens) {
42
+ return cbk([400, 'ExpectedMillitokensToGetStreamInvoice']);
43
+ }
44
+
45
+ if (!request) {
46
+ return cbk([400, 'ExpectedRequestFunctionToGetStreamInvoice']);
47
+ }
48
+
49
+ if (!url) {
50
+ return cbk([400, 'ExpectedServiceUrlToGetStreamInvoice']);
51
+ }
52
+
53
+ return cbk();
54
+ },
55
+
56
+ // The amount of the invoice to show
57
+ amount: ['validate', ({}, cbk) => {
58
+ return cbk(null, formatTokens({tokens: mtokensAsTokens(mtokens)}));
59
+ }],
60
+
61
+ // Ask the service for an invoice
62
+ getRequest: ['amount', ({amount}, cbk) => {
63
+ logger.info({requesting_invoice: amount.display});
64
+
65
+ const pay = {hash, mtokens, request, url};
66
+
67
+ return asyncReflect(getPayRequest)(pay, cbk);
68
+ }],
69
+
70
+ // Note the invoice, or that the service turned down the amount
71
+ invoice: ['amount', 'getRequest', ({amount, getRequest}, cbk) => {
72
+ const {error, value} = getRequest;
73
+
74
+ // Exit early when the service declined to make the invoice
75
+ if (isArray(error) && error[1] === serviceDeclinedError) {
76
+ const [,, details] = error;
77
+
78
+ logger.info({invoice_declined: amount.display, reason: details.err});
79
+
80
+ return cbk(null, {is_amount_too_high: true});
81
+ }
82
+
83
+ if (!!error) {
84
+ return cbk(error);
85
+ }
86
+
87
+ // The request is what the route search probes towards
88
+ logger.info({payment_request: value.request});
89
+
90
+ return cbk(null, {request: value.request});
91
+ }],
92
+ },
93
+ returnResult({reject, resolve, of: 'invoice'}, cbk));
94
+ });
95
+ };