@exodus/bitcoin-api 4.9.0 → 4.9.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,16 @@
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.9.1](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.9.0...@exodus/bitcoin-api@4.9.1) (2026-01-26)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+
12
+ * fix(bitcoin-api): add safe-string errors for Insight failures (#7320)
13
+
14
+
15
+
6
16
  ## [4.9.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.8.3...@exodus/bitcoin-api@4.9.0) (2025-12-23)
7
17
 
8
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/bitcoin-api",
3
- "version": "4.9.0",
3
+ "version": "4.9.1",
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",
@@ -33,6 +33,7 @@
33
33
  "@exodus/i18n-dummy": "^1.0.0",
34
34
  "@exodus/key-identifier": "^1.3.0",
35
35
  "@exodus/models": "^12.0.1",
36
+ "@exodus/safe-string": "^1.4.0",
36
37
  "@exodus/send-validation-model": "^1.0.0",
37
38
  "@exodus/simple-retry": "^0.0.6",
38
39
  "bech32": "^1.1.3",
@@ -60,5 +61,5 @@
60
61
  "type": "git",
61
62
  "url": "git+https://github.com/ExodusMovement/assets.git"
62
63
  },
63
- "gitHead": "39b743562bc1171bf3d145b47382db9023c660dd"
64
+ "gitHead": "481b327c2b57d6272c47807615be9f967ebb3d13"
64
65
  }
@@ -1,4 +1,3 @@
1
- import NumberUnit from '@exodus/currency'
2
1
  import { UtxoCollection } from '@exodus/models'
3
2
  import lodash from 'lodash'
4
3
  import assert from 'minimalistic-assert'
