paid-services 3.20.3 → 4.0.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,19 @@
1
1
  # Versions
2
2
 
3
+ ## Version 4.0.0
4
+
5
+ - `createGroupChannel`: Add method to coordinate a channel group
6
+ - `joinGroupChannel`: Add method to join a channel group
7
+
8
+ ### Breaking Changes
9
+
10
+ - Node.js version 14 or higher is now required
11
+
12
+ ## Version 3.21.0
13
+
14
+ - `manageSwap`: Add recovery for swap requests
15
+ - `manageSwap`: Add support for experimental Lightning Loop MuSig2 Taproot swap
16
+
3
17
  ## Version 3.20.3
4
18
 
5
19
  - `manageGroupJoin`: Allow coordinating pair when funds are below the capacity
@@ -0,0 +1,184 @@
1
+ const asyncAuto = require('async/auto');
2
+ const asyncMap = require('async/map');
3
+ const {getChainBalance} = require('ln-service');
4
+ const {getIdentity} = require('ln-service');
5
+ const {getMethods} = require('ln-service');
6
+ const {getNodeAlias} = require('ln-sync');
7
+ const {returnResult} = require('asyncjs-util');
8
+ const tinysecp = require('tiny-secp256k1');
9
+
10
+ const assembleChannelGroup = require('./assemble_channel_group');
11
+
12
+ const halfOf = n => n / 2;
13
+ const isOdd = n => !!(n % 2);
14
+ const join = arr => arr.join(', ');
15
+ const maxGroupSize = 420;
16
+ const minChannelSize = 2e4;
17
+ const minGroupSize = 2;
18
+ const niceName = ({alias, id}) => `${alias} ${id}`.trim();
19
+ const signPsbtEndpoint = '/walletrpc.WalletKit/SignPsbt';
20
+
21
+ /** Join a channel group
22
+
23
+ {
24
+ capacity: <Channel Capacity Tokens Number>
25
+ count: <Group Member Count Number>
26
+ lnd: <Authenticated LND API Object>
27
+ logger: <Winston Logger Object>
28
+ rate: <Opening Chain Fee Tokens Per VByte Rate Number>
29
+ }
30
+
31
+ @returns via cbk or Promise
32
+ {
33
+ transaction_id: <Transaction Id Hex String>
34
+ }
35
+ */
36
+ module.exports = (args, cbk) => {
37
+ return new Promise((resolve, reject) => {
38
+ return asyncAuto({
39
+ // Import ECPair library
40
+ ecp: async () => (await import('ecpair')).ECPairFactory(tinysecp),
41
+
42
+ // Check arguments
43
+ validate: cbk => {
44
+ if (!args.capacity) {
45
+ return cbk([400, 'ExpectedChannelCapacityToCreateChannelGroup']);
46
+ }
47
+
48
+ if (args.capacity < minChannelSize) {
49
+ return cbk([400, 'ExpectedCapacityGreaterThanMinSizeToCreateGroup']);
50
+ }
51
+
52
+ if (isOdd(args.capacity)) {
53
+ return cbk([400, 'ExpectedEvenChannelCapacityToCreateChannelGroup']);
54
+ }
55
+
56
+ if (!args.count) {
57
+ return cbk([400, 'ExpectedGroupSizeToCreateChannelGroup']);
58
+ }
59
+
60
+ if (args.count < minGroupSize || args.count > maxGroupSize) {
61
+ return cbk([400, 'ExpectedValidGroupSizeToCreateChannelGroup']);
62
+ }
63
+
64
+ if (!args.lnd) {
65
+ return cbk([400, 'ExpectedAuthenticatedLndToCreateChannelGroup']);
66
+ }
67
+
68
+ if (!args.logger) {
69
+ return cbk([400, 'ExpectedWinstonLoggerToCreateChannelGroup']);
70
+ }
71
+
72
+ if (!args.rate) {
73
+ return cbk([400, 'ExpectedOpeningFeeRateToCreateChannelGroup']);
74
+ }
75
+
76
+ return cbk();
77
+ },
78
+
79
+ // Get the on-chain balance to sanity check group creation
80
+ getBalance: ['validate', ({}, cbk) => {
81
+ return getChainBalance({lnd: args.lnd}, cbk);
82
+ }],
83
+
84
+ // Get identity public key
85
+ getIdentity: ['validate', ({}, cbk) => getIdentity({lnd: args.lnd}, cbk)],
86
+
87
+ // Get methods to confim partial signing is supported
88
+ getMethods: ['validate', ({}, cbk) => getMethods({lnd: args.lnd}, cbk)],
89
+
90
+ // Sanity check the on-chain balance is reasonable to create a group
91
+ confirmBalance: ['getBalance', ({getBalance}, cbk) => {
92
+ // A pair group requires half the amount of capital
93
+ const isPair = args.count === minGroupSize;
94
+
95
+ if (!isPair && args.capacity > getBalance.chain_balance) {
96
+ return cbk([400, 'ExpectedCapacityLowerThanCurrentChainBalance']);
97
+ }
98
+
99
+ if (isPair && halfOf(args.capacity) > getBalance.chain_balance) {
100
+ return cbk([400, 'ExpectedCapacityLowerThanCurrentChainBalance']);
101
+ }
102
+
103
+ return cbk();
104
+ }],
105
+
106
+ // Make sure that partially signing a PSBT is a known method
107
+ confirmSigner: ['getMethods', ({getMethods}, cbk) => {
108
+ if (!getMethods.methods.find(n => n.endpoint === signPsbtEndpoint)) {
109
+ return cbk([400, 'ExpectedLndSupportingPartialPsbtSigning']);
110
+ }
111
+
112
+ return cbk();
113
+ }],
114
+
115
+ // Fund and assemble the group
116
+ create: [
117
+ 'ecp',
118
+ 'confirmBalance',
119
+ 'confirmSigner',
120
+ 'getBalance',
121
+ 'getIdentity',
122
+ ({ecp, getIdentity}, cbk) =>
123
+ {
124
+ const coordinate = assembleChannelGroup({
125
+ ecp,
126
+ capacity: args.capacity,
127
+ count: args.count,
128
+ identity: getIdentity.public_key,
129
+ lnd: args.lnd,
130
+ rate: args.rate,
131
+ });
132
+
133
+ const code = getIdentity.public_key + coordinate.id;
134
+
135
+ args.logger.info({group_invite_code: code});
136
+
137
+ // The group must fill up with participants first
138
+ coordinate.events.once('filled', async ({ids}) => {
139
+ const members = ids.filter(n => n !== getIdentity.public_key);
140
+
141
+ const nodes = await asyncMap(members, async id => {
142
+ return niceName(await getNodeAlias({id, lnd: args.lnd}));
143
+ });
144
+
145
+ return args.logger.info({ready: join(nodes)});
146
+ });
147
+
148
+ // Once filled, members will connect with their partners
149
+ coordinate.events.once('connected', () => {
150
+ return args.logger.info({peered: true});
151
+ });
152
+
153
+ // Members will propose pending channels to each other
154
+ coordinate.events.once('proposed', () => {
155
+ return args.logger.info({proposed: true});
156
+ });
157
+
158
+ // Once all pending channels are in place, signatures will be received
159
+ coordinate.events.once('signed', () => {
160
+ return args.logger.info({signed: true});
161
+ });
162
+
163
+ // Finally the open channel tx will be broadcast
164
+ coordinate.events.once('broadcasting', broadcast => {
165
+ return args.logger.info({publishing: broadcast.transaction});
166
+ });
167
+
168
+ // After broadcasting the channels transaction needs to confirm
169
+ coordinate.events.once('broadcast', broadcast => {
170
+ coordinate.events.removeAllListeners();
171
+
172
+ return cbk(null, {transaction_id: broadcast.id});
173
+ });
174
+
175
+ coordinate.events.once('error', err => {
176
+ return cbk([503, 'UnexpectedErrorAssemblingChannelGroup', {err}]);
177
+ });
178
+
179
+ return;
180
+ }],
181
+ },
182
+ returnResult({reject, resolve, of: 'create'}, cbk));
183
+ });
184
+ };
@@ -0,0 +1,135 @@
1
+ const asyncAuto = require('async/auto');
2
+ const asyncRetry = require('async/retry');
3
+ const {connectPeer} = require('ln-sync');
4
+ const {getChainBalance} = require('ln-service');
5
+ const {getNodeAlias} = require('ln-sync');
6
+ const {returnResult} = require('asyncjs-util');
7
+
8
+ const {getGroupDetails} = require('./p2p');
9
+
10
+ const coordinatorFromJoinCode = n => n.slice(0, 66);
11
+ const groupIdFromJoinCode = n => n.slice(66);
12
+ const interval = 500;
13
+ const isCode = n => !!n && n.length === 98;
14
+ const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
15
+ const join = arr => arr.join(' ');
16
+ const niceName = n => `${n.alias} ${n.id}`.trim();
17
+ const times = 2 * 60;
18
+ const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
19
+
20
+ /** Ask to confirm joining a group
21
+
22
+ {
23
+ code: <Group Invite Code String>
24
+ lnd: <Authenticated LND API Object>
25
+ logger: <Winston Logger Object>
26
+ }
27
+
28
+ @returns via cbk or Promise
29
+ {
30
+ capacity: <Channel Capacity Tokens Number>
31
+ coordinator: <Group Coordinator Identity Public Key Hex String>
32
+ count: <Group Members Count>
33
+ id: <Group Id Hex String>
34
+ rate: <Chain Fee Tokens Per VByte Number>
35
+ }
36
+ */
37
+ module.exports = ({code, lnd, logger}, cbk) => {
38
+ return new Promise((resolve, reject) => {
39
+ return asyncAuto({
40
+ // Check arguments
41
+ validate: cbk => {
42
+ if (!isCode(code)) {
43
+ return cbk([400, 'ExpectedChannelGroupInviteCodeToGetJoinDetails']);
44
+ }
45
+
46
+ if (!lnd) {
47
+ return cbk([400, 'ExpectedAuthenticatedLndToGetJoinGroupDetails']);
48
+ }
49
+
50
+ if (!logger) {
51
+ return cbk([400, 'ExpectedWinstonLoggerObjectToGetJoinDetails']);
52
+ }
53
+
54
+ return cbk();
55
+ },
56
+
57
+ // Get the wallet balance to make sure there are enough funds to join
58
+ getBalance: ['validate', ({}, cbk) => getChainBalance({lnd}, cbk)],
59
+
60
+ // Parse the group join code
61
+ group: ['validate', ({}, cbk) => {
62
+ const coordinator = coordinatorFromJoinCode(code);
63
+
64
+ if (!isPublicKey(coordinator)) {
65
+ return cbk([400, 'ExpectedValidGroupJoinCodeToRequestGroupDetails']);
66
+ }
67
+
68
+ const id = groupIdFromJoinCode(code);
69
+
70
+ return cbk(null, {id, coordinator})
71
+ }],
72
+
73
+ // Connect to the coordinator
74
+ connect: ['group', ({group}, cbk) => {
75
+ return asyncRetry({interval, times}, cbk => {
76
+ return connectPeer({lnd, id: group.coordinator}, cbk);
77
+ },
78
+ cbk);
79
+ }],
80
+
81
+ // Get the coordinator node alias to log it out
82
+ getAlias: ['group', ({group}, cbk) => {
83
+ return getNodeAlias({lnd, id: group.coordinator}, cbk);
84
+ }],
85
+
86
+ // Get the group details from the coordinator
87
+ getDetails: ['group', ({group}, cbk) => {
88
+ return getGroupDetails({
89
+ lnd,
90
+ coordinator: group.coordinator,
91
+ id: group.id,
92
+ },
93
+ cbk);
94
+ }],
95
+
96
+ // Log the details of the group being joined
97
+ log: [
98
+ 'getAlias',
99
+ 'getBalance',
100
+ 'getDetails',
101
+ ({getAlias, getBalance, getDetails}, cbk) =>
102
+ {
103
+ const coordinatedBy = `coordinated by ${niceName(getAlias)}`;
104
+ const members = `${getDetails.count} member group`;
105
+ const size = `with ${tokensAsBigUnit(getDetails.capacity)} channels`;
106
+ const rate = `and paying ${getDetails.rate}/vbyte chain fee`;
107
+
108
+ logger.info({joining: join([members, coordinatedBy, size, rate])});
109
+
110
+ // Check to make sure that there are on chain funds for this group
111
+ if (getBalance.chain_balance < getDetails.funding) {
112
+ return cbk([
113
+ 400,
114
+ 'InsufficientChainFundsAvailableToJoinGroup',
115
+ {chain_balance: getBalance.chain_balance},
116
+ ]);
117
+ }
118
+
119
+ return cbk();
120
+ }],
121
+
122
+ // Go ahead with the group join
123
+ join: ['getDetails', 'group', 'log', ({getDetails, group}, cbk) => {
124
+ return cbk(null, {
125
+ capacity: getDetails.capacity,
126
+ coordinator: group.coordinator,
127
+ count: getDetails.count,
128
+ id: group.id,
129
+ rate: getDetails.rate,
130
+ });
131
+ }],
132
+ },
133
+ returnResult({reject, resolve, of: 'join'}, cbk));
134
+ });
135
+ };
package/groups/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ const createGroupChannel = require('./create_group_channel');
2
+ const joinGroupChannel = require('./join_group_channel');
1
3
  const manageGroupJoin = require('./manage_group_join');
