@exodus/bitcoin-api 2.18.4 → 2.20.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
@@ -3,6 +3,24 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [2.20.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@2.19.0...@exodus/bitcoin-api@2.20.0) (2024-07-09)
7
+
8
+
9
+ ### Features
10
+
11
+ * **BTC:** add changeAddressType to createAsset config ([#2772](https://github.com/ExodusMovement/assets/issues/2772)) ([d8e38dd](https://github.com/ExodusMovement/assets/commit/d8e38dddfbe59b3e8ff9fc2432049bb512a91c22))
12
+
13
+
14
+
15
+ ## [2.19.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@2.18.4...@exodus/bitcoin-api@2.19.0) (2024-07-05)
16
+
17
+
18
+ ### Features
19
+
20
+ * **BTC:** add taprootInputWitnessSize for tx size estimation ([#2737](https://github.com/ExodusMovement/assets/issues/2737)) ([13f727a](https://github.com/ExodusMovement/assets/commit/13f727a836f9fc620628abb6a94a49c1a6774ca5))
21
+
22
+
23
+
6
24
  ## [2.18.4](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@2.18.3...@exodus/bitcoin-api@2.18.4) (2024-06-20)
7
25
 
8
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/bitcoin-api",
3
- "version": "2.18.4",
3
+ "version": "2.20.0",
4
4
  "description": "Exodus bitcoin-api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -66,5 +66,5 @@
66
66
  "type": "git",
67
67
  "url": "git+https://github.com/ExodusMovement/assets.git"
68
68
  },
69
- "gitHead": "b7cc185dce729a04f48833e29c606eeb7b24407b"
69
+ "gitHead": "0dc141fc6a249ac051a5ca591d46cbc81096a4f3"
70
70
  }
@@ -18,6 +18,8 @@ const _canBumpTx = ({
18
18
  getFeeEstimator,
19
19
  allowUnconfirmedRbfEnabledUtxos,
20
20
  utxosDescendingOrder,
21
+ taprootInputWitnessSize,
22
+ changeAddressType,
21
23
  }) => {
22
24
  assert(asset, 'asset must be provided')
23
25
  assert(tx, 'tx must be provided')
@@ -93,6 +95,8 @@ const _canBumpTx = ({
93
95
  allowUnconfirmedRbfEnabledUtxos,
94
96
  unconfirmedTxAncestor,
95
97
  utxosDescendingOrder,
98
+ taprootInputWitnessSize,
99
+ changeAddressType,
96
100
  })
97
101
  if (replaceTx) return { bumpType: BumpType.RBF, bumpFee: fee.sub(replaceTx.feeAmount) }
98
102
  }
@@ -101,12 +105,14 @@ const _canBumpTx = ({
101
105
  asset,
102
106
  usableUtxos,
103
107
  feeRate,
104
- receiveAddress: 'P2WPKH',
108
+ receiveAddress: changeAddressType,
105
109
  getFeeEstimator,
106
110
  mustSpendUtxos: changeUtxos,
107
111
  allowUnconfirmedRbfEnabledUtxos,
108
112
  unconfirmedTxAncestor,
109
113
  utxosDescendingOrder,
114
+ taprootInputWitnessSize,
115
+ changeAddressType,
110
116
  })
111
117
 
112
118
  return fee ? { bumpType: BumpType.CPFP, bumpFee: fee } : { errorMessage: 'insufficient funds' }
@@ -48,6 +48,11 @@ const scriptPubKeyLengths = {
48
48
  [P2TR]: 34,
49
49
  }
50
50
 
51
+ // Only the 64 byte Schnorr signature is present for Taproot Key-Path spend
52
+ const signatureLength = 64
53
+ const taprootInputKeyPathWitnessSize =
54
+ varuint.encodingLength(1) + varuint.encodingLength(signatureLength) + signatureLength
55
+
51
56
  // bitcoin and bitcoin-like:
52
57
  // 10 = version: 4, locktime: 4, inputs and outputs count: 1
53
58
  // 148 = txId: 32, vout: 4, count: 1, script: 107 (max), sequence: 4
@@ -58,7 +63,12 @@ export const getSizeFactory = ({ defaultOutputType, addressApi }) => {
58
63
 
59
64
  const scriptClassifier = scriptClassifierFactory({ addressApi })
60
65
 
61
- return (asset, inputs, outputs, { compressed = true } = {}) => {
66
+ return (
67
+ asset,
68
+ inputs,
69
+ outputs,
70
+ { compressed = true, taprootInputWitnessSize = taprootInputKeyPathWitnessSize } = {}
71
+ ) => {
62
72
  if (inputs instanceof UtxoCollection) {
63
73
  inputs = [...inputs].map((utxo) => utxo.script || null)
64
74
  }
@@ -136,14 +146,7 @@ export const getSizeFactory = ({ defaultOutputType, addressApi }) => {
136
146
  }
137
147
 
138
148
  if ([P2TR].includes(utxoScriptType)) {
139
- // Only the 64 byte Schnorr signature is present for Taproot Key-Path spend
140
- const signatureLength = 64
141
- return (
142
- t +
143
- varuint.encodingLength(1) +
144
- varuint.encodingLength(signatureLength) +
145
- signatureLength
146
- )
149
+ return t + taprootInputWitnessSize
147
150
  }
148
151
 
149
152
  // Non-witness inputs get a placeholder zero byte
@@ -36,11 +36,12 @@ export default function createDefaultFeeEstimator(getSize) {
36
36
  inputs = options.inputs,
37
37
  outputs = options.outputs,
38
38
  unconfirmedTxAncestor = options.unconfirmedTxAncestor,
39
+ taprootInputWitnessSize,
39
40
  } = {}) => {
40
41
  const extraFee = getExtraFee({ asset, inputs, feePerKB, unconfirmedTxAncestor })
41
42
  // Yes, it's suppose to be '1000' and not '1024'
42
43
  // https://bitcoin.stackexchange.com/questions/24000/a-fee-is-added-per-kilobyte-of-data-that-means-1000-bytes-or-1024
43
- const size = getSize(asset, inputs, outputs, options)
44
+ const size = getSize(asset, inputs, outputs, { ...options, taprootInputWitnessSize })
44
45
  const feeRaw = Math.ceil((feePerKB.toBaseNumber() * size) / 1000)
45
46
  return asset.currency.baseUnit(feeRaw + extraFee)
46
47
  }
@@ -15,13 +15,20 @@ export class GetFeeResolver {
15
15
  #getFeeEstimator
16
16
  #allowUnconfirmedRbfEnabledUtxos
17
17
  #utxosDescendingOrder
18
+ #changeAddressType
18
19
 
19
- constructor({ getFeeEstimator, allowUnconfirmedRbfEnabledUtxos, utxosDescendingOrder }) {
20
+ constructor({
21
+ getFeeEstimator,
22
+ allowUnconfirmedRbfEnabledUtxos,
23
+ utxosDescendingOrder,
24
+ changeAddressType,
25
+ }) {
20
26
  assert(getFeeEstimator, 'getFeeEstimator must be provided')
21
27
  this.#getFeeEstimator = (asset, { feePerKB, ...options }) =>
22
28
  getFeeEstimator(asset, feePerKB, options)
23
29
  this.#allowUnconfirmedRbfEnabledUtxos = allowUnconfirmedRbfEnabledUtxos
24
30
  this.#utxosDescendingOrder = utxosDescendingOrder
31
+ this.#changeAddressType = changeAddressType
25
32
  }
26
33
 
27
34
  getFee = ({
@@ -35,6 +42,7 @@ export class GetFeeResolver {
35
42
  nft, // sending one nft
36
43
  brc20, // sending multiple inscriptions ids (as in sending multiple transfer ordinals)
37
44
  receiveAddress,
45
+ taprootInputWitnessSize,
38
46
  }) => {
39
47
  if (nft) {
40
48
  assert(!amount, 'amount must not be provided when nft is provided!!!')
@@ -60,11 +68,21 @@ export class GetFeeResolver {
60
68
  customFee,
61
69
  isSendAll,
62
70
  inscriptionIds,
71
+ taprootInputWitnessSize,
63
72
  })
64
73
  return { fee: resolvedFee, extraFee }
65
74
  }
66
75
 
67
- getAvailableBalance = ({ asset, accountState, txSet, feeData, amount, customFee, isSendAll }) => {
76
+ getAvailableBalance = ({
77
+ asset,
78
+ accountState,
79
+ txSet,
80
+ feeData,
81
+ amount,
82
+ customFee,
83
+ isSendAll,
84
+ taprootInputWitnessSize,
85
+ }) => {
68
86
  return this.#getUtxosData({
69
87
  asset,
70
88
  accountState,
@@ -73,16 +91,18 @@ export class GetFeeResolver {
73
91
  customFee,
74
92
  isSendAll,
75
93
  amount,
94
+ taprootInputWitnessSize,
76
95
  }).availableBalance
77
96
  }
78
97
 
79
- getSpendableBalance = ({ asset, accountState, txSet, feeData }) => {
98
+ getSpendableBalance = ({ asset, accountState, txSet, feeData, taprootInputWitnessSize }) => {
80
99
  return this.#getUtxosData({
81
100
  asset,
82
101
  accountState,
83
102
  txSet,
84
103
  feeData,
85
104
  isSendAll: true,
105
+ taprootInputWitnessSize,
86
106
  }).spendableBalance
87
107
  }
88
108
 
@@ -96,6 +116,7 @@ export class GetFeeResolver {
96
116
  customFee,
97
117
  isSendAll,
98
118
  inscriptionIds,
119
+ taprootInputWitnessSize,
99
120
  }) => {
100
121
  assert(asset, 'asset must be provided')
101
122
  assert(feeData, 'feeData must be provided')
@@ -136,10 +157,12 @@ export class GetFeeResolver {
136
157
  allowUnconfirmedRbfEnabledUtxos: this.#allowUnconfirmedRbfEnabledUtxos,
137
158
  unconfirmedTxAncestor,
138
159
  utxosDescendingOrder: this.#utxosDescendingOrder,
160
+ taprootInputWitnessSize,
161
+ changeAddressType: this.#changeAddressType,
139
162
  })
140
163
  }
141
164
 
142
- canBumpTx = ({ asset, tx, txSet, accountState, feeData }) => {
165
+ canBumpTx = ({ asset, tx, txSet, accountState, feeData, taprootInputWitnessSize }) => {
143
166
  return canBumpTx({
144
167
  asset,
145
168
  tx,
@@ -149,6 +172,8 @@ export class GetFeeResolver {
149
172
  getFeeEstimator: this.#getFeeEstimator,
150
173
  allowUnconfirmedRbfEnabledUtxos: this.#allowUnconfirmedRbfEnabledUtxos,
151
174
  utxosDescendingOrder: this.#utxosDescendingOrder,
175
+ taprootInputWitnessSize,
176
+ changeAddressType: this.#changeAddressType,
152
177
  })
153
178
  }
154
179
  }
@@ -35,6 +35,8 @@ export const selectUtxos = ({
35
35
  unconfirmedTxAncestor,
36
36
  inscriptionIds, // for each inscription transfer, we need to calculate one more input and one more output
37
37
  transferOrdinalsUtxos, // to calculate the size of the input
38
+ taprootInputWitnessSize,
39
+ changeAddressType = 'P2PKH',
38
40
  }) => {
39
41
  const resolvedReceiveAddresses = getBestReceiveAddresses({
40
42
  asset,
@@ -53,10 +55,6 @@ export const selectUtxos = ({
53
55
  assert(usableUtxos, 'usableUtxos is required')
54
56
  assert(getFeeEstimator, 'getFeeEstimator is required')
55
57
 
56
- const changeAddressType = ['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(asset.name)
57
- ? 'P2WPKH'
58
- : 'P2PKH'
59
-
60
58
  const feeEstimator = getFeeEstimator(asset, { feePerKB: feeRate, unconfirmedTxAncestor })
61
59
  const { currency } = asset
62
60
  if (!amount) amount = currency.ZERO
@@ -100,9 +98,13 @@ export const selectUtxos = ({
100
98
 
101
99
  if (isSendAll) {
102
100
  additionalUtxos = UtxoCollection.fromArray(confirmedUtxosArray, { currency })
103
- fee = replaceFeeEstimator({ inputs: inputs.union(additionalUtxos), outputs })
101
+ fee = replaceFeeEstimator({
102
+ inputs: inputs.union(additionalUtxos),
103
+ outputs,
104
+ taprootInputWitnessSize,
105
+ })
104
106
  } else {
105
- fee = replaceFeeEstimator({ inputs, outputs })
107
+ fee = replaceFeeEstimator({ inputs, outputs, taprootInputWitnessSize })
106
108
  additionalUtxos = UtxoCollection.createEmpty({ currency })
107
109
  while (replaceTxAmount.add(additionalUtxos.value).lt(amount.add(fee))) {
108
110
  if (confirmedUtxosArray.length === 0) {
@@ -116,6 +118,7 @@ export const selectUtxos = ({
116
118
  fee = replaceFeeEstimator({
117
119
  inputs: inputs.union(additionalUtxos),
118
120
  outputs: noChangeOutputs,
121
+ taprootInputWitnessSize,
119
122
  })
120
123
  }
121
124
 
@@ -127,6 +130,7 @@ export const selectUtxos = ({
127
130
  fee = replaceFeeEstimator({
128
131
  inputs: inputs.union(additionalUtxos),
129
132
  outputs,
133
+ taprootInputWitnessSize,
130
134
  })
131
135
  }
132
136
  }
@@ -136,6 +140,7 @@ export const selectUtxos = ({
136
140
  const chainFee = feeEstimator({
137
141
  inputs: changeUtxos.union(additionalUtxos),
138
142
  outputs: chainOutputs,
143
+ taprootInputWitnessSize,
139
144
  })
140
145
  // If batching is same or more expensive than chaining, don't replace
141
146
  // Unless we are accelerating a changeless tx, then we must replace because it can't be chained
@@ -173,7 +178,11 @@ export const selectUtxos = ({
173
178
 
174
179
  if (isSendAll) {
175
180
  const selectedUtxos = UtxoCollection.fromArray(utxosArray, { currency })
176
- const fee = feeEstimator({ inputs: selectedUtxos, outputs: receiveAddresses })
181
+ const fee = feeEstimator({
182
+ inputs: selectedUtxos,
183
+ outputs: receiveAddresses,
184
+ taprootInputWitnessSize,
185
+ })
177
186
  if (selectedUtxos.value.lt(amount.add(fee))) {
178
187
  return { fee }
179
188
  }
@@ -208,19 +217,23 @@ export const selectUtxos = ({
208
217
  ? [changeAddressType]
209
218
  : [...receiveAddresses, changeAddressType]
210
219
 
211
- let fee = feeEstimator({ inputs: selectedUtxos, outputs })
220
+ let fee = feeEstimator({ inputs: selectedUtxos, outputs, taprootInputWitnessSize })
212
221
 
213
222
  while (selectedUtxos.value.lt(amount.add(fee))) {
214
223
  // We ran out of UTXOs, give up now
215
224
  if (remainingUtxosArray.length === 0) {
216
225
  // Try fee with no change
217
- fee = feeEstimator({ inputs: selectedUtxos, outputs: receiveAddresses })
226
+ fee = feeEstimator({
227
+ inputs: selectedUtxos,
228
+ outputs: receiveAddresses,
229
+ taprootInputWitnessSize,
230
+ })
218
231
  break
219
232
  }
220
233
 
221
234
  // Add a new UTXO and recalculate the fee
222
235
  selectedUtxos = selectedUtxos.addUtxo(remainingUtxosArray.shift())
223
- fee = feeEstimator({ inputs: selectedUtxos, outputs })
236
+ fee = feeEstimator({ inputs: selectedUtxos, outputs, taprootInputWitnessSize })
224
237
  }
225
238
 
226
239
  if (selectedUtxos.value.lt(amount.add(fee))) {
@@ -246,6 +259,8 @@ export const getUtxosData = ({
246
259
  transferOrdinalsUtxos,
247
260
  unconfirmedTxAncestor,
248
261
  utxosDescendingOrder,
262
+ taprootInputWitnessSize,
263
+ changeAddressType,
249
264
  }) => {
250
265
  const { selectedUtxos, replaceTx, fee } = selectUtxos({
251
266
  asset,
@@ -263,6 +278,8 @@ export const getUtxosData = ({
263
278
  inscriptionIds,
264
279
  transferOrdinalsUtxos,
265
280
  utxosDescendingOrder,
281
+ taprootInputWitnessSize,
282
+ changeAddressType,
266
283
  })
267
284
 
268
285
  const resolvedFee = replaceTx ? fee.sub(replaceTx.feeAmount) : fee
@@ -223,6 +223,7 @@ export const getPrepareSendTransaction =
223
223
  utxosDescendingOrder,
224
224
  rbfEnabled: providedRbfEnabled,
225
225
  assetClientInterface,
226
+ changeAddressType,
226
227
  }) =>
227
228
  async ({ asset: maybeToken, walletAccount, address, amount: tokenAmount, options }) => {
228
229
  const {
@@ -236,12 +237,14 @@ export const getPrepareSendTransaction =
236
237
  isExchange,
237
238
  isBip70,
238
239
  isRbfAllowed,
240
+ taprootInputWitnessSize,
239
241
  } = options
240
242
 
241
243
  const asset = maybeToken.baseAsset
242
244
  const assetName = asset.name
243
245
  const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
244
246
  const feeData = await assetClientInterface.getFeeConfig({ assetName })
247
+ feeData.feePerKB = feePerKB ?? feeData.feePerKB
245
248
  const insightClient = asset.baseAsset.insightClient
246
249
 
247
250
  const blockHeight = providedBlockHeight || (await getBlockHeight({ assetName, insightClient }))
@@ -352,6 +355,8 @@ export const getPrepareSendTransaction =
352
355
  inscriptionIds,
353
356
  transferOrdinalsUtxos,
354
357
  utxosDescendingOrder,
358
+ taprootInputWitnessSize,
359
+ changeAddressType,
355
360
  })
356
361
 
357
362
  if (!selectedUtxos && !replaceTx) throw new Error('Not enough funds.')
@@ -471,6 +476,7 @@ export const createAndBroadcastTXFactory =
471
476
  ordinalsEnabled = false,
472
477
  utxosDescendingOrder,
473
478
  assetClientInterface,
479
+ changeAddressType,
474
480
  }) =>
475
481
  async ({ asset: maybeToken, walletAccount, address, amount: tokenAmount, options }) => {
476
482
  // Prepare transaction
@@ -498,6 +504,7 @@ export const createAndBroadcastTXFactory =
498
504
  utxosDescendingOrder,
499
505
  rbfEnabled,
500
506
  assetClientInterface,
507
+ changeAddressType,
501
508
  })({ asset: maybeToken, walletAccount, address, amount: tokenAmount, options })
502
509
  const {
503
510
  amount,