@@ -68,12 +67,10 @@ export const selectUtxos = ({
68
67
  const changeUtxos = usableUtxos.getTxIdUtxos(tx.txId)
69
68
  // Don't replace a tx that has already been spent
70
69
  if (tx.data.changeAddress && changeUtxos.size === 0) continue
71
- let feePerKB
72
- if (tx.data.feePerKB + MIN_RELAY_FEE > feeRate.toBaseNumber()) {
73
- feePerKB = new NumberUnit(tx.data.feePerKB + MIN_RELAY_FEE, asset.currency.baseUnit)
74
- } else {
75
- feePerKB = feeRate
76
- }
70
+ const feePerKB =
71
+ tx.data.feePerKB + MIN_RELAY_FEE > feeRate.toBaseNumber()
72
+ ? currency.baseUnit(tx.data.feePerKB + MIN_RELAY_FEE)
73
+ : feeRate
77
74
 
78
75
  const replaceFeeEstimator = getFeeEstimator(asset, { feePerKB, unconfirmedTxAncestor })
79
76
  const inputs = UtxoCollection.fromJSON(tx.data.inputs, { currency })
@@ -1,3 +1,4 @@
1
+ import { safeString } from '@exodus/safe-string'
1
2
  import { retry } from '@exodus/simple-retry'
2
3
  import delay from 'delay'
3
4
  import lodash from 'lodash'
@@ -5,16 +6,29 @@ import urlJoin from 'url-join'
5
6
 
6
7
  const { isEmpty } = lodash
7
8
 
8
- const getTextFromResponse = async (response) => {
9
- try {
10
- const responseBody = await response.text()
11
- return responseBody.slice(0, 100)
12
- } catch {
13
- return ''
14
- }
15
- }
16
-
17
- const fetchJson = async (url, fetchOptions, nullWhen404) => {
9
+ const INSIGHT_HTTP_ERROR_MESSAGE = safeString`insight-api-http-error`
10
+ const INSIGHT_JSON_ERROR_MESSAGE = safeString`insight-api-invalid-json`
11
+ const INSIGHT_MISSING_TXID_MESSAGE = safeString`insight-api-missing-txid`
12
+ const INSIGHT_HTTP_ERROR_BALANCE_MESSAGE = safeString`insight-api-http-error:balance`
13
+ 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
+ const INSIGHT_HTTP_ERROR_UTXO_MESSAGE = safeString`insight-api-http-error:utxo`
16
+ const INSIGHT_HTTP_ERROR_TX_MESSAGE = safeString`insight-api-http-error:tx`
17
+ const INSIGHT_HTTP_ERROR_FULLTX_MESSAGE = safeString`insight-api-http-error:fulltx`
18
+ const INSIGHT_HTTP_ERROR_RAWTX_MESSAGE = safeString`insight-api-http-error:rawtx`
19
+ const INSIGHT_HTTP_ERROR_ADDRS_TXS_MESSAGE = safeString`insight-api-http-error:addrs-txs`
20
+ const INSIGHT_HTTP_ERROR_UNCONFIRMED_ANCESTOR_MESSAGE = safeString`insight-api-http-error:unconfirmed-ancestor`
21
+ const INSIGHT_HTTP_ERROR_FEES_MESSAGE = safeString`insight-api-http-error:fees`
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`
25
+
26
+ const fetchJson = async (
27
+ url,
28
+ fetchOptions,
29
+ nullWhen404,
30
+ httpErrorMessage = INSIGHT_HTTP_ERROR_MESSAGE
31
+ ) => {
18
32
  const response = await fetch(url, fetchOptions)
19
33
 
20
34
  if (nullWhen404 && response.status === 404) {
@@ -22,20 +36,22 @@ const fetchJson = async (url, fetchOptions, nullWhen404) => {
22
36
  }
23
37
 
24
38
  if (!response.ok) {
25
- throw new Error(
26
- `${url} returned ${response.status}: ${
27
- response.statusText || 'Unknown Status Text'
28
- }. Body: ${await getTextFromResponse(response)}`
29
- )
39
+ const error = new Error(httpErrorMessage)
40
+ error.code = `${response.status}`
41
+ throw error
30
42
  }
31
43
 
32
- return response.json()
44
+ try {
45
+ return await response.json()
46
+ } catch (err) {
47
+ throw new Error(INSIGHT_JSON_ERROR_MESSAGE, { cause: err })
48
+ }
33
49
  }
34
50
 
35
- async function fetchJsonRetry(url, fetchOptions) {
51
+ async function fetchJsonRetry(url, fetchOptions, httpErrorMessage) {
36
52
  const waitTimes = ['5s', '10s', '20s', '30s']
37
53
  const fetchWithRetry = retry(fetchJson, { delayTimesMs: waitTimes })
38
- return fetchWithRetry(url, fetchOptions)
54
+ return fetchWithRetry(url, fetchOptions, false, httpErrorMessage)
39
55
  }
40
56
 
41
57
  export default class InsightAPIClient {
@@ -56,12 +72,17 @@ export default class InsightAPIClient {
56
72
  async fetchBalance(address) {
57
73
  const encodedAddress = encodeURIComponent(address)
58
74
  const url = urlJoin(this._baseURL, `/balance/${encodedAddress}`)
59
- return fetchJson(url)
75
+ return fetchJson(url, undefined, false, INSIGHT_HTTP_ERROR_BALANCE_MESSAGE)
60
76
  }
61
77
 
62
78
  async fetchBlockHeight() {
63
79
  const url = urlJoin(this._baseURL, '/status')
64
- const status = await fetchJson(url, { timeout: 10_000 })
80
+ const status = await fetchJson(
81
+ url,
82
+ { timeout: 10_000 },
83
+ false,
84
+ INSIGHT_HTTP_ERROR_STATUS_MESSAGE
85
+ )
65
86
  return status.info.blocks
66
87
  }
67
88
 
@@ -71,7 +92,7 @@ export default class InsightAPIClient {
71
92
  this._baseURL,
72
93
  opts?.includeTxs ? `/addr/${encodedAddress}` : `/addr/${encodedAddress}?noTxList=1`
73
94
  )
74
- return fetchJson(url)
95
+ return fetchJson(url, undefined, false, INSIGHT_HTTP_ERROR_ADDR_MESSAGE)
75
96
  }
76
97
 
77
98
  async fetchUTXOs(addresses, { assetNames = [] } = {}) {
@@ -83,7 +104,7 @@ export default class InsightAPIClient {
83
104
 
84
105
  const encodedAddresses = encodeURIComponent(addresses)
85
106
  const url = urlJoin(this._baseURL, `/addrs/${encodedAddresses}/utxo?${query}`)
86
- const utxos = await fetchJson(url)
107
+ const utxos = await fetchJson(url, undefined, false, INSIGHT_HTTP_ERROR_UTXO_MESSAGE)
87
108
 
88
109
  return utxos.map((utxo) => ({
89
110
  address: utxo.address,
@@ -100,12 +121,12 @@ export default class InsightAPIClient {
100
121
  async fetchTx(txId) {
101
122
  const encodedTxId = encodeURIComponent(txId)
102
123
  const url = urlJoin(this._baseURL, `/tx/${encodedTxId}`)
103
- return fetchJson(url, undefined, true)
124
+ return fetchJson(url, undefined, true, INSIGHT_HTTP_ERROR_TX_MESSAGE)
104
125
  }
105
126
 
106
127
  async fetchTxObject(txId) {
107
128
  const url = urlJoin(this._baseURL, `/fulltx?${new URLSearchParams({ hash: txId })}`)
108
- const object = await fetchJson(url, undefined, true)
129
+ const object = await fetchJson(url, undefined, true, INSIGHT_HTTP_ERROR_FULLTX_MESSAGE)
109
130
  if (!object || isEmpty(object)) {
110
131
  return null
111
132
  }
@@ -116,7 +137,7 @@ export default class InsightAPIClient {
116
137
  async fetchRawTx(txId) {
117
138
  const encodedTxId = encodeURIComponent(txId)
118
139
  const url = urlJoin(this._baseURL, `/rawtx/${encodedTxId}`)
119
- const { rawtx } = await fetchJson(url)
140
+ const { rawtx } = await fetchJson(url, undefined, false, INSIGHT_HTTP_ERROR_RAWTX_MESSAGE)
120
141
  return rawtx
121
142
  }
122
143
 
@@ -136,7 +157,7 @@ export default class InsightAPIClient {
136
157
  timeout: 10_000,
137
158
  }
138
159
 
139
- return fetchJsonRetry(url, fetchOptions)
160
+ return fetchJsonRetry(url, fetchOptions, INSIGHT_HTTP_ERROR_ADDRS_TXS_MESSAGE)
140
161
  }
141
162
 
142
163
  async fetchAllTxData(addrs = [], chunk = 25, httpDelay = 2000, shouldStopFetching = () => {}) {
@@ -159,12 +180,12 @@ export default class InsightAPIClient {
159
180
  async fetchUnconfirmedAncestorData(txId) {
160
181
  const encodedTxId = encodeURIComponent(txId)
161
182
  const url = urlJoin(this._baseURL, `/unconfirmed_ancestor/${encodedTxId}`)
162
- return fetchJson(url)
183
+ return fetchJson(url, undefined, false, INSIGHT_HTTP_ERROR_UNCONFIRMED_ANCESTOR_MESSAGE)
163
184
  }
164
185
 
165
186
  async fetchFeeRate() {
166
187
  const url = urlJoin(this._baseURL, '/v2/fees')
167
- return fetchJson(url)
188
+ return fetchJson(url, undefined, false, INSIGHT_HTTP_ERROR_FEES_MESSAGE)
168
189
  }
169
190
 
170
191
  async broadcastTx(rawTx) {
@@ -187,28 +208,32 @@ export default class InsightAPIClient {
187
208
  console.warn('Insight Broadcast HTTP Error:')
188
209
  console.warn(response.statusText)
189
210
  console.warn(data)
190
- throw new Error(`Insight Broadcast HTTP Error: ${data}`)
211
+ const error = new Error(INSIGHT_HTTP_ERROR_BROADCAST_MESSAGE)
212
+ error.code = `${response.status}`
213
+ throw error
191
214
  }
192
215
 
193
216
  try {
194
217
  data = JSON.parse(data)
195
218
  } catch (err) {
196
219
  console.warn('Insight Broadcast JSON Parse Error:', err.message, data)
197
- throw new Error(`data: ${data}`, { cause: err })
220
+ throw new Error(INSIGHT_JSON_ERROR_MESSAGE, { cause: err })
198
221
  }
199
222
 
200
- if (!data.txid) throw new Error('transaction id was not returned')
223
+ if (!data.txid) {
224
+ throw new Error(INSIGHT_MISSING_TXID_MESSAGE)
225
+ }
201
226
  }
202
227
 
203
228
  async getClaimable(address) {
204
229
  const encodedAddress = encodeURIComponent(address)
205
230
  const url = urlJoin(this._baseURL, `/addr/${encodedAddress}/claimable`)
206
- return fetchJson(url)
231
+ return fetchJson(url, undefined, false, INSIGHT_HTTP_ERROR_CLAIMABLE_MESSAGE)
207
232
  }
208
233
 
209
234
  async getUnclaimed(address) {
210
235
  const encodedAddress = encodeURIComponent(address)
211
236
  const url = urlJoin(this._baseURL, `/addr/${encodedAddress}/unclaimed`)
212
- return fetchJson(url)
237
+ return fetchJson(url, undefined, false, INSIGHT_HTTP_ERROR_UNCLAIMED_MESSAGE)
213
238
  }
214
239
  }