2
4
 
3
- module.exports = {manageGroupJoin};
5
+ module.exports = {createGroupChannel, joinGroupChannel, manageGroupJoin};
@@ -0,0 +1,119 @@
1
+ const asyncAuto = require('async/auto');
2
+ const asyncMap = require('async/map');
3
+ const {getMethods} = require('ln-service');
4
+ const {getNodeAlias} = require('ln-sync');
5
+ const {returnResult} = require('asyncjs-util');
6
+
7
+ const joinChannelGroup = require('./join_channel_group');
8
+ const getJoinDetails = require('./get_join_details');
9
+
10
+ const formatNodes = arr => arr.join(', ');
11
+ const isCode = n => !!n && n.length === 98;
12
+ const niceName = ({alias, id}) => `${alias} ${id}`.trim();
13
+ const signPsbtEndpoint = '/walletrpc.WalletKit/SignPsbt';
14
+
15
+ /** Join a channel group
16
+
17
+ {
18
+ code: <Group Invite Code String>
19
+ lnd: <Authenticated LND API Object>
20
+ logger: <Winston Logger Object>
21
+ max_rate: <Max Opening Chain Fee Tokens Per VByte Fee Rate Number>
22
+ }
23
+
24
+ @returns via cbk or Promise
25
+ {
26
+ transaction_id: <Channel Funding Transaction Id Hex String>
27
+ }
28
+ */
29
+ module.exports = (args, cbk) => {
30
+ return new Promise((resolve, reject) => {
31
+ return asyncAuto({
32
+ // Check arguments
33
+ validate: cbk => {
34
+ if (!isCode(args.code)) {
35
+ return cbk([400, 'ExpectedValidJoinCodeToJoinGroup']);
36
+ }
37
+
38
+ if (!args.lnd) {
39
+ return cbk([400, 'ExpectedAuthenticatedLndToJoinGroup']);
40
+ }
41
+
42
+ if (!args.logger) {
43
+ return cbk([400, 'ExpectedWinstonLoggerToJoinGroupp']);
44
+ }
45
+
46
+ if (!args.max_rate) {
47
+ return cbk([400, 'ExpectedMaxOpeningFeeRateToJoinGroup']);
48
+ }
49
+
50
+ return cbk();
51
+ },
52
+
53
+ // Get methods to confim partial signing is supported
54
+ getMethods: ['validate', ({}, cbk) => getMethods({lnd: args.lnd}, cbk)],
55
+
56
+ // Make sure that partially signing a PSBT is valid
57
+ confirmSigner: ['getMethods', ({getMethods}, cbk) => {
58
+ if (!getMethods.methods.find(n => n.endpoint === signPsbtEndpoint)) {
59
+ return cbk([400, 'ExpectedLndSupportingPartialPsbtSigningToJoin']);
60
+ }
61
+
62
+ return cbk();
63
+ }],
64
+
65
+ // Decode the group invite code and get group details
66
+ getJoinDetails: ['confirmSigner', ({}, cbk) => {
67
+ return getJoinDetails({
68
+ code: args.code,
69
+ lnd: args.lnd,
70
+ logger: args.logger,
71
+ },
72
+ cbk);
73
+ }],
74
+
75
+ // Join the channel group
76
+ join: [
77
+ 'confirmSigner',
78
+ 'getMethods',
79
+ 'getJoinDetails',
80
+ ({getJoinDetails}, cbk) =>
81
+ {
82
+ if (getJoinDetails.rate > args.max_rate) {
83
+ return cbk([400, 'ExpectedHigherMaxFeeRateToJoinGroup']);
84
+ }
85
+
86
+ args.logger.info({waiting_for_other_members: true});
87
+
88
+ const join = joinChannelGroup({
89
+ capacity: getJoinDetails.capacity,
90
+ coordinator: getJoinDetails.coordinator,
91
+ count: getJoinDetails.count,
92
+ id: getJoinDetails.id,
93
+ lnd: args.lnd,
94
+ rate: getJoinDetails.rate,
95
+ });
96
+
97
+ join.once('end', ({id}) => cbk(null, {transaction_id: id}));
98
+ join.once('error', err => cbk(err));
99
+
100
+ // After the group is filled the members are matched and peer up
101
+ join.once('peering', async ({inbound, outbound}) => {
102
+ const nodes = await asyncMap([inbound, outbound], async id => {
103
+ return niceName(await getNodeAlias({id, lnd: args.lnd}));
104
+ });
105
+
106
+ return args.logger.info({peering_with: formatNodes(nodes)});
107
+ });
108
+
109
+ // Once everyone is peered then the channel tx is made
110
+ join.once('publishing', ({refund, signed}) => {
111
+ return args.logger.info({refund, signed});
112
+ });
113
+
114
+ return;
115
+ }],
116
+ },
117
+ returnResult({reject, resolve, of: 'join'}, cbk));
118
+ });
119
+ };
package/index.js CHANGED
@@ -2,11 +2,13 @@ const {balancedOpenRequest} = require('./balanced');
2
2
  const {changeChannelCapacity} = require('./capacity');
