paid-services 3.3.0 → 3.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/.prettierignore +2 -0
- package/CHANGELOG.md +17 -0
- package/capacity/accept_capacity_change.js +3 -3
- package/capacity/ask_for_change_details.js +34 -1
- package/capacity/capacity_change_requests.js +9 -0
- package/capacity/change_channel_capacity.js +7 -1
- package/capacity/encode_change_request.js +8 -1
- package/capacity/fee_for_replacement.js +1 -1
- package/capacity/get_capacity_change_requests.js +2 -1
- package/capacity/get_capacity_replacement.js +1 -1
- package/capacity/initiate_capacity_change.js +5 -5
- package/capacity/parse_capacity_change_request.js +8 -1
- package/capacity/propose_capacity_change.js +4 -1
- package/p2p/service_peer_requests.js +10 -2
- package/package.json +10 -9
- package/service_types.json +16 -0
- package/services/response_for_relay.js +0 -1
- package/test/capacity/test_capacity_change_requests.js +1 -0
- package/test/capacity/test_encode_change_request.js +5 -0
- package/test/capacity/test_parse_capacity_change_request.js +1 -0
- package/test/integration/test_accept_capacity_change.js +50 -14
- package/test/integration/test_change_channel_capacity.js +7 -1
- package/test/integration/test_decrease_channel_capacity.js +16 -7
- package/test/integration/test_increase_channel_capacity.js +4 -1
- package/test/trades/test_decode_basic_trade.js +40 -0
- package/test/trades/test_decode_open_trade.js +50 -0
- package/test/trades/test_encode_open_trade.js +35 -0
- package/test/trades/test_encode_trade.js +37 -0
- package/trades/accept_trade.js +61 -0
- package/trades/create_trade.js +149 -37
- package/trades/decode_anchored_trade.js +56 -0
- package/trades/decode_basic_trade.js +45 -0
- package/trades/decode_open_trade.js +69 -0
- package/trades/decode_trade.js +48 -66
- package/trades/decode_trade_secret.js +89 -0
- package/trades/encode_anchored_trade.js +35 -0
- package/trades/encode_open_trade.js +89 -0
- package/trades/encode_trade.js +48 -33
- package/trades/encode_trade_secret.js +50 -0
- package/trades/finalize_trade_secret.js +123 -0
- package/trades/find_trade.js +339 -0
- package/trades/manage_trade.js +61 -39
- package/trades/manage_trades.js +209 -2
- package/trades/network_from_network_record.js +3 -3
- package/trades/network_record_from_network.js +36 -0
- package/trades/network_record_from_request.js +2 -1
- package/trades/node_as_record.js +98 -0
- package/trades/record_as_node.js +113 -0
- package/trades/request_trade_by_id.js +95 -0
- package/trades/service_anchored_trades.js +181 -0
- package/trades/service_open_trade.js +172 -0
- package/trades/service_trade_requests.js +255 -0
- package/trades/trade_from_invoice.js +51 -0
- package/capacity/service_types.json +0 -6
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
const EventEmitter = require('events');
|
|
2
|
+
|
|
3
|
+
const asyncUntil = require('async/until');
|
|
4
|
+
const {getInvoices} = require('ln-service');
|
|
5
|
+
const {subscribeToInvoice} = require('ln-service');
|
|
6
|
+
const {subscribeToInvoices} = require('ln-service');
|
|
7
|
+
|
|
8
|
+
const decodeAnchoredTrade = require('./decode_anchored_trade');
|
|
9
|
+
const serviceTradeRequests = require('./service_trade_requests');
|
|
10
|
+
const tradeFromInvoice = require('./trade_from_invoice');
|
|
11
|
+
|
|
12
|
+
const defaultInvoicesLimit = 100;
|
|
13
|
+
const events = ['details', 'error', 'failure', 'settled', 'trade'];
|
|
14
|
+
const fromNow = date => new Date(date) - new Date();
|
|
15
|
+
const {keys} = Object;
|
|
16
|
+
|
|
17
|
+
/** Service trades represented by anchored invoices
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
lnd: <Authenticated LND API Object>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Returned details
|
|
24
|
+
@event 'details'
|
|
25
|
+
{
|
|
26
|
+
to: <Public Key Id Hex String>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Terminal failure
|
|
30
|
+
@event 'error'
|
|
31
|
+
<Error Object>
|
|
32
|
+
|
|
33
|
+
// Encountered a failure
|
|
34
|
+
@event 'failure'
|
|
35
|
+
[<Failure Code Number>, <Failure Code String>]
|
|
36
|
+
|
|
37
|
+
// Received payment
|
|
38
|
+
@event 'settled'
|
|
39
|
+
{
|
|
40
|
+
description: <Trade Description String>
|
|
41
|
+
id: <Trade Id Hex String>
|
|
42
|
+
secret: <Secret Payload String>
|
|
43
|
+
to: <Public Key Id Hex String>
|
|
44
|
+
tokens: <Tokens Number>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Starting service for trade
|
|
48
|
+
@event 'start'
|
|
49
|
+
{
|
|
50
|
+
description: <Trade Description String>
|
|
51
|
+
expires_at: <Trade Expires at ISO 8601 Date String>
|
|
52
|
+
id: <Trade Id Hex String>
|
|
53
|
+
secret: <Secret Payload String>
|
|
54
|
+
tokens: <Tokens Number>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Return finalized trade
|
|
58
|
+
@event 'trade'
|
|
59
|
+
{
|
|
60
|
+
to: <Public Key Id Hex String>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@returns
|
|
64
|
+
<EventEmitter Object>
|
|
65
|
+
*/
|
|
66
|
+
module.exports = ({lnd}) => {
|
|
67
|
+
const emitter = new EventEmitter();
|
|
68
|
+
const listening = {};
|
|
69
|
+
const paging = {};
|
|
70
|
+
|
|
71
|
+
// Service a trade until it's paid or canceled
|
|
72
|
+
const serviceTrade = trade => {
|
|
73
|
+
// Exit early when already listening to this trade
|
|
74
|
+
if (!!listening[trade.id]) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const sub = serviceTradeRequests({
|
|
79
|
+
lnd,
|
|
80
|
+
description: trade.description,
|
|
81
|
+
expires_at: trade.expires_at,
|
|
82
|
+
id: trade.id,
|
|
83
|
+
secret: trade.secret,
|
|
84
|
+
tokens: trade.tokens,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Pick up the trade to service
|
|
88
|
+
emitter.emit('start', {
|
|
89
|
+
description: trade.description,
|
|
90
|
+
expires_at: trade.expires_at,
|
|
91
|
+
id: trade.id,
|
|
92
|
+
secret: trade.secret,
|
|
93
|
+
tokens: trade.tokens,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Settled a trade
|
|
97
|
+
sub.on('settled', ({to}) => {
|
|
98
|
+
return emitter.emit('settled', {
|
|
99
|
+
to,
|
|
100
|
+
id: trade.id,
|
|
101
|
+
description: trade.description,
|
|
102
|
+
secret: trade.secret,
|
|
103
|
+
tokens: trade.tokens,
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Remember this listener to avoid double-listening to the same trade
|
|
108
|
+
listening[trade.id] = sub;
|
|
109
|
+
|
|
110
|
+
sub.on('details', ({to}) => emitter.emit('details', {to, id: trade.id}));
|
|
111
|
+
sub.on('failure', err => emitter.emit('failure', {err, id: trade.id}));
|
|
112
|
+
sub.on('trade', ({to}) => emitter.emit('trade', {to, id: trade.id}));
|
|
113
|
+
|
|
114
|
+
sub.once('end', () => sub.removeAllListeners());
|
|
115
|
+
|
|
116
|
+
const invoiceUpdates = subscribeToInvoice({lnd, id: trade.id});
|
|
117
|
+
|
|
118
|
+
invoiceUpdates.on('invoice_updated', updated => {
|
|
119
|
+
if (!updated.is_canceled) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return invoiceUpdates.removeAllListeners();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Listen for any new invoices that represent anchored trades
|
|
130
|
+
const sub = subscribeToInvoices({lnd});
|
|
131
|
+
|
|
132
|
+
// Listen for new trades to appear
|
|
133
|
+
sub.on('invoice_updated', invoice => {
|
|
134
|
+
const {trade} = tradeFromInvoice(invoice);
|
|
135
|
+
|
|
136
|
+
// Exit early when there is no anchored trade found in the description
|
|
137
|
+
if (!trade) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return serviceTrade(trade);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Find open anchored trades
|
|
145
|
+
asyncUntil(
|
|
146
|
+
cbk => cbk(null, paging.token === false),
|
|
147
|
+
cbk => {
|
|
148
|
+
return getInvoices({
|
|
149
|
+
lnd,
|
|
150
|
+
is_unconfirmed: true,
|
|
151
|
+
limit: !paging.token ? defaultInvoicesLimit : undefined,
|
|
152
|
+
token: paging.token,
|
|
153
|
+
},
|
|
154
|
+
(err, res) => {
|
|
155
|
+
if (!!err) {
|
|
156
|
+
emitter.emit(err);
|
|
157
|
+
|
|
158
|
+
// Stop listening to all invoices
|
|
159
|
+
sub.removeAllListeners();
|
|
160
|
+
|
|
161
|
+
// Stop listening to all anchor invoices
|
|
162
|
+
keys(listening).forEach(id => listening[id].removeAllListeners());
|
|
163
|
+
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
paging.token = res.next || false;
|
|
168
|
+
|
|
169
|
+
res.invoices
|
|
170
|
+
.map(tradeFromInvoice)
|
|
171
|
+
.map(n => n.trade)
|
|
172
|
+
.filter(n => !!n)
|
|
173
|
+
.forEach(serviceTrade);
|
|
174
|
+
|
|
175
|
+
return cbk();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return emitter;
|
|
181
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const {getNodeAlias} = require('ln-sync');
|
|
3
|
+
const {returnResult} = require('asyncjs-util');
|
|
4
|
+
|
|
5
|
+
const encodeOpenTrade = require('./encode_open_trade');
|
|
6
|
+
const serviceTradeRequests = require('./service_trade_requests');
|
|
7
|
+
|
|
8
|
+
const asNumber = n => parseFloat(n, 10);
|
|
9
|
+
const {isArray} = Array;
|
|
10
|
+
const nodeName = (alias, id) => `${alias} ${id}`;
|
|
11
|
+
const slowTradeDate = () => new Date(Date.now() + (86400000*15)).toISOString();
|
|
12
|
+
const uriAsSocket = n => n.substring(67);
|
|
13
|
+
|
|
14
|
+
/** Service an individual trade
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
channels: [{
|
|
18
|
+
id: <Standard Format Channel Id String>
|
|
19
|
+
partner_public_key: <Node Public Key Hex String>
|
|
20
|
+
}]
|
|
21
|
+
description: <Trade Description String>
|
|
22
|
+
expires_at: <Expires At ISO 8601 Date String>
|
|
23
|
+
id: <Trade Anchor Id Hex String>
|
|
24
|
+
lnd: <Authenticated LND API Object>
|
|
25
|
+
logger: <Winston Logger Object>
|
|
26
|
+
network: <Network Name String>
|
|
27
|
+
public_key: <Identity Public Key Hex String>
|
|
28
|
+
secret: <Secret to Sell String>
|
|
29
|
+
tokens: <Trade Price Tokens Number>
|
|
30
|
+
uris: [<Node URI String>]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@returns via cbk or Promise
|
|
34
|
+
*/
|
|
35
|
+
module.exports = (args, cbk) => {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
return asyncAuto({
|
|
38
|
+
// Check arguments
|
|
39
|
+
validate: cbk => {
|
|
40
|
+
if (!isArray(args.channels)) {
|
|
41
|
+
return cbk([400, 'ExpectedArrayOfChannelsToServiceTrade']);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!args.description) {
|
|
45
|
+
return cbk([400, 'ExpectedDescriptionToServiceTrade']);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!args.expires_at) {
|
|
49
|
+
return cbk([400, 'ExpectedExpiresAtDateToServiceTrade']);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!args.id) {
|
|
53
|
+
return cbk([400, 'ExpectedTradeAnchorIdToServiceTrade']);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!args.lnd) {
|
|
57
|
+
return cbk([400, 'ExpectedAuthenticatedLndToServiceTrade']);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!args.logger) {
|
|
61
|
+
return cbk([400, 'ExpectedWinstonLoggerToServiceTrade']);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!args.network) {
|
|
65
|
+
return cbk([400, 'ExpectedNetworkNameToServiceTrade']);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!args.public_key) {
|
|
69
|
+
return cbk([400, 'ExpectedIdentityPublicKeyToServiceTrade']);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!args.secret) {
|
|
73
|
+
return cbk([400, 'ExpectedSecretToSellToServiceTrade']);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!args.tokens) {
|
|
77
|
+
return cbk([400, 'ExpectedTokensPriceToServiceTrade']);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!isArray(args.uris)) {
|
|
81
|
+
return cbk([400, 'ExpectedArrayOfNodeSocketUrisToServiceTrade']);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return cbk();
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
// Serve the trade
|
|
88
|
+
serve: ['validate', ({}, cbk) => {
|
|
89
|
+
const isSlowTrade = args.expires_at > slowTradeDate();
|
|
90
|
+
const settled = [];
|
|
91
|
+
|
|
92
|
+
// Encode the open trade details to give out
|
|
93
|
+
const openTrade = encodeOpenTrade({
|
|
94
|
+
network: args.network,
|
|
95
|
+
nodes: [{
|
|
96
|
+
channels: isSlowTrade ? [] : args.channels,
|
|
97
|
+
id: args.public_key,
|
|
98
|
+
sockets: !args.channels.length ? args.uris.map(uriAsSocket) : [],
|
|
99
|
+
}],
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
args.logger.info({trade_expiry: new Date(args.expires_at)});
|
|
103
|
+
|
|
104
|
+
const sub = serviceTradeRequests({
|
|
105
|
+
description: args.description,
|
|
106
|
+
expires_at: args.expires_at,
|
|
107
|
+
id: args.id,
|
|
108
|
+
lnd: args.lnd,
|
|
109
|
+
secret: args.secret,
|
|
110
|
+
tokens: asNumber(args.tokens),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Someone asked for trade details
|
|
114
|
+
sub.on('details', async ({to}) => {
|
|
115
|
+
try {
|
|
116
|
+
const {alias, id} = await getNodeAlias({id: to, lnd: args.lnd});
|
|
117
|
+
|
|
118
|
+
const toName = nodeName(alias, id);
|
|
119
|
+
|
|
120
|
+
return args.logger.info({return_trade_info_to: toName});
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return args.logger.error({err});
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
sub.on('failure', failure => args.logger.error({failure}));
|
|
127
|
+
|
|
128
|
+
sub.on('settled', ({to}) => settled.push(to));
|
|
129
|
+
|
|
130
|
+
sub.on('trade', async ({to}) => {
|
|
131
|
+
try {
|
|
132
|
+
const {alias, id} = await getNodeAlias({id: to, lnd: args.lnd});
|
|
133
|
+
|
|
134
|
+
const toName = nodeName(alias, id);
|
|
135
|
+
|
|
136
|
+
return args.logger.info({make_trade_invoice_for: toName});
|
|
137
|
+
} catch (err) {
|
|
138
|
+
return args.logger.error({err});
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Show the trade details blob
|
|
143
|
+
return args.logger.info({
|
|
144
|
+
waiting_for_trade_request_to: openTrade.trade,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Wait for the trade to end
|
|
148
|
+
sub.once('end', async () => {
|
|
149
|
+
try {
|
|
150
|
+
const [to] = settled;
|
|
151
|
+
|
|
152
|
+
// Exit early when there was no settlement
|
|
153
|
+
if (!to) {
|
|
154
|
+
args.logger.info({trade_ended: true});
|
|
155
|
+
|
|
156
|
+
return cbk();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const {alias, id} = await getNodeAlias({id: to, lnd: args.lnd});
|
|
160
|
+
|
|
161
|
+
args.logger.info({finished_trade_with: nodeName(alias, id)});
|
|
162
|
+
} catch (err) {
|
|
163
|
+
args.logger.error({err});
|
|
164
|
+
} finally {
|
|
165
|
+
return cbk();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}],
|
|
169
|
+
},
|
|
170
|
+
returnResult({reject, resolve}, cbk));
|
|
171
|
+
});
|
|
172
|
+
};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
const EventEmitter = require('events');
|
|
2
|
+
const {randomBytes} = require('crypto');
|
|
3
|
+
|
|
4
|
+
const {getInvoice} = require('ln-service');
|
|
5
|
+
const {subscribeToInvoice} = require('ln-service');
|
|
6
|
+
|
|
7
|
+
const acceptTrade = require('./accept_trade');
|
|
8
|
+
const finalizeTradeSecret = require('./finalize_trade_secret');
|
|
9
|
+
const {makePeerRequest} = require('./../p2p');
|
|
10
|
+
const {servicePeerRequests} = require('./../p2p');
|
|
11
|
+
const {serviceTypeRequestTrades} = require('./../service_types');
|
|
12
|
+
const {serviceTypeReceiveTrades} = require('./../service_types');
|
|
13
|
+
|
|
14
|
+
const descRecord = description => ({type: '2', value: description});
|
|
15
|
+
const events = ['details', 'end', 'failure', 'settled', 'trade'];
|
|
16
|
+
const findIdRecord = records => records.find(n => n.type === '0');
|
|
17
|
+
const findRequestIdRecord = records => records.find(n => n.type === '1');
|
|
18
|
+
const hashHexLength = 64;
|
|
19
|
+
const idBackType = '0';
|
|
20
|
+
const idRecord = id => ({type: '1', value: id});
|
|
21
|
+
const msRemainingToTime = time => time - Date.now();
|
|
22
|
+
const postOpenTradeTimeoutMs = 1000 * 30;
|
|
23
|
+
const sumOf = arr => arr.reduce((sum, n) => sum + n, 0);
|
|
24
|
+
const tradeSecretType = '1';
|
|
25
|
+
const utf8AsHex = utf8 => Buffer.from(utf8).toString('hex');
|
|
26
|
+
|
|
27
|
+
/** Service requests for an open trade
|
|
28
|
+
|
|
29
|
+
The maximum expiration date is three weeks
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
description: <Trade Description String>
|
|
33
|
+
expires_at: <Trade Expires At ISO 8601 Date String>
|
|
34
|
+
id: <Trade Id Hex String>
|
|
35
|
+
lnd: <Authenticated LND API Object>
|
|
36
|
+
secret: <Secret Payload String>
|
|
37
|
+
tokens: <Tokens Number>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Return details
|
|
41
|
+
@event 'details'
|
|
42
|
+
{
|
|
43
|
+
to: <Public Key Id Hex String>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Service ended
|
|
47
|
+
@event 'end'
|
|
48
|
+
|
|
49
|
+
// Encountered a failure
|
|
50
|
+
@event 'failure'
|
|
51
|
+
[<Failure Code Number>, <Failure Code String>]
|
|
52
|
+
|
|
53
|
+
// Received payment
|
|
54
|
+
@event 'settled'
|
|
55
|
+
{
|
|
56
|
+
to: <Public Key Id Hex String>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Return finalized trade
|
|
60
|
+
@event 'trade'
|
|
61
|
+
{
|
|
62
|
+
to: <Public Key Id Hex String>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@returns
|
|
66
|
+
<Service Event Emitter Object>
|
|
67
|
+
*/
|
|
68
|
+
module.exports = args => {
|
|
69
|
+
if (!args.description) {
|
|
70
|
+
throw new Error('ExpectedTradeDescriptionToServiceTradeRequests');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!args.id) {
|
|
74
|
+
throw new Error('ExpectedTradeIdToServiceTradeRequests');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (args.id.length !== hashHexLength) {
|
|
78
|
+
throw new Error('ExpectedShorterRequestIdValueToServiceTradeRequests');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!args.lnd) {
|
|
82
|
+
throw new Error('ExpectedAuthenticatedLndToServiceTradeRequests');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!args.secret) {
|
|
86
|
+
throw new Error('ExpectedSecretToEncryptToServiceTradeRequests');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!args.tokens) {
|
|
90
|
+
throw new Error('ExpectedTokensPriceForTradeToServiceTradeRequests');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const emitter = new EventEmitter();
|
|
94
|
+
|
|
95
|
+
const holds = [];
|
|
96
|
+
const service = servicePeerRequests({lnd: args.lnd});
|
|
97
|
+
|
|
98
|
+
// Stop the service when there are no more listeners
|
|
99
|
+
emitter.on('removeListener', () => {
|
|
100
|
+
// Exit early when there are still listeners on an event
|
|
101
|
+
if (!!sumOf(events.map(n => emitter.listenerCount(n)))) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return service.stop({});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const sub = subscribeToInvoice({id: args.id, lnd: args.lnd});
|
|
109
|
+
|
|
110
|
+
sub.on('invoice_updated', invoice => {
|
|
111
|
+
if (!invoice.is_canceled) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
holds.forEach(n => n.sub.removeAllListeners());
|
|
116
|
+
|
|
117
|
+
return service.stop({});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Notify when the service ends
|
|
121
|
+
service.end(() => emitter.emit('end', {}));
|
|
122
|
+
|
|
123
|
+
// Basic trade description
|
|
124
|
+
const basicTradeRecords = [
|
|
125
|
+
idRecord(args.id),
|
|
126
|
+
descRecord(utf8AsHex(args.description)),
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
// Wait for a request for the open trade
|
|
130
|
+
service.request({type: serviceTypeRequestTrades}, (req, res) => {
|
|
131
|
+
const {failure, success} = res;
|
|
132
|
+
const requestTradeId = findIdRecord(req.records);
|
|
133
|
+
|
|
134
|
+
// Exit early when the trade requested is unrelated
|
|
135
|
+
if (!!requestTradeId && requestTradeId.value !== args.id) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const requestIdRecord = findRequestIdRecord(req.records);
|
|
140
|
+
|
|
141
|
+
// A trade request either is for a specific trade or has a pingback id
|
|
142
|
+
if (!requestTradeId && !requestIdRecord) {
|
|
143
|
+
return emitter.emit('failure', [400, 'ExpectedRequestTradeOrRequestId']);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Exit early when the trade is expired
|
|
147
|
+
if (args.expires_at < new Date().toISOString()) {
|
|
148
|
+
service.stop({});
|
|
149
|
+
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Exit early and ping peer with trade when this is an open ended query
|
|
154
|
+
if (!requestTradeId) {
|
|
155
|
+
emitter.emit('details', {to: req.from});
|
|
156
|
+
|
|
157
|
+
const reqIdRecord = {type: idBackType, value: requestIdRecord.value};
|
|
158
|
+
|
|
159
|
+
const records = [].concat(basicTradeRecords).concat(reqIdRecord);
|
|
160
|
+
|
|
161
|
+
return getInvoice({id: args.id, lnd: args.lnd}, (err, res) => {
|
|
162
|
+
if (!!err || !!res.is_canceled) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Ping the node with trade details
|
|
167
|
+
return makePeerRequest({
|
|
168
|
+
records,
|
|
169
|
+
lnd: args.lnd,
|
|
170
|
+
timeout: postOpenTradeTimeoutMs,
|
|
171
|
+
to: req.from,
|
|
172
|
+
type: serviceTypeReceiveTrades,
|
|
173
|
+
},
|
|
174
|
+
err => {
|
|
175
|
+
if (!!err) {
|
|
176
|
+
failure([503, 'FailedToDeliverTradeInfo']);
|
|
177
|
+
|
|
178
|
+
return emitter.emit(
|
|
179
|
+
'failure',
|
|
180
|
+
[503, 'FailedToDeliverTradeInfo', {err}]
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return success({});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Make a finalized trade secret for the peer
|
|
190
|
+
emitter.emit('trade', ({to: req.from}));
|
|
191
|
+
|
|
192
|
+
// Create a trade secret for the requesting peer and return that
|
|
193
|
+
return finalizeTradeSecret({
|
|
194
|
+
description: args.description,
|
|
195
|
+
expires_at: args.expires_at,
|
|
196
|
+
is_hold: true,
|
|
197
|
+
lnd: args.lnd,
|
|
198
|
+
secret: args.secret,
|
|
199
|
+
to: req.from,
|
|
200
|
+
tokens: args.tokens,
|
|
201
|
+
},
|
|
202
|
+
(err, res) => {
|
|
203
|
+
if (!!err) {
|
|
204
|
+
emitter.emit('failure', err);
|
|
205
|
+
|
|
206
|
+
return failure([503, 'FailedToFinalizeTradeSecret']);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Create a record for the encoded trade
|
|
210
|
+
const record = {type: tradeSecretType, value: res.trade};
|
|
211
|
+
|
|
212
|
+
// Add hold invoice to set of held invoices, only one should settle
|
|
213
|
+
const sub = subscribeToInvoice({id: res.id, lnd: args.lnd});
|
|
214
|
+
|
|
215
|
+
// Add sub to list of holds
|
|
216
|
+
holds.push({sub, id: res.id});
|
|
217
|
+
|
|
218
|
+
// Wait for the invoice to be paid
|
|
219
|
+
sub.on('invoice_updated', updated => {
|
|
220
|
+
// Exit early when the payment is not held yet
|
|
221
|
+
if (!updated.is_held) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Do not accept any more bids
|
|
226
|
+
service.stop({});
|
|
227
|
+
|
|
228
|
+
// Stop listening to invoice events
|
|
229
|
+
holds.forEach(hold => hold.sub.removeAllListeners());
|
|
230
|
+
|
|
231
|
+
// Settle the held invoice with the preimage
|
|
232
|
+
return acceptTrade({
|
|
233
|
+
cancel: holds.filter(n => n.id !== updated.id).map(n => n.id),
|
|
234
|
+
id: args.id,
|
|
235
|
+
lnd: args.lnd,
|
|
236
|
+
secret: res.secret,
|
|
237
|
+
},
|
|
238
|
+
err => {
|
|
239
|
+
if (!!err) {
|
|
240
|
+
return emitter.emit('failure', err);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
emitter.emit('settled', {to: req.from});
|
|
244
|
+
|
|
245
|
+
return emitter.emit('end', {});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Return details about the trade
|
|
250
|
+
return success({records: [record]});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
return emitter;
|
|
255
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const decodeAnchoredTrade = require('./decode_anchored_trade');
|
|
2
|
+
|
|
3
|
+
/** Derive open trade details from an invoice
|
|
4
|
+
|
|
5
|
+
{
|
|
6
|
+
description: <Invoice Description String>
|
|
7
|
+
id: <Invoice Id Hex String>
|
|
8
|
+
expires_at: <Invoice Expiration ISO 8601 Date String>
|
|
9
|
+
[is_canceled]: <Invoice is Canceled Bool>
|
|
10
|
+
is_confirmed: <Invoice is Confirmed Bool>
|
|
11
|
+
[is_push]: <Invoice is Push Payment Bool>
|
|
12
|
+
tokens: <Invoiced Amount Number>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@returns
|
|
16
|
+
{
|
|
17
|
+
[trade]: {
|
|
18
|
+
description: <Trade Description String>
|
|
19
|
+
expires_at: <Trade Expires at ISO 8601 Date String>
|
|
20
|
+
id: <Trade Id Hex String>
|
|
21
|
+
secret: <Secret Payload String>
|
|
22
|
+
tokens: <Tokens Number>
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
*/
|
|
26
|
+
module.exports = args => {
|
|
27
|
+
if (!args.description || !!args.is_canceled || !!args.is_confirmed) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!!args.is_push || !args.tokens) {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const {trade} = decodeAnchoredTrade({encoded: args.description});
|
|
36
|
+
|
|
37
|
+
// Exit early when the invoice doesn't match an anchored trade
|
|
38
|
+
if (!trade) {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
trade: {
|
|
44
|
+
description: trade.description,
|
|
45
|
+
expires_at: args.expires_at,
|
|
46
|
+
id: args.id,
|
|
47
|
+
secret: trade.secret,
|
|
48
|
+
tokens: args.tokens,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
};
|