paid-services 3.13.2 → 3.14.2

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.
@@ -1,9 +1,13 @@
1
+ const {decodeBigSize} = require('bolt01');
1
2
  const {decodeTlvStream} = require('bolt01');
2
3
 
3
4
  const anchorPrefix = 'anchor-trade-secret:';
4
5
  const base64AsHex = base64 => Buffer.from(base64, 'base64').toString('hex');
6
+ const decodeNumber = encoded => Number(decodeBigSize({encoded}).decoded);
7
+ const findChannel = records => records.find(n => n.type === '3');
5
8
  const findDescription = records => records.find(n => n.type === '1');
6
9
  const findSecret = records => records.find(n => n.type === '0');
10
+ const findPrice = records => records.find(n => n.type === '2');
7
11
  const hexAsUtf = n => !n ? '' : Buffer.from(n.value, 'hex').toString('utf8');
8
12
 
9
13
  /** Decode an anchored trade data blob
@@ -11,8 +15,10 @@ const hexAsUtf = n => !n ? '' : Buffer.from(n.value, 'hex').toString('utf8');
11
15
  Data blobs look like anchored-trade-secret:<base64-encoded-data>
12
16
 
13
17
  Inside the base64 is encoded TLV data:
14
- 0: <Trade Secret>
18
+ [0]: <Trade Secret>
15
19
  [1]: <Trade Description>
20
+ [2]: <Price Expression>
21
+ [3]: <Channel Sale Tokens>
16
22
 
17
23
  {
18
24
  encoded: <Encoded Trade Data String>
@@ -21,8 +27,10 @@ const hexAsUtf = n => !n ? '' : Buffer.from(n.value, 'hex').toString('utf8');
21
27
  @returns
22
28
  {
23
29
  [trade]: {
24
- description: <Description of Trade String>
25
- secret: <Trade Secret String>
30
+ [channel]: <Channel Sale Capacity Tokens Number>
31
+ [description]: <Description of Trade String>
32
+ [price]: <Trade Price String>
33
+ [secret]: <Trade Secret String>
26
34
  }
27
35
  }
28
36
  */
@@ -41,16 +49,36 @@ module.exports = ({encoded}) => {
41
49
 
42
50
  const {records} = decodeTlvStream({encoded: base64AsHex(data)});
43
51
 
52
+ const channel = findChannel(records);
53
+ const description = findDescription(records);
54
+ const price = findPrice(records);
44
55
  const secret = findSecret(records);
45
56
 
46
- if (!secret.value) {
47
- return {};
57
+ if (!!channel) {
58
+ // Make sure the channel value is reasonable
59
+ try {
60
+ decodeNumber(channel.value);
61
+ } catch {
62
+ return {};
63
+ }
64
+
65
+ return {
66
+ trade: {
67
+ channel: decodeNumber(channel.value),
68
+ price: !!price ? hexAsUtf(price) : undefined,
69
+ },
70
+ };
71
+ }
72
+
73
+ if (!!description && !!secret) {
74
+ return {
75
+ trade: {
76
+ description: hexAsUtf(description),
77
+ price: !!price ? hexAsUtf(price) : undefined,
78
+ secret: hexAsUtf(secret),
79
+ },
80
+ };
48
81
  }
49
82
 
50
- return {
51
- trade: {
52
- description: hexAsUtf(findDescription(records)),
53
- secret: hexAsUtf(secret),
54
- },
55
- };
83
+ return {};
56
84
  };
@@ -55,6 +55,12 @@ module.exports = ({network, records}) => {
55
55
  throw new Error('ExpectedNodesRecordToDecodeOpenTradeDetails');
56
56
  }
57
57
 
58
+ try {
59
+ decodeTlvStream({encoded: nodesRecord.value});
60
+ } catch (err) {
61
+ throw new Error('ExpectedValidNodesTlvStreamToDecodeOpenTradeDetails');
62
+ }
63
+
58
64
  const nodeRecords = decodeTlvStream({encoded: nodesRecord.value}).records;
59
65
 
60
66
  if (!nodeRecords.length) {
@@ -2,9 +2,11 @@ const {decodeBigSize} = require('bolt01');
2
2
  const {decodeTlvStream} = require('bolt01');
3
3
 
4
4
  const decodeOpenTrade = require('./decode_open_trade');
5
+ const decodeTradePayment = require('./decode_trade_payment');
5
6
  const decodeTradeSecret = require('./decode_trade_secret');
6
7
  const networkFromNetworkRecord = require('./network_from_network_record');
7
8
 
9
+ const findDetailsRecord = records => records.find(n => n.type === '3');
8
10
  const findNetwork = records => records.find(n => n.type === '1');
9
11
  const findVer = records => records.find(n => n.type === '0') || {value: '00'};
10
12
  const findRequestRecord = records => records.find(n => n.type === '2');
@@ -39,6 +41,9 @@ const tradeData = trade => trade.slice('626f73ff'.length);
39
41
  }
40
42
  }]
