@exodus/bitcoin-api 2.28.0 → 2.29.1

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,25 @@
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.29.1](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@2.29.0...@exodus/bitcoin-api@2.29.1) (2024-11-13)
7
+
8
+ **Note:** Update exports in fee module of package @exodus/bitcoin-api
9
+
10
+
11
+ ## [2.29.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@2.28.0...@exodus/bitcoin-api@2.29.0) (2024-11-10)
12
+
13
+
14
+ ### Features
15
+
16
+ * filter immature coinbase utxos in getUsableUtxos ([#4507](https://github.com/ExodusMovement/assets/issues/4507)) ([3774f66](https://github.com/ExodusMovement/assets/commit/3774f66ee733ca5a3f07a7556c349d08c2d217e9))
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * fetch and require raw txs for segwit inputs ([#4399](https://github.com/ExodusMovement/assets/issues/4399)) ([f5822cc](https://github.com/ExodusMovement/assets/commit/f5822cc60d6e542f9aea88c051e1a2dbe56da13d))
22
+
23
+
24
+
6
25
  ## [2.28.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@2.27.0...@exodus/bitcoin-api@2.28.0) (2024-10-31)
7
26
 
8
27
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/bitcoin-api",
3
- "version": "2.28.0",
3
+ "version": "2.29.1",
4
4
  "description": "Exodus bitcoin-api",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -56,5 +56,5 @@
56
56
  "type": "git",
57
57
  "url": "git+https://github.com/ExodusMovement/assets.git"
58
58
  },
59
- "gitHead": "65c334463a6fd913d73d2873ac455c5ec675705a"
59
+ "gitHead": "5faabb2475dbf47e9877276b1ef22ad237628c40"
60
60
  }
@@ -79,49 +79,17 @@ export const getSizeFactory = ({ defaultOutputType, addressApi }) => {
79
79
  4 + // n_locktime
80
80
  varuint.encodingLength(inputs.length) + // inputs_len
81
81
  // input[]
82
- inputs.reduce((t, script) => {
83
- if (script === null) script = '76a914000000000000000000000000000000000000000088ac' // P2PKH
84
- assert(isHex(script), 'script must be hex string')
85
-
86
- const scriptType = scriptClassifier.classifyScriptHex({ assetName, script })
87
-
88
- const supportedTypes = supportedInputTypes[assetName] || supportedInputTypes.default
89
- assert(
90
- supportedTypes.includes(scriptType),
91
- `Only ${supportedTypes.join(', ')} inputs supported right now`
92
- )
93
-
94
- const scriptSigLengths = compressed
95
- ? scriptSigCompressedLengths
96
- : scriptSigUncompressedLengths
97
- const scriptSigLength = scriptSigLengths[scriptType]
98
- return t + 32 + 4 + varuint.encodingLength(scriptSigLength) + scriptSigLength + 4
99
- }, 0) +
82
+ inputs.reduce(
83
+ (t, script) => t + getInputSize({ script, scriptClassifier, assetName, compressed }),
84
+ 0
85
+ ) +
100
86
  varuint.encodingLength(outputs.length) + // outputs_len
101
87
  // output[]
102
- outputs.reduce((t, output) => {
103
- // if (output === null) output = get(asset, 'address.versions.bech32') ? 'P2WSH' : 'P2PKH'
104
-
105
- if (output === null) output = defaultOutputType
106
-
107
- let scriptType = scriptClassify.types[output]
108
- const supportedTypes = supportedOutputTypes[assetName] || supportedOutputTypes.default
109
-
110
- if (!supportedTypes.includes(scriptType)) {
111
- scriptType = scriptClassifier.classifyAddress({
112
- assetName,
113
- address: output,
114
- })
115
- }
116
-
117
- assert(
118
- supportedTypes.includes(scriptType),
119
- `Only ${supportedTypes.join(', ')} outputs supported right now`
120
- )
121
-
122
- const scriptPubKeyLength = scriptPubKeyLengths[scriptType]
123
- return t + 8 + varuint.encodingLength(scriptPubKeyLength) + scriptPubKeyLength
124
- }, 0)
88
+ outputs.reduce(
89
+ (t, output) =>
90
+ t + getOutputSize({ output, scriptClassifier, assetName, defaultOutputType }),
91
+ 0
92
+ )
125
93
 
126
94
  const witnessSize =
127
95
  1 + // marker
@@ -169,4 +137,45 @@ const getFeeEstimatorFactory = ({ defaultOutputType, addressApi }) => {
169
137
  return createDefaultFeeEstimator(getSize)
170
138
  }
171
139
 
140
+ export const getInputSize = ({ script, scriptClassifier, assetName, compressed }) => {
141
+ if (script === null) script = '76a914000000000000000000000000000000000000000088ac' // P2PKH
142
+ assert(isHex(script), 'script must be hex string')
143
+
144
+ const scriptType = scriptClassifier.classifyScriptHex({ assetName, script })
145
+
146
+ const supportedTypes = supportedInputTypes[assetName] || supportedInputTypes.default
147
+ assert(
148
+ supportedTypes.includes(scriptType),
149
+ `Only ${supportedTypes.join(', ')} inputs supported right now`
150
+ )
151
+
152
+ const scriptSigLengths = compressed ? scriptSigCompressedLengths : scriptSigUncompressedLengths
153
+ const scriptSigLength = scriptSigLengths[scriptType]
154
+ return 32 + 4 + varuint.encodingLength(scriptSigLength) + scriptSigLength + 4
155
+ }
156
+
157
+ export const getOutputSize = ({ output, scriptClassifier, assetName, defaultOutputType }) => {
158
+ // if (output === null) output = get(asset, 'address.versions.bech32') ? 'P2WSH' : 'P2PKH'
159
+
160
+ if (output === null) output = defaultOutputType
161
+
162
+ let scriptType = scriptClassify.types[output]
163
+ const supportedTypes = supportedOutputTypes[assetName] || supportedOutputTypes.default
164
+
165
+ if (!supportedTypes.includes(scriptType)) {
166
+ scriptType = scriptClassifier.classifyAddress({
167
+ assetName,
168
+ address: output,
169
+ })
170
+ }
171
+
172
+ assert(
173
+ supportedTypes.includes(scriptType),
174
+ `Only ${supportedTypes.join(', ')} outputs supported right now`
175
+ )
176
+
177
+ const scriptPubKeyLength = scriptPubKeyLengths[scriptType]
178
+ return 8 + varuint.encodingLength(scriptPubKeyLength) + scriptPubKeyLength
179
+ }
180
+
172
181
  export default getFeeEstimatorFactory
package/src/fee/index.js CHANGED
@@ -1,2 +1,9 @@
1
1
  export * from './get-fee-resolver.js'
2
- export { default as getFeeEstimatorFactory } from './fee-estimator.js'
2
+ export {
3
+ default as getFeeEstimatorFactory,
4
+ getInputSize,
5
+ getOutputSize,
6
+ getSizeFactory,
7
+ } from './fee-estimator.js'
8
+ export { default as createDefaultFeeEstimator } from './fee-utils.js'
9
+ export { scriptClassifierFactory } from './script-classifier.js'
@@ -418,6 +418,7 @@ export class BitcoinMonitorScanner {
418
418
  feePerKB: txItem.fees ? (txItem.fees / txItem.vsize) * 1000 * 1e8 : null,
419
419
  rbfEnabled: txItem.rbf,
420
420
  blocksSeen: 0,
421
+ ...(txItem.vin.length === 0 ? { isCoinbase: true } : undefined),
421
422
  },
422
423
  currencies: { [assetName]: currency },
423
424
  }
@@ -544,6 +545,7 @@ export class BitcoinMonitorScanner {
544
545
  script: vout.scriptPubKey.hex,
545
546
  value: val,
546
547
  rbfEnabled: txItem.rbf,
548
+ ...(txItem.vin.length === 0 ? { isCoinbase: true } : undefined),
547
549
  }
548
550
 
549
551
  if (this.#ordinalsEnabled) {
@@ -71,7 +71,14 @@ export async function getNonWitnessTxs(asset, utxos, insightClient) {
71
71
  utxos
72
72
  .getAddressesForTxId(txId)
73
73
  .toAddressStrings()
74
- .some((a) => asset.address.isP2PKH(a) || asset.address.isP2SH(a))
74
+ .some(
75
+ (a) =>
76
+ asset.address.isP2PKH(a) ||
77
+ asset.address.isP2SH(a) ||
78
+ asset.address.isP2SH2?.(a) ||
79
+ asset.address.isP2WPKH?.(a) ||
80
+ asset.address.isP2WSH?.(a)
81
+ )
75
82
  )
76
83
 
77
84
  if (nonWitnessTxIds.length > 0) {
@@ -85,17 +85,24 @@ function createPsbtFromTxData({
85
85
  const isTaprootAddress = purpose === 86
86
86
 
87
87
  const txIn = { hash: txId, index: vout, sequence }
88
- if (isSegwitAddress || isTaprootAddress) {
89
- if (isTaprootAddress && tapLeafScript) {
90
- txIn.tapLeafScript = tapLeafScript
91
- }
92
88
 
93
- // witness outputs only require the value and the script, not the full transaction
89
+ if (isTaprootAddress && tapLeafScript) {
90
+ txIn.tapLeafScript = tapLeafScript
91
+ }
92
+
93
+ if (isSegwitAddress || isTaprootAddress) {
94
+ // taproot outputs only require the value and the script, not the full transaction
94
95
  txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
95
- } else {
96
- const rawTx = (rawTxs || []).find((t) => t.txId === txId)
97
- // non-witness outptus require the full transaction
98
- assert(!!rawTx, 'Non-witness outputs require the full previous transaction.')
96
+ }
97
+
98
+ const rawTx = (rawTxs || []).find((t) => t.txId === txId)
99
+
100
+ // Non-taproot outputs require the full transaction
101
+ assert(
102
+ isTaprootAddress || !!rawTx?.rawData,
103
+ `Non-taproot outputs require the full previous transaction.`
104
+ )
105
+ if (!isTaprootAddress) {
99
106
  const rawTxBuffer = Buffer.from(rawTx.rawData, 'hex')
100
107
  if (canParseTx(Transaction, rawTxBuffer)) {
101
108
  txIn.nonWitnessUtxo = rawTxBuffer
@@ -233,11 +233,25 @@ function filterDustUtxos({ utxos, feeData }) {
233
233
  return utxos
234
234
  }
235
235
 
236
- export function getUsableUtxos({ asset, utxos, feeData, txSet, unconfirmedTxAncestor }) {
236
+ const COINBASE_MATURITY_HEIGHT = 100
237
+
238
+ export function getUsableUtxos({
239
+ asset,
240
+ utxos: someCoinbaseUtxos,
241
+ feeData,
242
+ txSet,
243
+ unconfirmedTxAncestor,
244
+ }) {
237
245
  assert(asset, 'asset is required')
238
- assert(utxos, 'utxos is required')
246
+ assert(someCoinbaseUtxos, 'utxos is required')
239
247
  assert(feeData, 'feeData is required')
240
248
  assert(txSet, 'txSet is required')
249
+
250
+ // Filter out immature coinbase outputs. They are not spendable.
251
+ const utxos = someCoinbaseUtxos.filter(
252
+ (utxo) => !utxo.isCoinbase || utxo.confirmations >= COINBASE_MATURITY_HEIGHT
253
+ )
254
+
241
255
  if (!['bitcoin', 'bitcointestnet', 'bitcoinregtest'].includes(asset.name))
242
256
  return filterDustUtxos({ utxos, feeData })
243
257