balanceofsatoshis 25.2.3 → 25.3.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,12 @@
1
1
  # Versions
2
2
 
3
+ ## 25.3.0
4
+
5
+ - `closed`: Avoid using external chain data lookups to derive resolution info
6
+ - `closed`: Include anchor and HTLC resolution fees in closing fees paid
7
+ - `closed`: Pro-rate the fees of sweeps that are shared with other channels
8
+ - `closed`: Count the value of an anchor output that was not swept as a fee
9
+
3
10
  ## 25.2.3
4
11
 
5
12
  - `telegram`: Indicate when a cooperative close was initiated by the peer
package/bos CHANGED
@@ -490,8 +490,7 @@ prog
490
490
 
491
491
  // Determine the outcomes of channel closings
492
492
  .command('closed', 'Get the status of a channel closings')
493
- .help('Channel closes with chain-transaction derived resolution details')
494
- .help('Privacy note: this requests tx data from third parties')
493
+ .help('Channel closes with resolution details of the closing outputs')
495
494
  .option('--limit [limit]', 'Limit of closings to get', INT, 20)
496
495
  .option('--no-color', 'Mute all colors')
497
496
  .option('--node <node_name>', 'Get channel closes from saved node')
@@ -501,7 +500,6 @@ prog
501
500
  return chain.getChannelCloses({
502
501
  limit: options.limit,
503
502
  lnd: (await lnd.authenticatedLnd({logger, node: options.node})).lnd,
504
- request: commands.simpleRequest,
505
503
  },
506
504
  responses.returnObject({logger, reject, resolve}));
