@exodus/bitcoin-api 4.9.6 → 4.11.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,32 @@
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
+ ## [4.11.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.10.0...@exodus/bitcoin-api@4.11.0) (2026-03-15)
7
+
8
+
9
+ ### Features
10
+
11
+
12
+ * feat: update wif to 4.0.0 (#6195)
13
+
14
+
15
+
16
+ ## [4.10.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.9.6...@exodus/bitcoin-api@4.10.0) (2026-03-11)
17
+
18
+
19
+ ### Features
20
+
21
+
22
+ * feat: extract trace id in bitcoin (#7278)
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+
28
+ * fix(bitcoin-api): avoid restoring spent dropped inputs (#7559)
29
+
30
+
31
+
6
32
  ## [4.9.6](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.9.5...@exodus/bitcoin-api@4.9.6) (2026-03-02)
7
33
 
8
34
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/bitcoin-api",
3
- "version": "4.9.6",
3
+ "version": "4.11.0",
4
4
  "description": "Bitcoin transaction and fee monitors, RPC with the blockchain node, other networking code.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -36,6 +36,7 @@
36
36
  "@exodus/safe-string": "^1.4.0",
37
37
  "@exodus/send-validation-model": "^1.0.0",
38
38
  "@exodus/simple-retry": "^0.0.6",
39
+ "@exodus/traceparent": "^3.0.1",
39
40
  "bech32": "^1.1.3",
40
41
  "bip32-path": "^0.4.2",
41
42
  "bs58check": "^3.0.1",
@@ -47,7 +48,7 @@
47
48
  "socket.io-client": "^2.1.1",
48
49
  "url-join": "^4.0.0",
49
50
  "varuint-bitcoin": "^1.1.0",
50
- "wif": "^2.0.6"
51
+ "wif": "^4.0.0"
51
52
  },
52
53
  "devDependencies": {
53
54
  "@exodus/bitcoin-meta": "^2.0.0",
@@ -61,5 +62,5 @@
61
62
  "type": "git",
62
63
  "url": "git+https://github.com/ExodusMovement/assets.git"
63
64
  },
64
- "gitHead": "04dc4d08ec82b0405ec1ec3a52d682ef11451914"
65
+ "gitHead": "e746a88ef78a0e7167587bfa14068ceb494d6fd7"
65
66
  }
@@ -1,3 +1,4 @@
1
+ // eslint-disable-next-line @exodus/import/no-deprecated
1
2
  import { isNumberUnit, UnitType } from '@exodus/currency'
2
3
  import { UtxoCollection } from '@exodus/models'
3
4
  import assert from 'minimalistic-assert'
@@ -53,6 +54,7 @@ export default function createDefaultFeeEstimator(getSize) {
53
54
  export function parseCurrency(val, currency) {
54
55
  assert(currency instanceof UnitType, 'Currency must be supples as a UnitType')
55
56
 
57
+ // eslint-disable-next-line @exodus/import/no-deprecated
56
58
  if (isNumberUnit(val)) return val // TODO: consider checking if the unitType.equals(currency) (if currency is object)
57
59
 
58
60
  if (typeof val === 'string') return currency.parse(val)
@@ -1,5 +1,6 @@
1
1
  import { memoizeLruCache } from '@exodus/asset-lib'
2
2
  import { scriptClassify } from '@exodus/bitcoinjs'
3
+ // eslint-disable-next-line @exodus/import/no-deprecated
3
4
  import { hashSync } from '@exodus/crypto/hash'
4
5
  import assert from 'minimalistic-assert'
5
6
 
@@ -8,6 +9,7 @@ const { P2PKH, P2SH, P2WPKH, P2WSH, P2TR } = scriptClassify.types
8
9
  const cacheSize = 1000
9
10
  const maxSize = 30
10
11
  const hashStringIfTooBig = (str) =>
12
+ // eslint-disable-next-line @exodus/import/no-deprecated
11
13
  str.length > maxSize ? hashSync('sha256', str, 'hex').slice(0, maxSize) : str
12
14
 
13
15
  export const scriptClassifierFactory = ({ addressApi }) => {
package/src/hash-utils.js CHANGED
@@ -1,9 +1,12 @@
1
+ // eslint-disable-next-line @exodus/import/no-deprecated
1
2
  import { hashSync } from '@exodus/crypto/hash'
2
3
 
3
4
  export function hash160(buffer) {
5
+ // eslint-disable-next-line @exodus/import/no-deprecated
4
6
  return hashSync('hash160', buffer)
5
7
  }
6
8
 
7
9
  export function sha256(buffer) {
10
+ // eslint-disable-next-line @exodus/import/no-deprecated
8
11
  return hashSync('sha256', buffer)
9
12
  }
@@ -1,5 +1,6 @@
1
1
  import { safeString } from '@exodus/safe-string'
2
2
  import { retry } from '@exodus/simple-retry'
3
+ import { TraceId } from '@exodus/traceparent'
3
4
  import delay from 'delay'
4
5
  import lodash from 'lodash'
5
6
  import urlJoin from 'url-join'
@@ -11,7 +12,6 @@ const INSIGHT_JSON_ERROR_MESSAGE = safeString`insight-api-invalid-json`
11
12
  const INSIGHT_MISSING_TXID_MESSAGE = safeString`insight-api-missing-txid`
12
13
  const INSIGHT_HTTP_ERROR_BALANCE_MESSAGE = safeString`insight-api-http-error:balance`
13
14
  const INSIGHT_HTTP_ERROR_STATUS_MESSAGE = safeString`insight-api-http-error:status`
14
- const INSIGHT_HTTP_ERROR_ADDR_MESSAGE = safeString`insight-api-http-error:addr`
15
15
  const INSIGHT_HTTP_ERROR_UTXO_MESSAGE = safeString`insight-api-http-error:utxo`
16
16
  const INSIGHT_HTTP_ERROR_TX_MESSAGE = safeString`insight-api-http-error:tx`
17
17
  const INSIGHT_HTTP_ERROR_FULLTX_MESSAGE = safeString`insight-api-http-error:fulltx`
@@ -20,8 +20,19 @@ const INSIGHT_HTTP_ERROR_ADDRS_TXS_MESSAGE = safeString`insight-api-http-error:a
20
20
  const INSIGHT_HTTP_ERROR_UNCONFIRMED_ANCESTOR_MESSAGE = safeString`insight-api-http-error:unconfirmed-ancestor`
21
21
  const INSIGHT_HTTP_ERROR_FEES_MESSAGE = safeString`insight-api-http-error:fees`
22
22
  const INSIGHT_HTTP_ERROR_BROADCAST_MESSAGE = safeString`insight-api-http-error:broadcast`
23
- const INSIGHT_HTTP_ERROR_CLAIMABLE_MESSAGE = safeString`insight-api-http-error:claimable`
24
- const INSIGHT_HTTP_ERROR_UNCLAIMED_MESSAGE = safeString`insight-api-http-error:unclaimed`
23
+
24
+ const parseBroadcastErrorReason = (data) => {
25
+ if (!data) {
26
+ return null
27
+ }
28
+
29
+ try {
30
+ const parsed = JSON.parse(data)
31
+ return parsed?.error || data
32
+ } catch {
33
+ return data
34
+ }
35
+ }
25
36
 
26
37
  const fetchJson = async (
27
38
  url,
@@ -35,8 +46,11 @@ const fetchJson = async (
35
46
  return null
36
47
  }
37
48
 
49
+ const traceId = TraceId.fromResponse(response)
50
+
38
51
  if (!response.ok) {
39
52
  const error = new Error(httpErrorMessage)
53
+ error.traceId = traceId
40
54
  error.code = `${response.status}`
41
55
  throw error
42
56
  }
@@ -44,7 +58,9 @@ const fetchJson = async (
44
58
  try {
45
59
  return await response.json()
46
60
  } catch (err) {
47
- throw new Error(INSIGHT_JSON_ERROR_MESSAGE, { cause: err })
61
+ const error = new Error(INSIGHT_JSON_ERROR_MESSAGE, { cause: err })
62
+ error.traceId = traceId
63
+ throw error
48
64
  }
49
65
  }
50
66
 
@@ -63,12 +79,6 @@ export default class InsightAPIClient {
63
79
  this._baseURL = baseURL
64
80
  }
65
81
 
66
- async isNetworkConnected() {
67
- const url = urlJoin(this._baseURL, '/peer')
68
- const peerStatus = await fetchJson(url, { timeout: 10_000 })
69
- return !!peerStatus.connected
70
- }
71
-
72
82
  async fetchBalance(address) {
73
83
  const encodedAddress = encodeURIComponent(address)
74
84
  const url = urlJoin(this._baseURL, `/balance/${encodedAddress}`)
@@ -86,24 +96,14 @@ export default class InsightAPIClient {
86
96
  return status.info.blocks
87
97
  }
88
98
 
89
- async fetchAddress(address, opts) {
99
+ async fetchUTXOs(address, { assetNames = [] } = {}) {
90
100
  const encodedAddress = encodeURIComponent(address)
91
- const url = urlJoin(
92
- this._baseURL,
93
- opts?.includeTxs ? `/addr/${encodedAddress}` : `/addr/${encodedAddress}?noTxList=1`
94
- )
95
- return fetchJson(url, undefined, false, INSIGHT_HTTP_ERROR_ADDR_MESSAGE)
96
- }
97
-
98
- async fetchUTXOs(addresses, { assetNames = [] } = {}) {
99
- if (Array.isArray(addresses)) addresses = addresses.join(',')
100
101
  const query = new URLSearchParams('noCache=1')
101
102
  if (assetNames) {
102
103
  query.set('assetNames', assetNames.join(','))
103
104
  }
104
105
 
105
- const encodedAddresses = encodeURIComponent(addresses)
106
- const url = urlJoin(this._baseURL, `/addrs/${encodedAddresses}/utxo?${query}`)
106
+ const url = urlJoin(this._baseURL, `/addrs/${encodedAddress}/utxo?${query}`)
107
107
  const utxos = await fetchJson(url, undefined, false, INSIGHT_HTTP_ERROR_UTXO_MESSAGE)
108
108
 
109
109
  return utxos.map((utxo) => ({
@@ -204,12 +204,16 @@ export default class InsightAPIClient {
204
204
  const response = await fetch(url, fetchOptions)
205
205
  let data = await response.text()
206
206
 
207
+ const traceId = TraceId.fromResponse(response)
208
+
207
209
  if (!response.ok) {
208
210
  console.warn('Insight Broadcast HTTP Error:')
209
211
  console.warn(response.statusText)
210
212
  console.warn(data)
211
213
  const error = new Error(INSIGHT_HTTP_ERROR_BROADCAST_MESSAGE)
214
+ error.traceId = traceId
212
215
  error.code = `${response.status}`
216
+ error.reason = parseBroadcastErrorReason(data)
213
217
  throw error
214
218
  }
215
219
 
@@ -221,19 +225,9 @@ export default class InsightAPIClient {
221
225
  }
222
226
 
223
227
  if (!data.txid) {
224
- throw new Error(INSIGHT_MISSING_TXID_MESSAGE)
228
+ const error = new Error(INSIGHT_MISSING_TXID_MESSAGE)
229
+ error.traceId = traceId
230
+ throw error
225
231
  }
226
232
  }
227
-
228
- async getClaimable(address) {
229
- const encodedAddress = encodeURIComponent(address)
230
- const url = urlJoin(this._baseURL, `/addr/${encodedAddress}/claimable`)
231
- return fetchJson(url, undefined, false, INSIGHT_HTTP_ERROR_CLAIMABLE_MESSAGE)
232
- }
233
-
234
- async getUnclaimed(address) {
235
- const encodedAddress = encodeURIComponent(address)
236
- const url = urlJoin(this._baseURL, `/addr/${encodedAddress}/unclaimed`)
237
- return fetchJson(url, undefined, false, INSIGHT_HTTP_ERROR_UNCLAIMED_MESSAGE)
238
- }
239
233
  }
package/src/move-funds.js CHANGED
@@ -204,7 +204,7 @@ export const moveFundsFactory = ({
204
204
  }
205
205
 
206
206
  async function getUtxos({ asset, address }) {
207
- const rawUtxos = await insightClient.fetchUTXOs([address])
207
+ const rawUtxos = await insightClient.fetchUTXOs(address)
208
208
  return UtxoCollection.fromArray(
209
209
  rawUtxos.map((utxo) => ({
210
210
  txId: utxo.txId,
@@ -632,9 +632,16 @@ export class BitcoinMonitorScanner {
632
632
  continue
633
633
  }
634
634
 
635
- const tx = await insightClient.fetchTx(utxo.txId)
636
- if (tx) {
637
- // previously spent tx still exists, readd utxo
635
+ let prevTx = null
636
+ try {
637
+ prevTx = await insightClient.fetchTxObject(utxo.txId)
638
+ } catch {
639
+ prevTx = null
640
+ }
641
+
642
+ const prevOutput = prevTx?.vout?.find((output) => output.n === utxo.vout)
643
+ if (prevOutput && !prevOutput.spentTxId) {
644
+ // Only readd inputs whose exact outpoint is still unspent.
638
645
  utxosToAdd.push(utxo)
639
646
  }
640
647
  }
@@ -1,5 +1,7 @@
1
1
  import { retry } from '@exodus/simple-retry'
2
2
 
3
+ const getErrorText = (error) => [error?.message, error?.reason].filter(Boolean).join(' ')
4
+
3
5
  /**
4
6
  * Broadcast a signed transaction to the Bitcoin network with retry logic
5
7
  * @param {Object} params
@@ -14,14 +16,15 @@ export async function broadcastTransaction({ asset, rawTx }) {
14
16
  try {
15
17
  return await asset.api.broadcastTx(rawTxHex)
16
18
  } catch (e) {
19
+ const errorText = getErrorText(e)
17
20
  // Mark certain errors as final (non-retryable)
18
21
  if (
19
- /missing inputs/i.test(e.message) ||
20
- /absurdly-high-fee/.test(e.message) ||
21
- /too-long-mempool-chain/.test(e.message) ||
22
- /txn-mempool-conflict/.test(e.message) ||
23
- /tx-size/.test(e.message) ||
24
- /txn-already-in-mempool/.test(e.message)
22
+ /(missing inputs|missingorspent)/i.test(errorText) ||
23
+ /absurdly-high-fee/.test(errorText) ||
24
+ /too-long-mempool-chain/.test(errorText) ||
25
+ /txn-mempool-conflict/.test(errorText) ||
26
+ /tx-size/.test(errorText) ||
27
+ /txn-already-in-mempool/.test(errorText)
25
28
  ) {
26
29
  e.finalError = true
27
30
  }
@@ -37,7 +40,7 @@ export async function broadcastTransaction({ asset, rawTx }) {
37
40
  try {
38
41
  await broadcastTxWithRetry(rawTxHex)
39
42
  } catch (err) {
40
- if (err.message.includes('txn-already-in-mempool')) {
43
+ if (getErrorText(err).includes('txn-already-in-mempool')) {
41
44
  // Not an error, transaction is already broadcast
42
45
  console.log('Transaction is already in the mempool.')
43
46
  return
@@ -6,6 +6,8 @@ import { extractTransactionContext } from '../psbt-parser.js'
6
6
  import { broadcastTransaction } from './broadcast-tx.js'
7
7
  import { updateAccountState, updateTransactionLog } from './update-state.js'
8
8
 
9
+ const getErrorText = (error) => [error?.message, error?.reason].filter(Boolean).join(' ')
10
+
9
11
  const checkTxExists = async ({ asset, txId }) => {
10
12
  try {
11
13
  const tx = await asset.insightClient.fetchTx(txId)
@@ -212,7 +214,7 @@ export const sendTxFactory = ({
212
214
  if (txExists) {
213
215
  console.warn(`tx-send: ${assetName} tx already broadcast`, txId)
214
216
  } else {
215
- if (/insight broadcast http error.*missing inputs/i.test(err.message)) {
217
+ if (/(missing inputs|missingorspent)/i.test(getErrorText(err))) {
216
218
  err.txInfo = JSON.stringify({
217
219
  amount: sendAmount.toDefaultString({ unit: true }),
218
220
  fee: ((fee && fee.toDefaultString({ unit: true })) || 0).toString(),