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.
@@ -0,0 +1,183 @@
1
+ const addPeer = require('ln-service');
2
+ const asyncAuto = require('async/auto');
3
+ const asyncDetect = require('async/detect');
4
+ const asyncDetectSeries = require('async/detectSeries');
5
+ const asyncMap = require('async/map');
6
+ const {getChannel} = require('ln-service');
7
+ const {getIdentity} = require('ln-service');
8
+ const {getNode} = require('ln-service');
9
+ const {getPeers} = require('ln-service');
10
+ const {returnResult} = require('asyncjs-util');
11
+
12
+ const {isArray} = Array;
13
+ const uniq = arr => Array.from(new Set(arr));
14
+
15
+ /** Connect to a seller
16
+
17
+ {
18
+ lnd: <Authenticated LND API Object>
19
+ logger: <Winston Logger Object>
20
+ nodes: [{
21
+ [high_channel]: <High Key Channel Id String>
22
+ [low_channel]: <Low Key Channel Id String>
23
+ [node]: {
24
+ id: <Node Public Key Id Hex String>
25
+ sockets: [<Peer Socket String>]
26
+ }
27
+ }]
28
+ }
29
+
30
+ @returns via cbk or Promise
31
+ {
32
+ id: <Seller Public Key Hex String>
33
+ }
34
+ */
35
+ module.exports = ({lnd, logger, nodes}, cbk) => {
36
+ return new Promise((resolve, reject) => {
37
+ return asyncAuto({
38
+ // Check arguments
39
+ validate: cbk => {
40
+ if (!logger) {
41
+ return cbk([400, 'ExpectedWinstonLoggerToConnectToSeller']);
42
+ }
43
+
44
+ if (!lnd) {
45
+ return cbk([400, 'ExpectedLndToConnectToSeller']);
46
+ }
47
+
48
+ if (!isArray(nodes)) {
49
+ return cbk([400, 'ExpectedArrayOfNodesToConnectToSeller']);
50
+ }
51
+
52
+ return cbk();
53
+ },
54
+
55
+ // Derive the self public key
56
+ getIdentity: ['validate', ({}, cbk) => getIdentity({lnd}, cbk)],
57
+
58
+ // Get the list of connected peers
59
+ getPeers: ['validate', ({}, cbk) => getPeers({lnd}, cbk)],
60
+
61
+ // Find node connect info to connect to
62
+ getNodes: ['getIdentity', ({getIdentity}, cbk) => {
63
+ return asyncMap(nodes, (connect, cbk) => {
64
+ // Exit early when the node is self
65
+ if (!!connect.node && connect.node.id === getIdentity.public_key) {
66
+ return cbk();
67
+ }
68
+
69
+ // Exit early when the node id and sockets is specified directly
70
+ if (!!connect.node) {
71
+ return getNode({
72
+ lnd,
73
+ is_omitting_channels: true,
74
+ public_key: connect.node.id,
75
+ },
76
+ (err, res) => {
77
+ if (!!err) {
78
+ return cbk(null, connect.node);
79
+ }
80
+
81
+ const sockets = res.sockets.map(n => n.socket);
82
+
83
+ return cbk(null, {
84
+ id: connect.node.id,
85
+ sockets: uniq([].concat(sockets).concat(connect.node.sockets)),
86
+ });
87
+ });
88
+ }
89
+
90
+ // Exit early when there is no landmark channel
91
+ if (!connect.high_channel && !connect.low_channel) {
92
+ return cbk();
93
+ }
94
+
95
+ const channelId = connect.high_channel || connect.low_channel;
96
+
97
+ // Get the public keys of the landmark channel
98
+ return getChannel({lnd, id: channelId}, (err, res) => {
99
+ // Exit early when the channel cannot be fetched
100
+ if (!!err) {
101
+ return cbk();
102
+ }
103
+
104
+ const [low, high] = res.policies.map(n => n.public_key);
105
+
106
+ // The node key is either the lower or the higher public key
107
+ const nodeId = !!connect.low_channel ? low : high;
108
+
109
+ // Exit early when the node is self
110
+ if (nodeId === getIdentity.public_key) {
111
+ return cbk();
112
+ }
113
+
114
+ // Find the sockets for the node
115
+ return getNode({
116
+ lnd,
117
+ is_omitting_channels: true,
118
+ public_key: nodeId,
119
+ },
120
+ (err, res) => {
121
+ if (!!err) {
122
+ return cbk();
123
+ }
124
+
125
+ return cbk(null, {
126
+ id: nodeId,
127
+ sockets: res.sockets.map(n => n.socket),
128
+ });
129
+ });
130
+ });
131
+ },
132
+ cbk);
133
+ }],
134
+
135
+ // Try and connect to a node in order to do p2p messaging
136
+ connect: ['getNodes', 'getPeers', ({getNodes, getPeers}, cbk) => {
137
+ const connected = getPeers.peers.map(n => n.public_key);
138
+
139
+ // Look for a node that is already a connected peer
140
+ const [alreadyConnected] = getNodes.filter(node => {
141
+ return !!node && connected.includes(node.id);
142
+ });
143
+
144
+ // Exit early when already connected to a node
145
+ if (!!alreadyConnected) {
146
+ return cbk(null, alreadyConnected);
147
+ }
148
+
149
+ // Try and connect to a referenced node
150
+ return asyncDetect(getNodes.filter(n => !!n), (node, cbk) => {
151
+ logger.info({connecting_to: node.id});
152
+
153
+ // Attempt referenced sockets to establish p2p connection
154
+ return asyncDetectSeries(node.sockets, (socket, cbk) => {
155
+ return addPeer({lnd, socket, public_key: node.id}, err => {
156
+ // Stop trying sockets when there is no error
157
+ return cbk(null, !err);
158
+ });
159
+ },
160
+ (err, socket) => {
161
+ if (!!err) {
162
+ return cbk(err);
163
+ }
164
+
165
+ // The node is connected if one of the sockets worked
166
+ return cbk(null, !!socket);
167
+ });
168
+ },
169
+ cbk);
170
+ }],
171
+
172
+ // Connected to seller
173
+ connected: ['connect', ({connect}, cbk) => {
174
+ if (!connect) {
175
+ return cbk([503, 'FailedToConnectToNode']);
176
+ }
177
+
178
+ return cbk(null, {id: connect.id});
179
+ }],
180
+ },
181
+ returnResult({reject, resolve, of: 'connected'}, cbk));
182
+ });
183
+ };
@@ -0,0 +1,112 @@
1
+ const asyncAuto = require('async/auto');
2
+ const asyncRetry = require('async/retry');
3
+ const {getPrices} = require('@alexbosworth/fiat');
4
+ const {returnResult} = require('asyncjs-util');
5
+
6
+ const cached = {};
7
+ const convert = (price, rate) => Math.round((price * 1e8) / (rate / 100));
8
+ const defaultFiatRateProvider = 'coingecko';
9
+ const interval = 1000;
10
+ const isFresh = date => Date.now() - date > 1000 * 60 * 10;
11
+ let isLocked = false;
12
+ const isNumber = n => !isNaN(n);
13
+ const {now} = Date;
14
+ const parseFiat = n => n.split('*')[1];
15
+ const parseFiatPrice = n => Number(n.split('*')[0]);
16
+ const removeSpaces = s => s.replace(/\s/g, '');
17
+
18
+ /** Converts fiat to tokens
19
+
20
+ A fiat price expression looks like AMOUNT*USD
21
+
22
+ {
23
+ price: <Fiat Price Expression String>
24
+ request: <Request Function>
25
+ }
26
+
27
+ @returns
28
+ {
29
+ cost: <Price of Trade in Tokens>
30
+ }
31
+ */
32
+ module.exports = ({price, request}, cbk) => {
33
+ return new Promise((resolve, reject) => {
34
+ return asyncAuto({
35
+ // Check arguments
36
+ validate: cbk => {
37
+ if (!price) {
38
+ return cbk([400, 'ExpectedFiatPriceToConvertFiat']);
39
+ }
40
+
41
+ if (!request) {
42
+ return cbk([400, 'ExpectedRequestFunctionToConvertFiat']);
43
+ }
44
+
45
+ if (!isNumber(parseFiatPrice(price))) {
46
+ return cbk([400, 'ExpectedFiatPriceNumberToConvert']);
47
+ }
48
+
49
+ return cbk();
50
+ },
51
+
52
+ // Wait for request lock to unlock
53
+ waitForLock: ['validate', ({}, cbk) => {
54
+ return asyncRetry({interval}, cbk => {
55
+ if (isLocked) {
56
+ return cbk([503, 'FailedToGetFiatRatesDueToBusyRateLookups']);
57
+ }
58
+
59
+ return cbk();
60
+ },
61
+ cbk);
62
+ }],
63
+
64
+ // Get the current exchange rate
65
+ getRate: ['waitForLock', ({}, cbk) => {
66
+ const fiat = parseFiat(removeSpaces(price));
67
+
68
+ // Exit early when the rate has a fresh cache
69
+ if (!!cached[fiat] && isFresh(cached[fiat].date)) {
70
+ return cbk(null, cached[fiat].rate);
71
+ }
72
+
73
+ // Enable the request lock to avoid making simultaneous requests
74
+ isLocked = true;
75
+
76
+ return getPrices({
77
+ request,
78
+ from: defaultFiatRateProvider,
79
+ symbols: [fiat],
80
+ },
81
+ (err, res) => {
82
+ // Disable the request lock to allow another request through
83
+ isLocked = false;
84
+
85
+ if (!!err) {
86
+ return cbk(err);
87
+ }
88
+
89
+ const [{rate}] = res.tickers;
90
+
91
+ if (!rate) {
92
+ return cbk([503, 'ExpectedFiatRateFromRateProvider']);
93
+ }
94
+
95
+ // Update the value cache to avoid hammering the service
96
+ cached[fiat] = {rate, date: now()};
97
+
98
+ // Return the fresh value
99
+ return cbk(null, rate);
100
+ });
101
+ }],
102
+
103
+ // Convert the price into a tokens cost
104
+ cost: ['getRate', ({getRate}, cbk) => {
105
+ const cost = convert(parseFiatPrice(price), getRate);
106
+
107
+ return cbk(null, {cost});
108
+ }],
109
+ },
110
+ returnResult({reject, resolve, of: 'cost'}, cbk));
111
+ });
112
+ };
@@ -4,17 +4,23 @@ const {returnResult} = require('asyncjs-util');
4
4
 
