@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 +10 -0
- package/package.json +3 -2
- package/src/fee/utxo-selector.js +4 -7
- package/src/insight-api-client/index.js +58 -33
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.
|
|
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": "
|
|
64
|
+
"gitHead": "481b327c2b57d6272c47807615be9f967ebb3d13"
|
|
64
65
|
}
|
package/src/fee/utxo-selector.js
CHANGED
|
@@ -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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
220
|
+
throw new Error(INSIGHT_JSON_ERROR_MESSAGE, { cause: err })
|
|
198
221
|
}
|
|
199
222
|
|
|
200
|
-
if (!data.txid)
|
|
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
|
}
|