3
3
  const {confirmServiceUse} = require('./client');
4
4
  const {createAnchoredTrade} = require('./trades');
5
+ const {createGroupChannel} = require('./groups');
5
6
  const {decodeTrade} = require('./trades');
6
7
  const {encodeTrade} = require('./trades');
7
8
  const {getAnchoredTrade} = require('./trades');
8
9
  const {getServiceSchema} = require('./client');
9
10
  const {getServicesList} = require('./client');
11
+ const {joinGroupChannel} = require('./groups');
10
12
  const {makePeerRequest} = require('./p2p');
11
13
  const {makeServiceRequest} = require('./client');
12
14
  const {manageGroupJoin} = require('./groups');
@@ -24,11 +26,13 @@ module.exports = {
24
26
  changeChannelCapacity,
25
27
  confirmServiceUse,
26
28
  createAnchoredTrade,
29
+ createGroupChannel,
27
30
  decodeTrade,
28
31
  encodeTrade,
29
32
  getAnchoredTrade,
30
33
  getServiceSchema,
31
34
  getServicesList,
35
+ joinGroupChannel,
32
36
  makePeerRequest,
33
37
  makeServiceRequest,
34
38
  manageGroupJoin,
package/package.json CHANGED
@@ -13,24 +13,24 @@
13
13
  "bech32": "2.0.0",
14
14
  "bolt01": "1.2.5",
15
15
  "bolt07": "1.8.2",
16
- "ecpair": "2.0.1",
17
- "goldengate": "11.2.3",
18
- "invoices": "2.1.0",
19
- "ln-service": "53.20.0",
20
- "ln-sync": "3.13.1",
16
+ "ecpair": "2.1.0",
17
+ "goldengate": "11.4.0",
18
+ "invoices": "2.2.0",
19
+ "ln-service": "54.2.0",
20
+ "ln-sync": "3.14.0",
21
21
  "psbt": "2.7.1",
22
- "p2tr": "1.3.1",
22
+ "p2tr": "1.3.2",
23
23
  "tiny-secp256k1": "2.2.1"
24
24
  },