5
5
  const encodeAnchoredTrade = require('./encode_anchored_trade');
6
6
 
7
- const maxDescriptionLength = 100;
7
+ const maxDescLength = 100;
8
8
  const maxSecretLength = 100;
9
9
 
10
10
  /** Create an anchored trade
11
11
 
12
+ Either a channel or description and secret is required
13
+
14
+ Either a price or tokens is required
15
+
12
16
  {
13
- description: <Trade Description String>
17
+ [channel]: <Channel Sale Capacity Tokens Number>
18
+ [description]: <Trade Description String>
14
19
  expires_at: <Trade Expires at ISO 8601 Date String>
15
20
  lnd: <Authenticated LND API Object>
16
- secret: <Trade Secret String>
17
- tokens: <Trade Price Tokens Number>
21
+ [price]: <Trade Price String>
22
+ [secret]: <Trade Secret String>
23
+ [tokens]: <Trade Price Tokens Number>
18
24
  }
19
25
 
20
26
  @returns via cbk or Promise
@@ -27,11 +33,15 @@ module.exports = (args, cbk) => {
27
33
  return asyncAuto({
28
34
  // Check arguments
29
35
  validate: cbk => {
30
- if (args.description === undefined) {
36
+ if (!args.channel && !args.description) {
31
37
  return cbk([400, 'ExpectedTradeDescriptionToCreateAnchoredTrade']);
32
38
  }
33
39
 
34
- if (args.description.length > maxDescriptionLength) {
40
+ if (!args.channel && !args.secret) {
41
+ return cbk([400, 'ExpectedEitherChannelOrSecretToTrade']);
42
+ }
43
+
44
+ if (!!args.description && args.description.length > maxDescLength) {
35
45
  return cbk([400, 'ExpectedShorterTradeDescriptionToCreateTrade']);
36
46
  }
37
47
 
@@ -47,28 +57,27 @@ module.exports = (args, cbk) => {
47
57
  return cbk([400, 'ExpectedAuthenticatedLndToCreateAnchoredTrade']);
48
58
  }
49
59
 
50
- if (!args.secret) {
51
- return cbk([400, 'ExpectedTradeSecretToCreateAnchoredTrade']);
60
+ if (!args.price && !args.tokens) {
61
+ return cbk([400, 'ExpectedTokensPriceToCreateAnchoredTrade']);
52
62
  }
53
63
 
54
- if (args.secret.length > maxSecretLength) {
64
+ if (!!args.secret && args.secret.length > maxSecretLength) {
55
65
  return cbk([400, 'ExpectedShorterTradeSecretToCreateAnchoredTrade']);
56
66
  }
57
67
 
58
- if (!args.tokens) {
59
- return cbk([400, 'ExpectedTokensPriceToCreateAnchoredTrade']);
60
- }
61
-
62
68
  return cbk();
63
69
  },
64
70
 
65
71
  // Create the trade
66
72
  create: ['validate', ({}, cbk) => {
67
73
  const {encoded} = encodeAnchoredTrade({
74
+ channel: args.channel,
68
75
  description: args.description,
76
+ price: args.price,
69
77
  secret: args.secret,
70
78
  });
71
79
 
80
+ // The anchor invoice description will contain the trade details
72
81
  return createInvoice({
73
82
  description: encoded,
74
83
  expires_at: args.expires_at,