paid-services 3.16.3 → 3.17.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/groups/ask_for_group_details.js +160 -0
  3. package/groups/ask_to_confirm_group_join.js +140 -0
  4. package/groups/assemble/assemble_unsigned_psbt.js +169 -0
  5. package/groups/assemble/coordinate_group.js +383 -0
  6. package/groups/assemble/index.js +3 -0
  7. package/groups/assemble_channel_group.js +178 -0
  8. package/groups/attach_to_channel_group.js +80 -0
  9. package/groups/coordinate_channel_group.js +122 -0
  10. package/groups/funding/confirm_incoming_channel.js +108 -0
  11. package/groups/funding/index.js +9 -0
  12. package/groups/funding/propose_group_channel.js +167 -0
  13. package/groups/funding/sign_and_fund_group_channel.js +188 -0
  14. package/groups/index.js +3 -0
  15. package/groups/join_channel_group.js +193 -0
  16. package/groups/manage_group_join.js +96 -0
  17. package/groups/members/index.js +3 -0
  18. package/groups/members/partners_from_members.js +26 -0
  19. package/groups/messages/decode_connected_records.js +56 -0
  20. package/groups/messages/decode_group_details.js +99 -0
  21. package/groups/messages/decode_partners_records.js +57 -0
  22. package/groups/messages/decode_pending_proposal.js +167 -0
  23. package/groups/messages/decode_signed_funding.js +42 -0
  24. package/groups/messages/decode_signed_records.js +56 -0
  25. package/groups/messages/decode_unsigned_funding.js +43 -0
  26. package/groups/messages/encode_connected_records.js +26 -0
  27. package/groups/messages/encode_group_details.js +32 -0
  28. package/groups/messages/encode_partners_records.js +22 -0
  29. package/groups/messages/encode_pending_proposal.js +91 -0
  30. package/groups/messages/encode_signed_records.js +26 -0
  31. package/groups/messages/encode_unsigned_funding.js +19 -0
  32. package/groups/messages/index.js +29 -0
  33. package/groups/p2p/find_group_partners.js +122 -0
  34. package/groups/p2p/get_group_details.js +81 -0
  35. package/groups/p2p/index.js +15 -0
  36. package/groups/p2p/peer_with_partners.js +57 -0
  37. package/groups/p2p/register_group_connected.js +107 -0
  38. package/groups/p2p/register_pending_open.js +218 -0
  39. package/groups/p2p/register_signed_open.js +118 -0
  40. package/index.js +2 -0
  41. package/package.json +3 -3
  42. package/service_types.json +11 -1
  43. package/test/integration/test_group.js +174 -0
  44. package/trades/buy_channel.js +1 -1
  45. package/trades/buy_preimage.js +1 -1
  46. package/trades/create_channel_sale.js +6 -6
  47. package/trades/create_trade.js +7 -7
  48. package/trades/find_trade.js +1 -1
  49. package/trades/manage_trade.js +4 -4
  50. package/trades/manage_trades.js +3 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Versions
2
2
 
3
+ ## Version 3.17.2
4
+
5
+ - `manageGroupJoin`: Add method to coordinate or join a channels group
6
+
3
7
  ## Version 3.16.3
4
8
 
5
9
  - `manageSwap`: Add external funding, conf target setting, keysend pushes