25
25
  "description": "Lightning Paid Services library",
26
26
  "devDependencies": {
27
27
  "@alexbosworth/tap": "15.0.11",
28
- "ln-docker-daemons": "2.3.4",
29
- "mock-lnd": "1.4.3",
28
+ "ln-docker-daemons": "3.1.0",
29
+ "mock-lnd": "1.4.4",
30
30
  "secp256k1": "4.0.3"
31
31
  },
32
32
  "engines": {
33
- "node": ">=12.20"
33
+ "node": ">=14"
34
34
  },
35
35
  "keywords": [
36
36
  "lightning",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "scripts": {
47
47
  "integration-tests": "tap -j 2 --branches=1 --functions=1 --lines=1 --statements=1 -t 180 test/integration/*.js",
48
- "test": "tap --branches=1 --functions=1 --lines=1 --statements=1 -t 60 test/actions/*.js test/balanced/*.js test/capacity/*.js test/client/*.js test/config/*.js test/p2p/*.js test/records/*.js test/respond/*.js test/server/*.js test/server/*.js test/services/*.js test/trades/*.js"
48
+ "test": "tap --branches=1 --functions=1 --lines=1 --statements=1 -t 60 test/actions/*.js test/balanced/*.js test/capacity/*.js test/client/*.js test/config/*.js test/p2p/*.js test/records/*.js test/respond/*.js test/server/*.js test/swaps/*.js test/services/*.js test/trades/*.js"
49
49
  },
50
- "version": "3.20.3"
50
+ "version": "4.0.0"
51
51
  }