41
43
  }
44
+ [payment]: {
45
+ request: <BOLT 11 Payment Request String>
46
+ }
42
47
  [secret]: {
43
48
  auth: <Encrypted Payload Auth Hex String>
44
49
  payload: <Preimage Encrypted Payload Hex String>
@@ -81,10 +86,17 @@ module.exports = ({trade}) => {
81
86
  // Decode the network record
82
87
  const {network} = networkFromNetworkRecord({value: networkRecord.value});
83
88
 
84
- // A trade is either a trade secret to someone or an open trade offer
85
- if (!!findRequestRecord(records)) {
89
+ const requestRecord = findRequestRecord(records);
90
+
91
+ // Exit early when this is a secret to trade
92
+ if (!!requestRecord && !!findDetailsRecord(records)) {
86
93
  return {secret: decodeTradeSecret({network, records})};
87
- } else {
88
- return {connect: decodeOpenTrade({network, records})};
89
94
  }
95
+
96
+ // Exit early when this is just a payment request
97
+ if (!!requestRecord) {
98
+ return {payment: decodeTradePayment({network, records})};
99
+ }
100
+
101
+ return {connect: decodeOpenTrade({network, records})};
90
102
  };
@@ -0,0 +1,45 @@
1
+ const {requestRecordsAsRequest} = require('./../records');
2
+
3
+ const findRequest = records => records.find(n => n.type === '2');
4
+ const {isArray} = Array;
5
+
6
+ /** Decode a trade payment request
7
+
8
+ 2: <Payment Request Record>
9
+
10
+ {
11
+ network: <Network Name String>
12
+ records: [{
13
+ type: <Type Number String>
14
+ value: <Hex Encoded Value String>
15
+ }]
16
+ }
17
+
18
+ @throws
19
+ <Error>
20
+
21
+ @returns
22
+ {
23
+ request: <BOLT 11 Payment Request String>
24
+ }
25
+ */
26
+ module.exports = ({network, records}) => {
27
+ if (!network) {
28
+ throw new Error('ExpectedNetworkNameToDecodeTradePayment');
29
+ }
30
+
31
+ if (!isArray(records)) {
32
+ throw new Error('ExpectedArrayOfRecordsToDecodeTradePayment');
33
+ }
34
+
35
+ // Get the payment request record
36
+ const requestRecord = findRequest(records);
37
+
38
+ if (!requestRecord) {
39
+ throw new Error('ExpectedRequestRecordInTradeDataForTradePayment');
40
+ }
41
+
42
+ const encoded = requestRecord.value;
43
+
44
+ return {request: requestRecordsAsRequest({encoded, network}).request};
45
+ };
@@ -1,8 +1,11 @@
1
+ const {encodeBigSize} = require('bolt01');
1
2
  const {encodeTlvStream} = require('bolt01');
2
3
 
3
4
  const anchorPrefix = 'anchor-trade-secret:';
4
5
  const descriptionRecord = value => !!value ? ({value, type: '1'}) : undefined;
5
- const secretRecord = value => ({value, type: '0'});
6
+ const priceRecord = value => !!value ? ({value, type: '2'}) : undefined;
7
+ const secretRecord = value => !!value ? ({value, type: '0'}) : undefined;
8
+ const typeForChannelSale = '3';
6
9
  const utf8AsHex = utf8 => Buffer.from(utf8, 'utf8').toString('hex');
7
10
  const withPrefix = (pre, v) => pre + Buffer.from(v, 'hex').toString('base64');
8
11
 
@@ -11,12 +14,16 @@ const withPrefix = (pre, v) => pre + Buffer.from(v, 'hex').toString('base64');
11
14
  This will look like anchored-trade-secret:<base64-encoded-data>
12
15
 
13
16
  The data encodes a TLV:
14
- 0: <Trade Secret>
17
+ [0]: <Trade Secret>
15
18
  [1]: <Trade Description>
19
+ [2]: <Price Expression>
20
+ [3]: <Channel Sale Tokens>
16
21
 
17
22
  {
23
+ [channel]: <Channel Sale Tokens Number>
18
24
  [description]: <Open Trade Description String>
19
- secret: <Open Trade Secret String>
25
+ [price]: <Trade Price String>
26
+ [secret]: <Open Trade Secret String>
20
27
  }
21
28
 
22
29
  @returns
@@ -24,11 +31,24 @@ const withPrefix = (pre, v) => pre + Buffer.from(v, 'hex').toString('base64');
24
31
  encoded: <Encoded Anchor Trade Data Blob String>
25
32
  }
26
33
  */
27
- module.exports = ({description, secret}) => {
28
- const records = [secretRecord(secret), descriptionRecord(description)]
34
+ module.exports = ({channel, description, price, secret}) => {
35
+ const elements = [
36
+ secretRecord(secret),
37
+ descriptionRecord(description),
38
+ priceRecord(price),
39
+ ];
40
+
41
+ const records = elements
29
42
  .filter(n => !!n)
30
43
  .map(({type, value}) => ({type, value: utf8AsHex(value)}));
31
44
 
45
+ if (!!channel) {
46
+ records.push({
47
+ type: typeForChannelSale,
48
+ value: encodeBigSize({number: channel.toString()}).encoded,
49
+ });
50
+ }
51
+
32
52
  const {encoded} = encodeTlvStream({records});
33
53
 
34
54
  return {encoded: withPrefix(anchorPrefix, encoded)};
@@ -44,7 +44,7 @@ module.exports = ({connect, secret}) => {
44
44
  }
45
45
 
46
46
  if (!!connect && !!secret) {
47
- throw new Error('ExpectedEitherConnectDetailsOrTradeSecretNotBoth');
47
+ throw new Error('ExpectedOnlyOneConnectDetailsOrTradeSecretNotBoth');
48
48
  }
49
49
 
50
50
  // Exit early when this is an open ended trade
@@ -0,0 +1,38 @@
1
+ const {encodeTlvStream} = require('bolt01');
2
+
3
+ const networkRecordFromRequest = require('./network_record_from_request');
4
+ const {requestAsRequestRecords} = require('./../records');
5
+
6
+ const asTrade = hex => `626f73ff${hex}`;
7
+
8
+ /** Encode a trade for a regular payment
9
+
10
+ [1]: <Network Name>
11
+ 2: <Payment Request Record>
12
+
13
+ {
14
+ request: <BOLT 11 Payment Request String>
15
+ }
16
+
17
+ @returns
18
+ {
19
+ trade: <Hex Encoded Trade String>
20
+ }
21
+ */
22
+ module.exports = ({request}, cbk) => {
23
+ // Encode the request into a trade record
24
+ const tradeRecords = [{
25
+ type: '2',
26
+ value: requestAsRequestRecords({request}).encoded,
27
+ }];
28
+
29
+ // Add a network name record if required
30
+ if (!!networkRecordFromRequest({request}).value) {
31
+ tradeRecords.push({
32
+ type: '1',
33
+ value: networkRecordFromRequest({request}).value,
34
+ });
35
+ }
36
+
37
+ return {trade: asTrade(encodeTlvStream({records: tradeRecords}).encoded)};
38
+ };
@@ -38,7 +38,7 @@ module.exports = ({auth, payload, request}, cbk) => {
38
38
  records: [{type: '0', value: payload}, {type: '1', value: auth}],
39
39
  });
40
40
 
41
- // Encode the details of the trade
41
+ // Encode the encrypted elements of the trade
42
42
  const detailsRecords = encodeTlvStream({
43
43
  records: [{type: '0', value: encryptionRecords.encoded}],
44
44
  });
@@ -0,0 +1,244 @@
1
+ const {createHash} = require('crypto');
2
+ const {randomBytes} = require('crypto');
3
+
4
+ const asyncAuto = require('async/auto');
5
+ const {createHodlInvoice} = require('ln-service');
6
+ const {createInvoice} = require('ln-service');
7
+ const {getInvoice} = require('ln-service');
8
+ const {getChainFeeRate} = require('ln-service');
9
+ const {parsePaymentRequest} = require('ln-service');
10
+ const {returnResult} = require('asyncjs-util');
11
+
12
+ const convertFiat = require('./convert_fiat');
13
+ const decodeAnchoredTrade = require('./decode_anchored_trade');
14
+ const encodeTradePayment = require('./encode_trade_payment');
15
+ const encodeTradeSecret = require('./encode_trade_secret');
16
+ const encryptTradeSecret = require('./encrypt_trade_secret');
17
+
18
+ const asNumber = n => parseFloat(n, 10);
19
+ const bufferAsHex = buffer => buffer.toString('hex');
20
+ const {ceil} = Math;
21
+ const defaultShortInvoiceExpiryMs = 1000 * 60 * 30;
22
+ const estimatedChannelOpenSizeVbytes = 250;
23
+ const futureDate = ms => new Date(Date.now() + ms).toISOString();
24
+ const hexAsBuffer = hex => Buffer.from(hex, 'hex');
25
+ const makePaymentSecret = () => randomBytes(32).toString('hex');
26
+ const sha256 = preimage => createHash('sha256').update(preimage).digest();
27
+ const slowConf = 144;
28
+ const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
29
+ const utf8AsHex = utf8 => Buffer.from(utf8, 'utf8').toString('hex');
30
+
31
+ /** Create a trade secret for a node
32
+
33
+ {
34
+ [channel]: <Sell Channel With Capacity Tokens Number>
35
+ [description]: <Trade Description String>
36
+ expires_at: <Trade Expires At ISO 8601 Date String>
37
+ [id]: <Trade Anchor Id Hex String>
38
+ is_hold: <Create Hold Invoice Bool>
39
+ lnd: <Authenticated LND API Object>
40
+ [price]: <Trade Price String>
41
+ [request]: <Request Function>
42
+ [secret]: <Secret Payload String>
43
+ to: <To Public Key Hex Encoded String>
44
+ [tokens]: <Price Tokens Number or Fiat>
45
+ }
46
+
47
+ @returns via cbk or Promise
48
+ {
49
+ id: <Payment Hash Hex String>
50
+ [secret]: <Hex Encoded Payment Secret String>
51
+ trade: <Hex Encoded Trade String>
52
+ }
53
+ */
54
+ module.exports = (args, cbk) => {
55
+ return new Promise((resolve, reject) => {
56
+ return asyncAuto({
57
+ // Check arguments
58
+ validate: cbk => {
59
+ if (!args.channel && !args.description) {
60
+ return cbk([400, 'ExpectedDescriptionToFinalizeTrade']);
61
+ }
62
+
63
+ if (!!args.channel && !args.is_hold) {
64
+ return cbk([400, 'ExpectedChannelAsHoldInvoiceToFinalizeTrade']);
65
+ }
66
+
67
+ if (!args.expires_at) {
68
+ return cbk([400, 'ExpectedExpiresAtDateToFinalizeTrade']);
69
+ }
70
+
71
+ if (!args.lnd) {
72
+ return cbk([400, 'ExpectedAuthenticatedLndToFinalizeTrade']);
73
+ }
74
+
75
+ if (!args.price && !args.tokens) {
76
+ return cbk([400, 'ExpectedTokensPriceToFinalizeTrade']);
77
+ }
78
+
79
+ if (!!args.price && !args.request) {
80
+ return cbk([400, 'ExpectedRequestFunctionToFinalizeTrade']);
81
+ }
82
+
83
+ if (!args.channel && !args.secret) {
84
+ return cbk([400, 'ExpectedChannelOrSecretValueToFinalizeTrade']);
85
+ }
86
+
87
+ if (!args.to) {
88
+ return cbk([400, 'ExpectedToPublicKeyToFinalizeTrade']);
89
+ }
90
+
91
+ return cbk();
92
+ },
93
+
94
+ // Description for the final invoice
95
+ description: ['validate', ({}, cbk) => {
96
+ if (!args.channel) {
97
+ return cbk(null, args.description);
98
+ }
99
+
100
+ // Make a description for a channel creation
101
+ const capacity = tokensAsBigUnit(args.channel);
102
+ const node = args.to;
103
+
104
+ const description = `Attempt ${capacity} channel creation to ${node}`;
105
+
106
+ return cbk(null, description);
107
+ }],
108
+
109
+ // Encrypt the secret data
110
+ encrypt: ['validate', ({}, cbk) => {
111
+ // Exit early when there is no trade of a secret
112
+ if (!args.secret) {
113
+ return cbk(null, {payment_secret: makePaymentSecret()});
114
+ }
115
+
116
+ return encryptTradeSecret({
117
+ lnd: args.lnd,
118
+ secret: utf8AsHex(args.secret),
119
+ to: args.to,
120
+ },
121
+ cbk);
122
+ }],
123
+
124
+ // Expiration for the final invoice
125
+ expiry: ['validate', ({}, cbk) => {
126
+ // Exit early when there is no price expression
127
+ if (!args.price) {
128
+ return cbk(null, args.expires_at);
129
+ }
130
+
131
+ // Price expressions get nearer term expiration dates
132
+ const shortDate = futureDate(defaultShortInvoiceExpiryMs);
133
+
134
+ // Check to make sure the short date doesn't go past the expiry
135
+ if (shortDate > args.expires_at) {
136
+ return cbk([410, 'TradeIsExpired']);
137
+ }
138
+
139
+ return cbk(null, shortDate);
140
+ }],
141
+
142
+ // Get the current chain fee rate to add a charge for channel sales
143
+ getFeeRate: ['validate', ({}, cbk) => {
144
+ // Exit early when this is not a channel sale
145
+ if (!args.channel) {
146
+ return cbk();
147
+ }
148
+
149
+ return getChainFeeRate({
150
+ confirmation_target: slowConf,
151
+ lnd: args.lnd,
152
+ },
153
+ cbk);
154
+ }],
155
+
156
+ // Get the sale price in tokens
157
+ getTokens: ['validate', ({}, cbk) => {
158
+ // Exit early when there is no price expression
159
+ if (!args.price) {
160
+ return cbk(null, {cost: args.tokens});
161
+ }
162
+
163
+ // Convert the price expression into a tokens cost
164
+ return convertFiat({price: args.price, request: args.request}, cbk);
165
+ }],
166
+
167
+ // Calculate the final sale price
168
+ tokens: ['getFeeRate', 'getTokens', ({getFeeRate, getTokens}, cbk) => {
169
+ // Exit early when there is no need to add chain fee for channel sales
170
+ if (!args.channel) {
171
+ return cbk(null, getTokens.cost);
172
+ }
173
+
174
+ // Create a surcharge for the vbytes cost of a channel open
175
+ const vbytes = estimatedChannelOpenSizeVbytes;
176
+
177
+ const chainFeeCharge = ceil(getFeeRate.tokens_per_vbyte * vbytes);
178
+
179
+ return cbk(null, getTokens.cost + chainFeeCharge);
180
+ }],
181
+
182
+ // Create the final purchasing invoice
183
+ createInvoice: [
184
+ 'description',
185
+ 'encrypt',
186
+ 'expiry',
187
+ 'tokens',
188
+ ({description, encrypt, expiry, tokens}, cbk) =>
189
+ {
190
+ // Exit early when this is a hold invoice
191
+ if (!!args.is_hold) {
192
+ return createHodlInvoice({
193
+ description,
194
+ tokens,
195
+ expires_at: expiry,
196
+ id: bufferAsHex(sha256(hexAsBuffer(encrypt.payment_secret))),
197
+ lnd: args.lnd,
198
+ secret: encrypt.payment_secret,
199
+ },
200
+ cbk);
201
+ }
202
+
203
+ return createInvoice({
204
+ description,
205
+ tokens,
206
+ expires_at: args.expires_at,
207
+ lnd: args.lnd,
208
+ secret: encrypt.payment_secret,
209
+ },
210
+ cbk);
211
+ }],
212
+
213
+ // Encode all the trade data into wire format
214
+ encode: ['createInvoice', 'encrypt', ({createInvoice, encrypt}, cbk) => {
215
+ // Exit early when this is a channel trade, there is no secret
216
+ if (!!args.channel) {
217
+ return cbk(null, {
218
+ id: createInvoice.id,
219
+ secret: encrypt.payment_secret,
220
+ trade: encodeTradePayment({request: createInvoice.request}).trade,
221
+ });
222
+ }
223
+
224
+ try {
225
+ // Encode the trade secret records
226
+ const {trade} = encodeTradeSecret({
227
+ auth: encrypt.trade_auth_tag,
228
+ payload: encrypt.trade_cipher,
229
+ request: createInvoice.request,
230
+ });
231
+
232
+ return cbk(null, {
233
+ trade,
234
+ id: createInvoice.id,
235
+ secret: encrypt.payment_secret,
236
+ });
237
+ } catch (err) {
238
+ return cbk([500, err.message]);
239
+ }
240
+ }],
241
+ },
242
+ returnResult({reject, resolve, of: 'encode'}, cbk));
243
+ });
244
+ };