@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 +26 -0
- package/package.json +4 -3
- package/src/fee/fee-utils.js +2 -0
- package/src/fee/script-classifier.js +2 -0
- package/src/hash-utils.js +3 -0
- package/src/insight-api-client/index.js +29 -35
- package/src/move-funds.js +1 -1
- package/src/tx-log/bitcoin-monitor-scanner.js +10 -3
- package/src/tx-send/broadcast-tx.js +10 -7
- package/src/tx-send/index.js +3 -1
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.
|
|
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": "^
|
|
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": "
|
|
65
|
+
"gitHead": "e746a88ef78a0e7167587bfa14068ceb494d6fd7"
|
|
65
66
|
}
|
package/src/fee/fee-utils.js
CHANGED
|
@@ -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
|
-
|
|
24
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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(
|
|
20
|
-
/absurdly-high-fee/.test(
|
|
21
|
-
/too-long-mempool-chain/.test(
|
|
22
|
-
/txn-mempool-conflict/.test(
|
|
23
|
-
/tx-size/.test(
|
|
24
|
-
/txn-already-in-mempool/.test(
|
|
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.
|
|
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
|
package/src/tx-send/index.js
CHANGED
|
@@ -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 (/
|
|
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(),
|