@@ -0,0 +1,160 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {getChainBalance} = require('ln-service');
3
+ const {getChainFeeRate} = require('ln-service');
4
+ const {returnResult} = require('asyncjs-util');
5
+
6
+ const defaultChannelCapacity = 5e6;
7
+ const defaultGroupSize = 3;
8
+ const {floor} = Math;
9
+ const isNumber = n => !isNaN(n);
10
+ const isOdd = n => !!(n % 2);
11
+ const maxChannelSize = 21e14;
12
+ const minChannelSize = 2e4;
13
+ const maxGroupSize = 420;
14
+ const minGroupSize = 3;
15
+ const {round} = Math;
16
+
17
+ /** Ask for new group details to create a group
18
+
19
+ {
20
+ ask: <Ask Function>
21
+ lnd: <Authenticated LND API Object>
22
+ }
23
+
24
+ @returns via cbk or Promise
25
+ {
26
+ capacity: <Channel Capacity Tokens Number>
27
+ count: <Group Members Number>
28
+ rate: <Chain Fee Tokens Per VByte Number>
29
+ }
30
+ */
31
+ module.exports = ({ask, lnd}, cbk) => {
32
+ return new Promise((resolve, reject) => {
33
+ return asyncAuto({
34
+ // Check arguments
35
+ validate: cbk => {
36
+ if (!ask) {
37
+ return cbk([400, 'ExpectedAskFunctionToAskForGroupDetails']);
38
+ }
39
+
40
+ if (!lnd) {
41
+ return cbk([400, 'ExpectedAuthenticatedLndToAskForGroupDetails']);
42
+ }
43
+
44
+ return cbk();
45
+ },
46
+
47
+ // Get the wallet balance to make sure there are enough funds to join
48
+ getBalance: ['validate', ({}, cbk) => getChainBalance({lnd}, cbk)],
49
+
50
+ // Ask for how big the channels should be
51
+ askForCapacity: ['getBalance', ({getBalance}, cbk) => {
52
+ return ask({
53
+ default: defaultChannelCapacity,
54
+ name: 'capacity',
55
+ message: 'Channel capacity?',
56
+ validate: input => {
57
+ if (!input || !isNumber(input)) {
58
+ return false;
59
+ }
60
+
61
+ if (round(Number(input)) !== Number(input)) {
62
+ return false;
63
+ }
64
+
65
+ if (Number(input) > getBalance.chain_balance) {
66
+ return `Current chain balance is ${getBalance.chain_balance}`;
67
+ }
68
+
69
+ if (Number(input) < minChannelSize) {
70
+ return `Minimum channel size is ${minChannelSize}`;
71
+ }
72
+
73
+ if (isOdd(Number(input))) {
74
+ return 'Channel capacity must be even';
75
+ }
76
+
77
+ return true;
78
+ },
79
+ },
80
+ ({capacity}) => cbk(null, Number(capacity)));
81
+ }],
82
+
83
+ // Get the chain fee rate to use for a default in the chain fee query
84
+ getFeeRate: ['validate', ({}, cbk) => getChainFeeRate({lnd}, cbk)],
85
+
86
+ // Ask for how many group members there should be
87
+ askForCount: ['askForCapacity', ({askForCapacity}, cbk) => {
88
+ return ask({
89
+ default: defaultGroupSize,
90
+ name: 'size',
91
+ message: 'Total number of group members?',
92
+ validate: input => {
93
+ if (!input || !isNumber(input)) {
94
+ return false;
95
+ }
96
+
97
+ // Do not allow fractional members
98
+ if (round(Number(input)) !== Number(input)) {
99
+ return false;
100
+ }
101
+
102
+ // Do not allow too many members
103
+ if (Number(input) > maxGroupSize) {
104
+ return `The maximum group size is ${maxGroupSize}`;
105
+ }
106
+
107
+ // Do not allow too few members
108
+ if (Number(input) < minGroupSize) {
109
+ return `The minimum group size is ${minGroupSize}`;
110
+ }
111
+
112
+ return true;
113
+ },
114
+ },
115
+ ({size}) => cbk(null, size));
116
+ }],
117
+
118
+ // Ask for a chain fee rate
119
+ askForFeeRate: [
120
+ 'askForCount',
121
+ 'getFeeRate',
122
+ ({askForCount, getFeeRate}, cbk) =>
123
+ {
124
+ return ask({
125
+ default: floor(getFeeRate.tokens_per_vbyte),
126
+ name: 'rate',
127
+ message: 'Chain fee per vbyte?',
128
+ validate: input => {
129
+ if (!input || !isNumber(input) || !Number(input)) {
130
+ return false;
131
+ }
132
+
133
+ // Do not allow fractional fee rates
134
+ if (round(Number(input)) !== Number(input)) {
135
+ return 'Fractional fee rate setting is not supported';
136
+ }
137
+
138
+ return true;
139
+ },
140
+ },
141
+ ({rate}) => cbk(null, rate));
142
+ }],
143
+
144
+ // Final group details
145
+ group: [
146
+ 'askForCapacity',
147
+ 'askForCount',
148
+ 'askForFeeRate',
149
+ ({askForCapacity, askForCount, askForFeeRate}, cbk) =>
150
+ {
151
+ return cbk(null, {
152
+ capacity: askForCapacity,
153
+ count: askForCount,
154
+ rate: askForFeeRate,
155
+ });
156
+ }],
157
+ },
158
+ returnResult({reject, resolve, of: 'group'}, cbk));
159
+ });
160
+ };
@@ -0,0 +1,140 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {connectPeer} = require('ln-sync');
3
+ const {getChainBalance} = require('ln-service');
4
+ const {getNodeAlias} = require('ln-sync');
5
+ const {returnResult} = require('asyncjs-util');
6
+
7
+ const {getGroupDetails} = require('./p2p');
8
+
9
+ const coordinatorFromJoinCode = n => n.slice(0, 66);
10
+ const groupIdFromJoinCode = n => n.slice(66);
11
+ const isCode = n => !!n && n.length === 98;
12
+ const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
13
+ const niceName = n => `${n.alias} ${n.id}`.trim();
14
+ const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
15
+
16
+ /** Ask to confirm joining a group
17
+
18
+ {
19
+ ask: <Ask Function>
20
+ lnd: <Authenticated LND API Object>
21
+ }
22
+
23
+ @returns via cbk or Promise
24
+ {
25
+ capacity: <Channel Capacity Tokens Number>
26
+ coordinator: <Group Coordinator Identity Public Key Hex String>
27
+ count: <Group Members Count>
28
+ id: <Group Id Hex String>
29
+ rate: <Chain Fee Tokens Per VByte Number>
30
+ }
31
+ */
32
+ module.exports = ({ask, lnd}, cbk) => {
33
+ return new Promise((resolve, reject) => {
34
+ return asyncAuto({
35
+ // Check arguments
36
+ validate: cbk => {
37
+ if (!ask) {
38
+ return cbk([400, 'ExpectedAskFunctionToConfirmGroupJoin']);
39
+ }
40
+
41
+ if (!lnd) {
42
+ return cbk([400, 'ExpectedAuthenticatedLndToConfirmGroupJoin']);
43
+ }
44
+
45
+ return cbk();
46
+ },
47
+
48
+ // Ask for the group entry code
49
+ askForCode: ['validate', ({}, cbk) => {
50
+ return ask({
51
+ name: 'code',
52
+ message: 'Enter a group join code to join a group',
53
+ validate: input => !!isCode(input),
54
+ },
55
+ ({code}) => cbk(null, code));
56
+ }],
57
+
58
+ // Get the wallet balance to make sure there are enough funds to join
59
+ getBalance: ['validate', ({}, cbk) => getChainBalance({lnd}, cbk)],
60
+
61
+ // Parse the group join code
62
+ group: ['askForCode', ({askForCode}, cbk) => {
63
+ const coordinator = coordinatorFromJoinCode(askForCode);
64
+
65
+ if (!isPublicKey(coordinator)) {
66
+ return cbk([400, 'ExpectedValidGroupJoinCodeToRequestGroupDetails']);
67
+ }
68
+
69
+ const id = groupIdFromJoinCode(askForCode);
70
+
71
+ return cbk(null, {id, coordinator})
72
+ }],
73
+
74
+ // Connect to the coordinator
75
+ connect: ['group', ({group}, cbk) => {
76
+ return connectPeer({lnd, id: group.coordinator}, cbk);
77
+ }],
78
+
79
+ // Get the coordinator node alias
80
+ getAlias: ['group', ({group}, cbk) => {
81
+ return getNodeAlias({lnd, id: group.coordinator}, cbk);
82
+ }],
83
+
84
+ // Get the group details from the coordinator
85
+ getDetails: ['group', ({group}, cbk) => {
86
+ return getGroupDetails({
87
+ lnd,
88
+ coordinator: group.coordinator,
89
+ id: group.id,
90
+ },
91
+ cbk);
92
+ }],
93
+
94
+ // Confirm the group join
95
+ ok: [
96
+ 'getAlias',
97
+ 'getBalance',
98
+ 'getDetails',
99
+ ({getAlias, getBalance, getDetails}, cbk) =>
100
+ {
101
+ // Check to make sure that there are on chain funds for this group
102
+ if (getBalance.chain_balance < getDetails.capacity) {
103
+ return cbk([
104
+ 400,
105
+ 'InsufficientChainFundsAvailableToJoinGroup',
106
+ {channel_capacity: tokensAsBigUnit(getDetails.capacity)},
107
+ ]);
108
+ }
109
+
110
+ const coordinatedBy = `coordinated by ${niceName(getAlias)}`;
111
+ const members = `${getDetails.count} member group`;
112
+ const size = `with ${tokensAsBigUnit(getDetails.capacity)} channels`;
113
+ const rate = `${getDetails.rate}/vbyte chain fee`;
114
+
115
+ return ask({
116
+ name: 'join',
117
+ message: `Join ${members} ${size} at ${rate}, ${coordinatedBy}?`,
118
+ type: 'confirm',
119
+ },
120
+ ({join}) => cbk(null, join));
121
+ }],
122
+
123
+ // Go ahead with the group join
124
+ join: ['getDetails', 'group', 'ok', ({getDetails, group, ok}, cbk) => {
125
+ if (!ok) {
126
+ return cbk([400, 'CanceledGroupChannelJoin']);
127
+ }
128
+
129
+ return cbk(null, {
130
+ capacity: getDetails.capacity,
131
+ coordinator: group.coordinator,
132
+ count: getDetails.count,
133
+ id: group.id,
134
+ rate: getDetails.rate,
135
+ });
136
+ }],
137
+ },
138
+ returnResult({reject, resolve, of: 'join'}, cbk));
139
+ });
140
+ };
@@ -0,0 +1,169 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {returnResult} = require('asyncjs-util');
3
+ const {createPsbt} = require('psbt');
4
+ const {extendPsbt} = require('psbt');
5
+ const tinysecp = require('tiny-secp256k1');
6
+ const {Transaction} = require('bitcoinjs-lib');
7
+
8
+ const {ceil} = Math;
9
+ const dummyEcdsaSignature = Buffer.alloc(74);
10
+ const dummyPublicKey = Buffer.alloc(33);
11
+ const dummySchnorrSignature = Buffer.alloc(64);
12
+ const dustValue = 330;
13
+ const flatten = arr => [].concat(...arr);
14
+ const hexAsBuffer = hex => Buffer.from(hex, 'hex');
15
+ const {isArray} = Array;
16
+ const isP2tr = n => n.startsWith('5120') && n.length === 68;
17
+ const isP2wpkh = n => n.startsWith('0014') && n.length === 44;
18
+ const {random} = Math;
19
+ const sumOf = arr => arr.reduce((sum, n) => sum + n, 0);
20
+
21
+ /** Assemble group channel unsigned PSBT
22
+
23
+ {
24
+ capacity: <Channel Capacity Tokens Number>
25
+ proposed: [{
26
+ [change]: <Change Output Hex String>
27
+ funding: <Funding Output Hex String>
28
+ utxos: [{
29
+ [non_witness_utxo]: <Spending Transaction Hex String>
30
+ transaction_id: <Transaction Id Hex String>
31
+ transaction_vout: <Transaction Output Index Number>
32
+ witness_utxo: {
33
+ script_pub: <Witness Output Script Hex String>
34
+ tokens: <Tokens Number>
35
+ }
36
+ }]
37
+ }]
38
+ rate: <Fee Rate Number>
39
+ }
40
+
41
+ @returns via cbk or Promise
42
+ {
43
+ psbt: <Unsigned Funding Transaction PSBT Hex String>
44
+ }
45
+ */
46
+ module.exports = ({capacity, proposed, rate}, cbk) => {
47
+ return new Promise((resolve, reject) => {
48
+ return asyncAuto({
49
+ // Import ECPair library
50
+ ecp: async () => (await import('ecpair')).ECPairFactory(tinysecp),
51
+
52
+ // Check arguments
53
+ validate: cbk => {
54
+ if (!capacity) {
55
+ return cbk([400, 'ExpectedCapacityToAssembleUnsignedPsbt']);
56
+ }
57
+
58
+ if (!isArray(proposed)) {
59
+ return cbk([400, 'ExpectedChannelProposalsToAssembleUnsignedPsbt']);
60
+ }
61
+
62
+ if (!rate) {
63
+ return cbk([400, 'ExpectedChainFeeRateToAssembleUnsignedPsbt']);
64
+ }
65
+
66
+ return cbk();
67
+ },
68
+
69
+ // Derive funding transaction outputs
70
+ outputs: ['validate', ({}, cbk) => {
71
+ // Create a dummy tx to use for looking at vsize contributions
72
+ const tx = new Transaction();
73
+
74
+ // A tx has some base wrapper vbytes to pay for
75
+ const startSize = tx.virtualSize();
76
+
77
+ // Members should split the cost of the wrapper bytes
78
+ const wrapperShare = ceil(startSize / proposed.length);
79
+
80
+ const outputs = proposed.map(member => {
81
+ const tare = tx.virtualSize();
82
+
83
+ const inputsOffset = tx.ins.length;
84
+
85
+ [member.change, member.funding].filter(n => !!n).forEach(out => {
86
+ return tx.addOutput(hexAsBuffer(out), capacity);
87
+ });
88
+
89
+ member.utxos.forEach(utxo => {
90
+ return tx.addInput(
91
+ hexAsBuffer(utxo.transaction_id),
92
+ utxo.transaction_vout
93
+ );
94
+ });
95
+
96
+ member.utxos.forEach((utxo, i) => {
97
+ // Set a dummy signature stack on the input
98
+ if (isP2wpkh(utxo.witness_utxo.script_pub)) {
99
+ return tx.setWitness(
100
+ i + inputsOffset,
101
+ [dummyPublicKey, dummyEcdsaSignature]
102
+ );
103
+ }
104
+
105
+ if (isP2tr(utxo.witness_utxo.script_pub)) {
106
+ return tx.setWitness(i + inputsOffset, [dummySchnorrSignature]);
107
+ }
108
+
109
+ throw new Error('UnsupportedOutputType');
110
+ });
111
+
112
+ const vbytes = tx.virtualSize() - tare + wrapperShare;
113
+
114
+ const funded = sumOf(member.utxos.map(n => n.witness_utxo.tokens));
115
+
116
+ return [
117
+ {
118
+ script: member.funding,
119
+ tokens: capacity,
120
+ },
121
+ {
122
+ script: member.change,
123
+ tokens: funded - capacity - (vbytes * rate),
124
+ },
125
+ ];
126
+ });
127
+
128
+ // Collect outputs and shuffle them
129
+ const finalOutputs = flatten(outputs).filter(n => !!n.script)
130
+ .map(value => ({value, sort: random()}))
131
+ .sort((a, b) => a.sort - b.sort)
132
+ .map(({value}) => value);
133
+
134
+ return cbk(null, finalOutputs);
135
+ }],
136
+
137
+ // Assemble the funding for the group channel
138
+ funding: ['ecp', 'outputs', ({ecp, outputs}, cbk) => {
139
+ // Put together all inputs funding the transaction, shuffle inputs
140
+ const utxos = flatten(proposed.map(n => n.utxos))
141
+ .map(utxo => ({
142
+ id: utxo.transaction_id,
143
+ non_witness_utxo: utxo.non_witness_utxo,
144
+ vout: utxo.transaction_vout,
145
+ witness_utxo: utxo.witness_utxo,
146
+ }))
147
+ .map(value => ({value, sort: random()}))
148
+ .sort((a, b) => a.sort - b.sort)
149
+ .map(({value}) => value);
150
+
151
+ // Setup a baseline PSBT with the inputs and outputs
152
+ const fundingBase = createPsbt({outputs, utxos});
153
+
154
+ // Extend the base PSBT with the UTXO metadata
155
+ const extended = extendPsbt({
156
+ ecp,
157
+ inputs: utxos.map(utxo => ({
158
+ non_witness_utxo: utxo.non_witness_utxo,
159
+ witness_utxo: utxo.witness_utxo,
160
+ })),
161
+ psbt: fundingBase.psbt,
162
+ });
163
+
164
+ return cbk(null, {psbt: extended.psbt});
165
+ }],
166
+ },
167
+ returnResult({reject, resolve, of: 'funding'}, cbk));
168
+ });
169
+ };