507
505
  } catch (err) {
@@ -0,0 +1,23 @@
1
+ /** Describe the outcome of a closed channel anchor output
2
+
3
+ {
4
+ [is_confirmed]: <Anchor Output Spend Is Confirmed Bool>
5
+ [is_pending]: <Anchor Output Spend Is Pending Confirmation Bool>
6
+ }
7
+
8
+ @returns
9
+ {
10
+ outcome: <Anchor Outcome String>
11
+ }
12
+ */
13
+ module.exports = args => {
14
+ if (!!args.is_confirmed) {
15
+ return {outcome: 'swept'};
16
+ }
17
+
18
+ if (!!args.is_pending) {
19
+ return {outcome: 'pending'};
20
+ }
21
+
22
+ return {outcome: 'unclaimed'};
23
+ };
@@ -0,0 +1,86 @@
1
+ const {componentsOfTransaction} = require('@alexbosworth/blockchain');
2
+
3
+ const anchorOutputScript = require('./anchor_output_script');
4
+ const fundingKeys = require('./funding_keys');
5
+
6
+ /** Find our anchor output in a close transaction
7
+
8
+ {
9
+ [anchor_vout]: <LND Reported Anchor Output Index Number>
10
+ [close_balance_spent_by]: <Close Balance Spent By Id Hex String>
11
+ [close_balance_vout]: <Close Balance Output Index Number>
12
+ close_payments: [{
13
+ transaction_id: <HTLC Transaction Id Hex String>
14
+ transaction_vout: <HTLC Transaction Output Index Number>
15
+ }]
16
+ close_transaction_id: <Close Transaction Id Hex String>
17
+ [is_cooperative_close]: <Channel Was Cooperatively Closed Bool>
18
+ [transaction]: <Raw Close Transaction Hex String>
19
+ }
20
+
21
+ @returns
22
+ {
23
+ [vout]: <Anchor Output Index Number>
24
+ }
25
+ */
26
+ module.exports = args => {
27
+ // Exit early when LND reported the anchor output
28
+ if (args.anchor_vout !== undefined) {
29
+ return {vout: args.anchor_vout};
30
+ }
31
+
32
+ // Exit early when there are no anchors in a cooperative close
33
+ if (!!args.is_cooperative_close || !args.transaction) {
34
+ return {};
35
+ }
36
+
37
+ const {inputs, outputs} = componentsOfTransaction({
38
+ transaction: args.transaction,
39
+ });
40
+
41
+ const [fundingSpend] = inputs;
42
+
43
+ const {keys} = fundingKeys({witness: fundingSpend.witness});
44
+
45
+ // Exit early when the funding keys that anchors are derived from are unknown
46
+ if (!keys) {
47
+ return {};
48
+ }
49
+
50
+ const anchorScripts = keys.map(key => anchorOutputScript({key}).script);
51
+
52
+ // Outputs that resolved as the balance or as HTLCs are not anchors
53
+ const htlcVouts = args.close_payments
54
+ .filter(n => n.transaction_id === args.close_transaction_id)
55
+ .map(n => n.transaction_vout);
56
+
57
+ const balanceVout = args.close_balance_vout;
58
+
59
+ const knownVouts = []
60
+ .concat(balanceVout !== undefined ? [balanceVout] : [])
61
+ .concat(htlcVouts);
62
+
63
+ const anchors = outputs
64
+ .map(({script, tokens}, vout) => ({script, tokens, vout}))
65
+ .filter(n => !knownVouts.includes(n.vout))
66
+ .filter(n => anchorScripts.includes(n.script));
67
+
68
+ // Exit early when there are no anchor outputs
69
+ if (!anchors.length) {
70
+ return {};
71
+ }
72
+
73
+ const [anchor, otherAnchor] = anchors;
74
+
75
+ // Exit early when both sides have an anchor, one of them is ours
76
+ if (!!otherAnchor) {
77
+ return {vout: anchor.vout};
78
+ }
79
+
80
+ // Exit early when a single anchor is ours as we had outputs to protect
81
+ if (!!args.close_balance_spent_by || !!args.close_payments.length) {
82
+ return {vout: anchor.vout};
83
+ }
84
+
85
+ return {};
86
+ };
@@ -0,0 +1,43 @@
1
+ const {createHash} = require('node:crypto');
2
+
3
+ const {p2wshOutputScript} = require('@alexbosworth/blockchain');
4
+ const {scriptElementsAsScript} = require('@alexbosworth/blockchain');
5
+
6
+ const bufferAsHex = buffer => buffer.toString('hex');
7
+ const hexAsBuffer = hex => Buffer.from(hex, 'hex');
8
+ const OP_16 = 96;
9
+ const OP_CHECKSEQUENCEVERIFY = 178;
10
+ const OP_CHECKSIG = 172;
11
+ const OP_ENDIF = 104;
12
+ const OP_IFDUP = 115;
13
+ const OP_NOTIF = 100;
14
+ const sha256 = preimage => createHash('sha256').update(preimage).digest();
15
+
16
+ /** Derive the anchor output script for a funding public key
17
+
18
+ {
19
+ key: <Funding Public Key Hex String>
20
+ }
21
+
22
+ @returns
23
+ {
24
+ script: <Anchor P2WSH Output Script Hex String>
25
+ }
26
+ */
27
+ module.exports = ({key}) => {
28
+ const {script} = scriptElementsAsScript({
29
+ elements: [
30
+ hexAsBuffer(key),
31
+ OP_CHECKSIG,
32
+ OP_IFDUP,
33
+ OP_NOTIF,
34
+ OP_16,
35
+ OP_CHECKSEQUENCEVERIFY,
36
+ OP_ENDIF,
37
+ ],
38
+ });
39
+
40
+ const hash = sha256(hexAsBuffer(script));
41
+
42
+ return {script: bufferAsHex(p2wshOutputScript({hash}).script)};
43
+ };
@@ -0,0 +1,48 @@
1
+ const {componentsOfTransaction} = require('@alexbosworth/blockchain');
2
+
3
+ const simpleCloseSequence = 0xfffffffd;
4
+
5
+ /** Determine which side paid the fee of a channel close transaction
6
+
7
+ {
8
+ [is_cooperative_close]: <Channel Was Cooperatively Closed Bool>
9
+ [is_partner_closed]: <Channel Was Closed By Peer Bool>
10
+ [is_partner_initiated]: <Channel Was Opened By Peer Bool>
11
+ [transaction]: <Raw Close Transaction Hex String>
12
+ }
13
+
14
+ @returns
15
+ {
16
+ [is_peer_paying]: <Peer Paid The Close Transaction Fee Bool>
17
+ }
18
+ */
19
+ module.exports = args => {
20
+ const isPeerClosed = args.is_partner_closed;
21
+ const isPeerOpened = args.is_partner_initiated;
22
+
23
+ // Exit early when the channel was force closed, the opener paid for that
24
+ if (!args.is_cooperative_close) {
25
+ return {is_peer_paying: isPeerOpened};
26
+ }
27
+
28
+ const isOpenerClosing = isPeerOpened === isPeerClosed;
29
+
30
+ // Exit early when the opener also closed, they paid in either protocol
31
+ if (isPeerOpened !== undefined && isOpenerClosing) {
32
+ return {is_peer_paying: isPeerOpened};
33
+ }
34
+
35
+ // Exit early when the close protocol is unknown without the close tx
36
+ if (!args.transaction) {
37
+ return {};
38
+ }
39
+
40
+ const {inputs} = componentsOfTransaction({transaction: args.transaction});
41
+
42
+ const [fundingSpend] = inputs;
43
+
44
+ // The closer pays for a simple close, otherwise the opener pays
45
+ const isSimpleClose = fundingSpend.sequence === simpleCloseSequence;
46
+
47
+ return {is_peer_paying: isSimpleClose ? isPeerClosed : isPeerOpened};
48
+ };
@@ -0,0 +1,114 @@
1
+ const {componentsOfTransaction} = require('@alexbosworth/blockchain');
2
+
3
+ const anchorOutcome = require('./anchor_outcome');
4
+ const linkPaymentStages = require('./link_payment_stages');
5
+ const paymentOutcome = require('./payment_outcome');
6
+ const {sortBy} = require('./../arrays');
7
+
8
+ const sortByVout = arr => sortBy({array: arr, attribute: 'vout'}).sorted;
9
+
10
+ /** Derive the resolutions of closed channel outputs from LND close details
11
+
12
+ Output values for balance and anchor outputs are only known when the close
13
+ transaction is present in the wallet transactions
14
+
15
+ {
16
+ [anchor_is_confirmed]: <Anchor Output Spend Is Confirmed Bool>
17
+ [anchor_is_pending]: <Anchor Output Spend Is Pending Confirmation Bool>
18
+ [anchor_spent_by]: <Anchor Sweep Transaction Id Hex String>
19
+ [anchor_vout]: <Anchor Output Index Number>
20
+ [close_balance_spent_by]: <Balance Sweep Transaction Id Hex String>
21
+ [close_balance_vout]: <Balance Output Index Number>
22
+ close_payments: [{
23
+ is_outgoing: <Payment Is Outgoing Bool>
24
+ is_paid: <Payment Is Claimed With Preimage Bool>
25
+ is_pending: <Payment Resolution Is Pending Bool>
26
+ is_refunded: <Payment Timed Out And Went Back To Payer Bool>
27
+ [spent_by]: <Close Transaction Spent By Transaction Id Hex String>
28
+ tokens: <Associated Tokens Number>
29
+ transaction_id: <Transaction Id Hex String>
30
+ transaction_vout: <Transaction Output Index Number>
31
+ }]
32
+ close_transaction_id: <Close Transaction Id Hex String>
33
+ transactions: [{
34
+ id: <Transaction Id Hex String>
35
+ [transaction]: <Raw Transaction Hex String>
36
+ }]
37
+ }
38
+
39
+ @returns
40
+ {
41
+ resolutions: [{
42
+ [transaction_id]: <Latest Resolving Transaction Id Hex String>
43
+ type: <Resolution Type String>
44
+ [value]: <Output Value Tokens Number>
45
+ }]
46
+ }
47
+ */
48
+ module.exports = args => {
49
+ // Output values are only known when the close tx is in the wallet
50
+ const [closeTx] = args.transactions
51
+ .filter(n => !!n.transaction)
52
+ .filter(n => n.id === args.close_transaction_id)
53
+ .map(({transaction}) => componentsOfTransaction({transaction}));
54
+
55
+ const outputs = !!closeTx ? closeTx.outputs : [];
56
+
57
+ const tokensAt = vout => !outputs[vout] ? undefined : outputs[vout].tokens;
58
+
59
+ const anchorResolution = anchorOutcome({
60
+ is_confirmed: args.anchor_is_confirmed,
61
+ is_pending: args.anchor_is_pending,
62
+ });
63
+
64
+ const anchor = {
65
+ transaction_id: args.anchor_spent_by,
66
+ type: `anchor_${anchorResolution.outcome}`,
67
+ value: tokensAt(args.anchor_vout),
68
+ vout: args.anchor_vout,
69
+ };
70
+
71
+ const balance = {
72
+ transaction_id: args.close_balance_spent_by,
73
+ type: 'balance_swept',
74
+ value: tokensAt(args.close_balance_vout),
75
+ vout: args.close_balance_vout,
76
+ };
77
+
78
+ // HTLCs resolved over two stages are linked to get their final outcome
79
+ const {payments} = linkPaymentStages({
80
+ close_payments: args.close_payments,
81
+ transactions: args.transactions,
82
+ });
83
+
84
+ const htlcs = payments.map(payment => {
85
+ const direction = !!payment.is_outgoing ? 'outgoing' : 'incoming';
86
+
87
+ const {outcome} = paymentOutcome({
88
+ is_paid: payment.is_paid,
89
+ is_pending: payment.is_pending,
90
+ is_refunded: payment.is_refunded,
91
+ });
92
+
93
+ return {
94
+ transaction_id: payment.spent_by,
95
+ type: `${direction}_htlc_${outcome}`,
96
+ value: payment.tokens,
97
+ vout: payment.transaction_vout,
98
+ };
99
+ });
100
+
101
+ // Anchor and balance resolutions are only present when LND reported them
102
+ const resolutions = []
103
+ .concat(args.anchor_vout !== undefined ? [anchor] : [])
104
+ .concat(!!args.close_balance_spent_by ? [balance] : [])
105
+ .concat(htlcs);
106
+
107
+ return {
108
+ resolutions: sortByVout(resolutions).map(resolution => ({
109
+ transaction_id: resolution.transaction_id || undefined,
110
+ type: resolution.type,
111
+ value: resolution.value,
112
+ })),
113
+ };
114
+ };
@@ -1,51 +1,158 @@
1
1
  const {componentsOfTransaction} = require('@alexbosworth/blockchain');
2
2
 
3
+ const closeFeePayer = require('./close_fee_payer');
4
+ const linkPaymentStages = require('./link_payment_stages');
5
+ const resolutionFee = require('./resolution_fee');
6
+
7
+ const outputsOf = tx => componentsOfTransaction({transaction: tx}).outputs;
3
8
  const sumOf = arr => arr.reduce((sum, n) => sum + n, Number());
9
+ const uniq = arr => Array.from(new Set(arr));
4
10
 
5
11
  /** Calculate channel closing fees paid
6
12
 
13
+ Fees are only returned when every fee that may have been paid is known.
14
+
15
+ Fees include the close transaction fee when it was paid by this side, the
16
+ fees of our own transactions that resolve the outputs of the close, and the
17
+ value of an anchor output that was never swept and so is lost.
18
+
19
+ When a resolving transaction also spends the outputs of other channels, the
20
+ fee is pro-rated by the weight of the inputs spending this channel's outputs.
21
+
7
22
  {
23
+ [anchor_is_confirmed]: <Anchor Sweep Is Confirmed Bool>
24
+ [anchor_is_pending]: <Anchor Sweep Is Pending Confirmation Bool>
25
+ [anchor_spent_by]: <Anchor Sweep Transaction Id Hex String>
26
+ [anchor_vout]: <Anchor Output Index Number>
8
27
  [capacity]: <Channel Capacity Tokens Number>
9
28
  [close_balance_spent_by]: <Close Balance Spent By Id Hex String>
29
+ close_payments: [{
30
+ is_outgoing: <Payment Is Outgoing Bool>
31
+ is_paid: <Payment Is Claimed With Preimage Bool>
32
+ is_pending: <Payment Resolution Is Pending Bool>
33
+ is_refunded: <Payment Timed Out And Went Back To Payer Bool>
34
+ [spent_by]: <Spent By Transaction Id Hex String>
35
+ tokens: <Associated Tokens Number>
36
+ transaction_id: <Transaction Id Hex String>
37
+ transaction_vout: <Transaction Output Index Number>
38
+ }]
10
39
  close_transaction_id: <Close Transaction Id Hex String>
11
- is_partner_initiated: <Channel is Initiated By Peer Bool>
40
+ [is_cooperative_close]: <Channel Was Cooperatively Closed Bool>
41
+ [is_partner_closed]: <Channel Was Closed By Peer Bool>
42
+ [is_partner_initiated]: <Channel is Initiated By Peer Bool>
43
+ other_contract_outputs: [{
44
+ id: <Other Channel Contract Transaction Id Hex String>
45
+ [vout]: <Other Channel Contract Transaction Output Index Number>
46
+ }]
12
47
  transactions: [{
13
48
  [fee]: <Paid Transaction Fee Tokens Number>
49
+ id: <Transaction Id Hex String>
14
50
  [transaction]: <Raw Transaction Hex String>
15
51
  }]
16
52
  }
17
53
 
18
54
  @returns
19
55
  {
20
- fees: <Closing Fees Paid Number>
56
+ [fees]: <Known Closing Fees Paid Tokens Number>
21
57
  }
22
58
  */
23
59
  module.exports = args => {
24
- const closer = args.transactions
60
+ const [closeTx] = args.transactions
25
61
  .filter(n => !!n.transaction)
26
- .find(n => n.id === args.close_transaction_id);
62
+ .filter(n => n.id === args.close_transaction_id);
63
+
64
+ const closeTransaction = !!closeTx ? closeTx.transaction : undefined;
65
+
66
+ const payer = closeFeePayer({
67
+ is_cooperative_close: args.is_cooperative_close,
68
+ is_partner_closed: args.is_partner_closed,
69
+ is_partner_initiated: args.is_partner_initiated,
70
+ transaction: closeTransaction,
71
+ });
72
+
73
+ // Exit early when it is not known which side paid the close tx fee
74
+ if (payer.is_peer_paying === undefined) {
75
+ return {};
76
+ }
77
+
78
+ // Payments resolved over two stages are linked to find their final status
79
+ const {payments} = linkPaymentStages({
80
+ close_payments: args.close_payments,
81
+ transactions: args.transactions,
82
+ });
83
+
84
+ // Exit early when a resolution is still pending, fees are not yet final
85
+ if (!!args.anchor_is_pending || !!payments.find(n => !!n.is_pending)) {
86
+ return {};
87
+ }
88
+
89
+ // Close outputs are spent in resolving transactions that may be our own
90
+ const ownSpends = args.close_payments
91
+ .filter(n => !!n.spent_by)
92
+ .filter(payment => {
93
+ // Exit early when the spend is a first stage second level transaction
94
+ if (!!payment.is_pending) {
95
+ return true;
96
+ }
97
+
98
+ return !!payment.is_outgoing ? !!payment.is_refunded : !!payment.is_paid;
99
+ })
100
+ .map(n => n.spent_by);
101
+
102
+ const resolvingIds = uniq([]
103
+ .concat(!!args.close_balance_spent_by ? [args.close_balance_spent_by] : [])
104
+ .concat(!!args.anchor_spent_by ? [args.anchor_spent_by] : [])
105
+ .concat(ownSpends));
27
106
 
28
- const balanceSpend = args.transactions
29
- .find(n => n.id === args.close_balance_spent_by);
107
+ // Close tx outputs and specific second level tx outputs are this channel's
108
+ const contractOutputs = []
109
+ .concat({id: args.close_transaction_id})
110
+ .concat(args.close_payments.map(payment => ({
111
+ id: payment.transaction_id,
112
+ vout: payment.transaction_vout,
113
+ })));
30
114
 
31
- // Exit early when the partner was responsible for close fees
32
- if (args.is_partner_initiated === true) {
33
- return {fees: !!balanceSpend ? balanceSpend.fee : Number()};
115
+ const resolvingFees = resolvingIds.map(id => {
116
+ return resolutionFee({
117
+ id,
118
+ contract_outputs: contractOutputs,
119
+ other_contract_outputs: args.other_contract_outputs,
120
+ transactions: args.transactions,
121
+ }).fee;
122
+ });
123
+
124
+ // Exit early when the fee of a resolving transaction is not known
125
+ if (resolvingFees.includes(undefined)) {
126
+ return {};
34
127
  }
35
128
 
36
- // Exit early when the closing transaction isn't known
37
- if (!args.capacity || !closer || !closer.transaction) {
129
+ const closeOutputs = !closeTransaction ? [] : outputsOf(closeTransaction);
130
+
131
+ const anchor = closeOutputs[args.anchor_vout];
132
+
133
+ // An anchor output that was never swept is lost and counted as a fee
134
+ const isAnchorLost = args.anchor_vout !== undefined && !args.anchor_spent_by;
135
+
136
+ // Exit early when the value of a lost anchor output is not known
137
+ if (!!isAnchorLost && !anchor) {
38
138
  return {};
39
139
  }
40
140
 
41
- // Calculate the value of the outputs in the tx that spends the capacity
42
- const {outputs} = componentsOfTransaction({transaction: closer.transaction});
141
+ const lostTokens = !isAnchorLost ? Number() : anchor.tokens;
43
142
 
44
- const outValue = sumOf(outputs.map(n => n.tokens));
143
+ const resolvingTokens = sumOf(resolvingFees) + lostTokens;
144
+
145
+ // Exit early when the peer paid the close tx fee
146
+ if (!!payer.is_peer_paying) {
147
+ return {fees: resolvingTokens};
148
+ }
149
+
150
+ // Exit early when the close tx fee was paid but the close tx is not known
151
+ if (!args.capacity || !closeTransaction) {
152
+ return {};
153
+ }
45
154
 
46
- const fees = []
47
- .concat(args.capacity - outValue)
48
- .concat(!!balanceSpend ? balanceSpend.fee : Number());
155
+ const closeFee = args.capacity - sumOf(closeOutputs.map(n => n.tokens));
49
156
 
50
- return {fees: sumOf(fees)};
157
+ return {fees: closeFee + resolvingTokens};
51
158
  };
@@ -0,0 +1,30 @@
1
+ /** Derive the contract outputs of a channel from its close details
2
+
3
+ The outputs of the close transaction all belong to the channel, while HTLC
4
+ outputs are referred to specifically as a second level transaction may be
5
+ shared with other channels.
6
+
7
+ {
8
+ [close_transaction_id]: <Close Transaction Id Hex String>
9
+ payments: [{
10
+ transaction_id: <HTLC Transaction Id Hex String>
11
+ transaction_vout: <HTLC Transaction Output Index Number>
12
+ }]
13
+ }
14
+
15
+ @returns
16
+ [{
17
+ id: <Contract Transaction Id Hex String>
18
+ [vout]: <Contract Transaction Output Index Number>
19
+ }]
20
+ */
21
+ module.exports = args => {
22
+ const id = args.close_transaction_id;
23
+
24
+ const closeOutputs = !id ? [] : [{id}];
25
+
26
+ return closeOutputs.concat(args.payments.map(payment => ({
27
+ id: payment.transaction_id,
28
+ vout: payment.transaction_vout,
29
+ })));
30
+ };
@@ -0,0 +1,58 @@
1
+ const splitFee = require('./split_fee');
2
+
3
+ const minimumOutputCount = 1;
4
+
5
+ /** Count the split outputs that selected inputs can fund to reach a count
6
+
7
+ Every counted utxo that is spent has to be replaced by a new utxo
8
+
9
+ {
10
+ counted: <Spending Counted UTXOs Count Number>
11
+ inputs: <Spending Inputs Count Number>
12
+ needed: <Needed Additional UTXOs Count Number>
13
+ rate: <Fee Tokens Per Virtual Byte Number>
14
+ size: <Split Output Tokens Number>
15
+ tokens: <Spending Inputs Tokens Number>
16
+ vbytes: <Spending Inputs Virtual Bytes Number>
17
+ }
18
+
19
+ @returns
20
+ {
21
+ [outputs]: <Split Outputs Count Number>
22
+ }
23
+ */
24
+ module.exports = args => {
25
+ const outputs = args.needed + args.counted;
26
+
27
+ // When the change is large enough it can be one of the new utxos
28
+ const withChange = outputs - minimumOutputCount;
29
+
30
+ const changeFee = splitFee({
31
+ has_change: true,
32
+ inputs: args.inputs,
33
+ outputs: withChange,
34
+ rate: args.rate,
35
+ vbytes: args.vbytes,
36
+ });
37
+
38
+ const change = args.tokens - (withChange * args.size) - changeFee;
39
+
40
+ // Exit early when the change will be large enough to count as a new utxo
41
+ if (withChange >= minimumOutputCount && change >= args.size) {
42
+ return {outputs: withChange};
43
+ }
44
+
45
+ const fee = splitFee({
46
+ outputs,
47
+ inputs: args.inputs,
48
+ rate: args.rate,
49
+ vbytes: args.vbytes,
50
+ });
51
+
52
+ // Exit early when the inputs cannot pay for all of the outputs
53
+ if (args.tokens < (outputs * args.size) + fee) {
54
+ return {};
55
+ }
56
+
57
+ return {outputs};
58
+ };
@@ -0,0 +1,59 @@
1
+ const {scriptAsScriptElements} = require('@alexbosworth/blockchain');
2
+
3
+ const bufferAsHex = buffer => buffer.toString('hex');
4
+ const fundingScriptElementsCount = 5;
5
+ const {isBuffer} = Buffer;
6
+ const OP_2 = 82;
7
+ const OP_CHECKMULTISIG = 174;
8
+ const publicKeyByteLength = 33;
9
+
10
+ /** Derive the funding public keys from the witness of the funding output spend
11
+
12
+ A close transaction spends the funding output with a 2 of 2 multisig witness
13
+ script that commits to both funding public keys.
14
+
15
+ {
16
+ [witness]: [<Witness Stack Element Hex String>]
17
+ }
18
+
19
+ @returns
20
+ {
21
+ [keys]: [<Funding Public Key Hex String>]
22
+ }
23
+ */
24
+ module.exports = ({witness}) => {
25
+ // Exit early when there is no witness script to look at
26
+ if (!witness || !witness.length) {
27
+ return {};
28
+ }
29
+
30
+ const [script] = [].concat(witness).reverse();
31
+
32
+ // Witness elements are always hex, a malformed script has no elements
33
+ const {elements} = scriptAsScriptElements({script});
34
+
35
+ // Exit early when the script is not shaped like a 2 of 2 multisig
36
+ if (!elements || elements.length !== fundingScriptElementsCount) {
37
+ return {};
38
+ }
39
+
40
+ const [op2, key1, key2, op2Again, opCheckMultisig] = elements;
41
+
42
+ // Exit early when the script is not a 2 of 2 multisig
43
+ if (op2 !== OP_2 || op2Again !== OP_2) {
44
+ return {};
45
+ }
46
+
47
+ if (opCheckMultisig !== OP_CHECKMULTISIG) {
48
+ return {};
49
+ }
50
+
51
+ const keys = [key1, key2];
52
+
53
+ // Exit early when the keys are not public keys
54
+ if (!!keys.find(n => !isBuffer(n) || n.length !== publicKeyByteLength)) {
55
+ return {};
56
+ }
57
+
58
+ return {keys: keys.map(bufferAsHex)};
59
+ };