@exodus/bitcoin-api 4.1.2 → 4.1.4

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,26 @@
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.1.4](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.1.3...@exodus/bitcoin-api@4.1.4) (2025-10-15)
7
+
8
+ **Note:** Version bump only for package @exodus/bitcoin-api
9
+
10
+
11
+
12
+
13
+
14
+ ## [4.1.3](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.1.1...@exodus/bitcoin-api@4.1.3) (2025-10-14)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+
20
+ * fix: remove isBip70 from bitcoin libs (#6660)
21
+
22
+ * fix: remove unused bitcoin.api.prepareSendTx (#6662)
23
+
24
+
25
+
6
26
  ## [4.1.2](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.1.1...@exodus/bitcoin-api@4.1.2) (2025-10-09)
7
27
 
8
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/bitcoin-api",
3
- "version": "4.1.2",
3
+ "version": "4.1.4",
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",
@@ -60,5 +60,5 @@
60
60
  "type": "git",
61
61
  "url": "git+https://github.com/ExodusMovement/assets.git"
62
62
  },
63
- "gitHead": "6237b0d98791ed9e6290260947068d2600bf8270"
63
+ "gitHead": "0efd644cc413c308688b43c8563b172b3a80e3fa"
64
64
  }
@@ -2,13 +2,7 @@ import assert from 'minimalistic-assert'
2
2
 
3
3
  import { findUnconfirmedSentRbfTxs } from '../tx-utils.js'
4
4
  import { getUnconfirmedTxAncestorMap } from '../unconfirmed-ancestor-data.js'
5
- import {
6
- getInscriptionIds,
7
- getOrdinalsUtxos,
8
- getTransferOrdinalsUtxos,
9
- getUsableUtxos,
10
- getUtxos,
11
- } from '../utxos-utils.js'
5
+ import { getUsableUtxos, getUtxos } from '../utxos-utils.js'
12
6
  import { canBumpTx } from './can-bump-tx.js'
13
7
  import { getUtxosData } from './utxo-selector.js'
14
8
 
@@ -40,17 +34,9 @@ export class GetFeeResolver {
40
34
  amount,
41
35
  customFee,
42
36
  isSendAll,
43
- nft, // sending one nft
44
37
  receiveAddress,
45
38
  taprootInputWitnessSize,
46
39
  }) => {
47
- if (nft) {
48
- assert(!amount, 'amount must not be provided when nft is provided!!!')
49
- assert(!isSendAll, 'isSendAll must not be provided when nft is provided!!!')
50
- }
51
-
52
- const inscriptionIds = getInscriptionIds({ nft })
53
-
54
40
  const { fee, unspendableFee, extraFeeData } = this.#getUtxosData({
55
41
  asset,
56
42
  accountState,
@@ -60,7 +46,6 @@ export class GetFeeResolver {
60
46
  amount,
61
47
  customFee,
62
48
  isSendAll,
63
- inscriptionIds,
64
49
  taprootInputWitnessSize,
65
50
  })
66
51
  return { fee, unspendableFee, extraFeeData }
@@ -75,7 +60,6 @@ export class GetFeeResolver {
75
60
  amount,
76
61
  customFee,
77
62
  isSendAll,
78
- inscriptionIds,
79
63
  taprootInputWitnessSize,
80
64
  }) => {
81
65
  assert(asset, 'asset must be provided')
@@ -87,12 +71,6 @@ export class GetFeeResolver {
87
71
  const utxos = getUtxos({ accountState, asset })
88
72
  const unconfirmedTxAncestor = getUnconfirmedTxAncestorMap({ accountState })
89
73
 
90
- const ordinalsUtxos = getOrdinalsUtxos({ accountState, asset })
91
-
92
- const transferOrdinalsUtxos = inscriptionIds
93
- ? getTransferOrdinalsUtxos({ inscriptionIds, ordinalsUtxos })
94
- : undefined
95
-
96
74
  const usableUtxos = getUsableUtxos({
97
75
  asset,
98
76
  utxos,
@@ -110,8 +88,6 @@ export class GetFeeResolver {
110
88
  amount,
111
89
  feeRate: feePerKB,
112
90
  receiveAddress,
113
- transferOrdinalsUtxos,
114
- inscriptionIds,
115
91
  isSendAll,
116
92
  getFeeEstimator: this.#getFeeEstimator,
117
93
  allowUnconfirmedRbfEnabledUtxos: this.#allowUnconfirmedRbfEnabledUtxos,
@@ -11,11 +11,7 @@ const { sortBy } = lodash
11
11
 
12
12
  const MIN_RELAY_FEE = 1000
13
13
 
14
- const getBestReceiveAddresses = ({ asset, receiveAddress, inscriptionIds }) => {
15
- if (inscriptionIds) {
16
- return receiveAddress || 'P2TR'
17
- }
18
-
14
+ const getBestReceiveAddresses = ({ asset, receiveAddress }) => {
19
15
  if (receiveAddress === null) {
20
16
  return null
21
17
  }
@@ -38,20 +34,15 @@ export const selectUtxos = ({
38
34
  mustSpendUtxos,
39
35
  allowUnconfirmedRbfEnabledUtxos,
40
36
  unconfirmedTxAncestor,
41
- inscriptionIds, // for each inscription transfer, we need to calculate one more input and one more output
42
- transferOrdinalsUtxos, // to calculate the size of the input
43
37
  taprootInputWitnessSize,
44
38
  changeAddressType = 'P2PKH',
45
39
  }) => {
46
40
  const resolvedReceiveAddresses = getBestReceiveAddresses({
47
41
  asset,
48
42
  receiveAddress,
49
- inscriptionIds,
50
43
  })
51
44
 
52
- if (inscriptionIds) {
53
- receiveAddresses.push(...inscriptionIds.map(() => resolvedReceiveAddresses))
54
- } else if (receiveAddresses.length === 0) {
45
+ if (receiveAddresses.length === 0) {
55
46
  receiveAddresses.push(resolvedReceiveAddresses)
56
47
  }
57
48
 
@@ -66,7 +57,6 @@ export const selectUtxos = ({
66
57
  // We can only replace for a sendAll if only 1 replaceable tx and no unconfirmed utxos
67
58
  const confirmedUtxosArray = getConfirmedUtxos({ asset, utxos: usableUtxos }).toArray()
68
59
  const canReplace =
69
- !inscriptionIds &&
70
60
  !mustSpendUtxos &&
71
61
  !disableReplacement &&
72
62
  replaceableTxs &&
@@ -86,7 +76,6 @@ export const selectUtxos = ({
86
76
  }
87
77
 
88
78
  const replaceFeeEstimator = getFeeEstimator(asset, { feePerKB, unconfirmedTxAncestor })
89
- // how to avoid replace tx inputs when inputs are ordinals? !!!!
90
79
  const inputs = UtxoCollection.fromJSON(tx.data.inputs, { currency })
91
80
  const outputs = isSendAll
92
81
  ? tx.data.sent.map(({ address }) => address)
@@ -211,15 +200,10 @@ export const selectUtxos = ({
211
200
  selectedUtxosValue = selectedUtxosValue.add(newUtxo.value)
212
201
  }
213
202
 
214
- let selectedUtxos = (transferOrdinalsUtxos || UtxoCollection.createEmpty({ currency })).union(
215
- UtxoCollection.fromArray(selectedUtxosArray, { currency })
216
- ) // extremelly important, orden must be kept!!! ordinals utxos go first!!!
203
+ let selectedUtxos = UtxoCollection.fromArray(selectedUtxosArray, { currency })
217
204
 
218
205
  // start figuring out fees
219
- const outputs =
220
- amount.isZero && !inscriptionIds
221
- ? [changeAddressType]
222
- : [...receiveAddresses, changeAddressType]
206
+ const outputs = amount.isZero ? [changeAddressType] : [...receiveAddresses, changeAddressType]
223
207
 
224
208
  let fee = feeEstimator({ inputs: selectedUtxos, outputs, taprootInputWitnessSize })
225
209
 
@@ -259,8 +243,6 @@ export const getUtxosData = ({
259
243
  disableReplacement,
260
244
  mustSpendUtxos,
261
245
  allowUnconfirmedRbfEnabledUtxos,
262
- inscriptionIds,
263
- transferOrdinalsUtxos,
264
246
  unconfirmedTxAncestor,
265
247
  utxosDescendingOrder,
266
248
  taprootInputWitnessSize,
@@ -283,8 +265,6 @@ export const getUtxosData = ({
283
265
  mustSpendUtxos,
284
266
  allowUnconfirmedRbfEnabledUtxos,
285
267
  unconfirmedTxAncestor,
286
- inscriptionIds,
287
- transferOrdinalsUtxos,
288
268
  utxosDescendingOrder,
289
269
  taprootInputWitnessSize,
290
270
  changeAddressType,
@@ -7,13 +7,7 @@ import { parseCurrency, serializeCurrency } from '../fee/fee-utils.js'
7
7
  import { selectUtxos } from '../fee/utxo-selector.js'
8
8
  import { findUnconfirmedSentRbfTxs } from '../tx-utils.js'
9
9
  import { getUnconfirmedTxAncestorMap } from '../unconfirmed-ancestor-data.js'
10
- import {
11
- getInscriptionIds,
12
- getOrdinalsUtxos,
13
- getTransferOrdinalsUtxos,
14
- getUsableUtxos,
15
- getUtxos,
16
- } from '../utxos-utils.js'
10
+ import { getUsableUtxos, getUtxos } from '../utxos-utils.js'
17
11
  import { createInputs, createOutput, getBlockHeight, getNonWitnessTxs } from './tx-create-utils.js'
18
12
 
19
13
  async function createUnsignedTx({
@@ -64,13 +58,11 @@ const transferHandler = {
64
58
  customFee,
65
59
  isSendAll,
66
60
  bumpTxId,
67
- nft,
68
61
  isExchange,
69
62
  isRbfAllowed,
70
63
  taprootInputWitnessSize,
71
64
  accountState,
72
65
  feeData,
73
- ordinalsEnabled,
74
66
  getFeeEstimator,
75
67
  allowUnconfirmedRbfEnabledUtxos,
76
68
  utxosDescendingOrder,
@@ -84,18 +76,11 @@ const transferHandler = {
84
76
  const blockHeight = providedBlockHeight || (await getBlockHeight({ assetName, insightClient }))
85
77
 
86
78
  const rbfEnabled =
87
- providedRbfEnabled || (updatedFeeData.rbfEnabled && !isExchange && isRbfAllowed && !nft)
88
-
89
- const inscriptionIds = getInscriptionIds({ nft })
90
-
91
- assert(
92
- ordinalsEnabled || !inscriptionIds,
93
- 'inscriptions cannot be sent when ordinalsEnabled=false '
94
- )
79
+ providedRbfEnabled || (updatedFeeData.rbfEnabled && !isExchange && isRbfAllowed)
95
80
 
96
81
  const shuffle = (list) => {
97
82
  // Using full lodash.shuffle notation so it can be mocked with spyOn in tests
98
- return inscriptionIds ? list : lodash.shuffle(list) // don't shuffle when sending ordinal!!!!
83
+ return lodash.shuffle(list)
99
84
  }
100
85
 
101
86
  assert(
@@ -107,11 +92,6 @@ const transferHandler = {
107
92
  'should not be called without either a receiving toAddress or to bump a tx'
108
93
  )
109
94
 
110
- if (inscriptionIds) {
111
- assert(!bumpTxId, 'only inscriptionIds or bumpTxId must be provided')
112
- assert(toAddress, 'toAddress must be provided when sending ordinals')
113
- }
114
-
115
95
  const useCashAddress = asset.address.isCashAddress?.(toAddress)
116
96
 
117
97
  const changeAddress = multipleAddressesEnabled
@@ -120,11 +100,6 @@ const transferHandler = {
120
100
 
121
101
  const txSet = await assetClientInterface.getTxLog({ assetName, walletAccount })
122
102
 
123
- const currentOrdinalsUtxos = getOrdinalsUtxos({ accountState, asset })
124
- const transferOrdinalsUtxos = inscriptionIds
125
- ? getTransferOrdinalsUtxos({ inscriptionIds, ordinalsUtxos: currentOrdinalsUtxos })
126
- : undefined
127
-
128
103
  const unconfirmedTxAncestor = getUnconfirmedTxAncestorMap({ accountState })
129
104
  const usableUtxos = getUsableUtxos({
130
105
  asset,
@@ -160,14 +135,14 @@ const transferHandler = {
160
135
  }
161
136
  }
162
137
 
163
- const sendAmount = bumpTxId || transferOrdinalsUtxos ? asset.currency.ZERO : amount
138
+ const sendAmount = bumpTxId ? asset.currency.ZERO : amount
164
139
  const receiveAddress = bumpTxId
165
140
  ? replaceableTxs.length > 0
166
141
  ? null
167
142
  : changeAddressType
168
143
  : processedAddress
169
144
  const feeRate = updatedFeeData.feePerKB
170
- const resolvedIsSendAll = (!rbfEnabled && feePerKB) || transferOrdinalsUtxos ? false : isSendAll
145
+ const resolvedIsSendAll = !rbfEnabled && feePerKB ? false : isSendAll
171
146
 
172
147
  let { selectedUtxos, fee, replaceTx } = selectUtxos({
173
148
  asset,
@@ -182,8 +157,6 @@ const transferHandler = {
182
157
  mustSpendUtxos: utxosToBump,
183
158
  allowUnconfirmedRbfEnabledUtxos,
184
159
  unconfirmedTxAncestor,
185
- inscriptionIds,
186
- transferOrdinalsUtxos,
187
160
  utxosDescendingOrder,
188
161
  taprootInputWitnessSize,
189
162
  changeAddressType,
@@ -207,7 +180,6 @@ const transferHandler = {
207
180
  return { ...to, amount: serializeCurrency(to.amount, asset.currency) }
208
181
  })
209
182
  selectedUtxos = selectedUtxos.union(
210
- // how to avoid replace tx inputs when inputs are ordinals? !!!!
211
183
  UtxoCollection.fromJSON(replaceTx.data.inputs, { currency: asset.currency })
212
184
  )
213
185
  }
@@ -225,16 +197,8 @@ const transferHandler = {
225
197
  // Send output
226
198
  let sendOutput
227
199
  if (processedAddress) {
228
- if (transferOrdinalsUtxos) {
229
- outputs.push(
230
- ...transferOrdinalsUtxos
231
- .toArray()
232
- .map((ordinalUtxo) => createOutput(assetName, processedAddress, ordinalUtxo.value))
233
- )
234
- } else {
235
- sendOutput = createOutput(assetName, processedAddress, sendAmount)
236
- outputs.push(sendOutput)
237
- }
200
+ sendOutput = createOutput(assetName, processedAddress, sendAmount)
201
+ outputs.push(sendOutput)
238
202
  }
239
203
 
240
204
  const totalAmount = replaceTx
@@ -244,10 +208,7 @@ const transferHandler = {
244
208
  )
245
209
  : sendAmount
246
210
 
247
- const change = selectedUtxos.value
248
- .sub(totalAmount)
249
- .sub(transferOrdinalsUtxos?.value || asset.currency.ZERO)
250
- .sub(fee)
211
+ const change = selectedUtxos.value.sub(totalAmount).sub(fee)
251
212
  const dust = getChangeDustValue(asset)
252
213
  let ourAddress = replaceTx?.data?.changeAddress || changeAddress
253
214
  if (asset.address.toLegacyAddress) {
@@ -282,30 +243,28 @@ const transferHandler = {
282
243
  })
283
244
 
284
245
  return {
285
- amount,
286
- change,
287
- totalAmount,
288
- currentOrdinalsUtxos,
289
- inscriptionIds,
290
- address: processedAddress,
291
- ourAddress,
292
- receiveAddress,
293
- sendAmount,
294
- fee,
295
- usableUtxos,
296
- selectedUtxos,
297
- transferOrdinalsUtxos,
298
- replaceTx,
299
- sendOutput,
300
- changeOutput,
301
246
  unsignedTx,
247
+ fee,
248
+ metadata: {
249
+ amount,
250
+ change,
251
+ totalAmount,
252
+ address: processedAddress,
253
+ ourAddress,
254
+ receiveAddress,
255
+ sendAmount,
256
+ usableUtxos,
257
+ selectedUtxos,
258
+ replaceTx,
259
+ sendOutput,
260
+ changeOutput,
261
+ },
302
262
  }
303
263
  },
304
264
  }
305
265
 
306
266
  export const createTxFactory =
307
267
  ({
308
- ordinalsEnabled,
309
268
  getFeeEstimator,
310
269
  allowUnconfirmedRbfEnabledUtxos,
311
270
  utxosDescendingOrder,
@@ -325,7 +284,6 @@ export const createTxFactory =
325
284
  customFee,
326
285
  isSendAll,
327
286
  bumpTxId,
328
- nft,
329
287
  isExchange,
330
288
  isRbfAllowed,
331
289
  taprootInputWitnessSize,
@@ -351,13 +309,11 @@ export const createTxFactory =
351
309
  customFee,
352
310
  isSendAll,
353
311
  bumpTxId,
354
- nft,
355
312
  isExchange,
356
313
  isRbfAllowed,
357
314
  taprootInputWitnessSize,
358
315
  accountState,
359
316
  feeData,
360
- ordinalsEnabled,
361
317
  getFeeEstimator,
362
318
  allowUnconfirmedRbfEnabledUtxos,
363
319
  utxosDescendingOrder,
@@ -0,0 +1,48 @@
1
+ import { retry } from '@exodus/simple-retry'
2
+
3
+ /**
4
+ * Broadcast a signed transaction to the Bitcoin network with retry logic
5
+ * @param {Object} params
6
+ * @param {Object} params.asset - The asset object
7
+ * @param {Buffer} params.rawTx - The raw signed transaction
8
+ * @returns {Promise<void>}
9
+ * @throws {Error} With finalError=true for non-retryable errors
10
+ */
11
+ export async function broadcastTransaction({ asset, rawTx }) {
12
+ const broadcastTxWithRetry = retry(
13
+ async (rawTxHex) => {
14
+ try {
15
+ return await asset.api.broadcastTx(rawTxHex)
16
+ } catch (e) {
17
+ // Mark certain errors as final (non-retryable)
18
+ 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)
25
+ ) {
26
+ e.finalError = true
27
+ }
28
+
29
+ throw e
30
+ }
31
+ },
32
+ { delayTimesMs: ['10s'] }
33
+ )
34
+
35
+ const rawTxHex = rawTx.toString('hex')
36
+
37
+ try {
38
+ await broadcastTxWithRetry(rawTxHex)
39
+ } catch (err) {
40
+ if (err.message.includes('txn-already-in-mempool')) {
41
+ // Not an error, transaction is already broadcast
42
+ console.log('Transaction is already in the mempool.')
43
+ return
44
+ }
45
+
46
+ throw err
47
+ }
48
+ }
@@ -1,11 +1,10 @@
1
1
  import * as defaultBitcoinjsLib from '@exodus/bitcoinjs'
2
- import { Address } from '@exodus/models'
3
- import { retry } from '@exodus/simple-retry'
4
2
  import assert from 'minimalistic-assert'
5
3
 
6
- import { serializeCurrency } from '../fee/fee-utils.js'
7
4
  import { createTxFactory } from '../tx-create/create-tx.js'
8
5
  import { getBlockHeight } from '../tx-create/tx-create-utils.js'
6
+ import { broadcastTransaction } from './broadcast-tx.js'
7
+ import { updateAccountState, updateTransactionLog } from './update-state.js'
9
8
 
10
9
  const getSize = (tx) => {
11
10
  if (typeof tx.size === 'number') return tx.size
@@ -109,13 +108,11 @@ const getPrepareSendTransaction = async ({
109
108
  changeAddressType,
110
109
  getFeeEstimator,
111
110
  options,
112
- ordinalsEnabled,
113
111
  rbfEnabled,
114
112
  utxosDescendingOrder,
115
113
  walletAccount,
116
114
  }) => {
117
115
  const createTx = createTxFactory({
118
- ordinalsEnabled,
119
116
  getFeeEstimator,
120
117
  allowUnconfirmedRbfEnabledUtxos,
121
118
  utxosDescendingOrder,
@@ -142,21 +139,20 @@ export const createAndBroadcastTXFactory =
142
139
  getFeeEstimator,
143
140
  getSizeAndChangeScript = getSizeAndChangeScriptFactory(), // for decred customizations
144
141
  allowUnconfirmedRbfEnabledUtxos,
145
- ordinalsEnabled = false,
146
142
  utxosDescendingOrder,
147
143
  assetClientInterface,
148
144
  changeAddressType,
149
145
  }) =>
150
146
  async ({ asset, walletAccount, address, amount, options }) => {
151
147
  // Prepare transaction
152
- const { bumpTxId, nft, isExchange, isRbfAllowed = true } = options
148
+ const { bumpTxId, isExchange, isRbfAllowed = true } = options
153
149
 
154
150
  const assetName = asset.name
155
151
  const feeData = await assetClientInterface.getFeeConfig({ assetName })
156
152
  const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
157
153
  const insightClient = asset.baseAsset.insightClient
158
154
 
159
- const rbfEnabled = feeData.rbfEnabled && !isExchange && isRbfAllowed && !nft
155
+ const rbfEnabled = feeData.rbfEnabled && !isExchange && isRbfAllowed
160
156
 
161
157
  // blockHeight
162
158
  const blockHeight = await getBlockHeight({ assetName, insightClient })
@@ -171,31 +167,17 @@ export const createAndBroadcastTXFactory =
171
167
  changeAddressType,
172
168
  getFeeEstimator,
173
169
  options,
174
- ordinalsEnabled,
175
170
  rbfEnabled,
176
171
  utxosDescendingOrder,
177
172
  walletAccount,
178
173
  })
179
- const {
180
- change,
181
- totalAmount,
182
- currentOrdinalsUtxos,
183
- inscriptionIds,
184
- ourAddress,
185
- receiveAddress,
186
- sendAmount,
187
- fee,
188
- usableUtxos,
189
- selectedUtxos,
190
- transferOrdinalsUtxos,
191
- replaceTx,
192
- sendOutput,
193
- changeOutput,
194
- unsignedTx,
195
- } = transactionDescriptor
174
+
175
+ const { unsignedTx, fee, metadata } = transactionDescriptor
176
+ const { sendAmount, usableUtxos, replaceTx, sendOutput, changeOutput } = metadata
177
+
196
178
  const outputs = unsignedTx.txData.outputs
197
179
 
198
- address = transactionDescriptor.address
180
+ address = metadata.address
199
181
 
200
182
  // Sign transaction
201
183
  const { rawTx, txId, tx } = await signTransaction({
@@ -206,44 +188,18 @@ export const createAndBroadcastTXFactory =
206
188
  })
207
189
 
208
190
  // Broadcast transaction
209
- const broadcastTxWithRetry = retry(
210
- async (rawTx) => {
211
- try {
212
- return await asset.api.broadcastTx(rawTx)
213
- } catch (e) {
214
- if (
215
- /missing inputs/i.test(e.message) ||
216
- /absurdly-high-fee/.test(e.message) ||
217
- /too-long-mempool-chain/.test(e.message) ||
218
- /txn-mempool-conflict/.test(e.message) ||
219
- /tx-size/.test(e.message) ||
220
- /txn-already-in-mempool/.test(e.message)
221
- ) {
222
- e.finalError = true
223
- }
224
-
225
- throw e
226
- }
227
- },
228
- { delayTimesMs: ['10s'] }
229
- )
230
-
231
191
  try {
232
- await broadcastTxWithRetry(rawTx.toString('hex'))
192
+ await broadcastTransaction({ asset, rawTx })
233
193
  } catch (err) {
234
- if (err.message.includes('txn-already-in-mempool')) {
235
- // It's not an error, we must ignore it.
236
- console.log('Transaction is already in the mempool.')
237
- } else if (/insight broadcast http error.*missing inputs/i.test(err.message)) {
194
+ if (/insight broadcast http error.*missing inputs/i.test(err.message)) {
238
195
  err.txInfo = JSON.stringify({
239
196
  amount: sendAmount.toDefaultString({ unit: true }),
240
- fee: ((fee && fee.toDefaultString({ unit: true })) || 0).toString(), // todo why does 0 not have a unit? Is default unit ok here?
197
+ fee: ((fee && fee.toDefaultString({ unit: true })) || 0).toString(),
241
198
  allUtxos: usableUtxos.toJSON(),
242
199
  })
243
- throw err
244
- } else {
245
- throw err
246
200
  }
201
+
202
+ throw err
247
203
  }
248
204
 
249
205
  function findUtxoIndex(output) {
@@ -263,135 +219,35 @@ export const createAndBroadcastTXFactory =
263
219
  const changeUtxoIndex = findUtxoIndex(changeOutput)
264
220
  const sendUtxoIndex = findUtxoIndex(sendOutput)
265
221
 
266
- const { script, size } = getSizeAndChangeScript({ assetName, tx, rawTx, changeUtxoIndex, txId })
267
-
268
- // for ordinals, used to allow users spending change utxos even when unconfirmed and ordinals are unknown
269
- const knownBalanceUtxoIds = accountState.knownBalanceUtxoIds || []
270
- let remainingUtxos = usableUtxos.difference(selectedUtxos)
271
- if (changeUtxoIndex !== -1) {
272
- const address = Address.create(ourAddress.address, ourAddress.meta)
273
- const changeUtxo = {
274
- txId,
275
- address,
276
- vout: changeUtxoIndex,
277
- script,
278
- value: change,
279
- confirmations: 0,
280
- rbfEnabled,
281
- }
282
-
283
- knownBalanceUtxoIds.push(`${changeUtxo.txId}:${changeUtxo.vout}`.toLowerCase())
284
- remainingUtxos = remainingUtxos.addUtxo(changeUtxo)
285
- }
286
-
287
- if (replaceTx) {
288
- remainingUtxos = remainingUtxos.difference(remainingUtxos.getTxIdUtxos(replaceTx.txId))
289
- }
290
-
291
- const remainingOrdinalsUtxos = transferOrdinalsUtxos
292
- ? currentOrdinalsUtxos.difference(transferOrdinalsUtxos)
293
- : currentOrdinalsUtxos
294
-
295
- await assetClientInterface.updateAccountState({
296
- assetName,
297
- walletAccount,
298
- newData: {
299
- utxos: remainingUtxos,
300
- ordinalsUtxos: remainingOrdinalsUtxos,
301
- knownBalanceUtxoIds,
302
- },
303
- })
304
-
305
- const config = await assetClientInterface.getAssetConfig?.({
222
+ const { size } = await updateAccountState({
223
+ assetClientInterface,
306
224
  assetName,
307
225
  walletAccount,
226
+ accountState,
227
+ txId,
228
+ metadata,
229
+ tx,
230
+ rawTx,
231
+ changeUtxoIndex,
232
+ getSizeAndChangeScript,
233
+ rbfEnabled,
308
234
  })
309
- const walletAddressObjects = await assetClientInterface.getReceiveAddresses({
310
- walletAccount,
311
- assetName,
312
- multiAddressMode: config?.multiAddressMode ?? true,
313
- })
314
- // There are two cases of bumping, replacing or chaining a self-send.
315
- // If we have a bumpTxId, but we aren't replacing, then it is a self-send.
316
- const selfSend = bumpTxId
317
- ? !replaceTx
318
- : walletAddressObjects.some((receiveAddress) => String(receiveAddress) === String(address))
319
-
320
- const displayReceiveAddress = asset.address.displayAddress?.(receiveAddress) || receiveAddress
321
-
322
- const receivers = bumpTxId
323
- ? replaceTx
324
- ? replaceTx.data.sent
325
- : []
326
- : replaceTx
327
- ? [
328
- ...replaceTx.data.sent,
329
- { address: displayReceiveAddress, amount: serializeCurrency(amount, asset.currency) },
330
- ]
331
- : [{ address: displayReceiveAddress, amount: serializeCurrency(amount, asset.currency) }]
332
-
333
- const calculateCoinAmount = () => {
334
- if (selfSend) {
335
- return asset.currency.ZERO
336
- }
337
-
338
- if (nft) {
339
- return transferOrdinalsUtxos.value.abs().negate()
340
- }
341
-
342
- return totalAmount.abs().negate()
343
- }
344
235
 
345
- const coinAmount = calculateCoinAmount()
346
-
347
- await assetClientInterface.updateTxLogAndNotify({
348
- assetName: asset.name,
236
+ await updateTransactionLog({
237
+ asset,
238
+ assetClientInterface,
349
239
  walletAccount,
350
- txs: [
351
- {
352
- txId,
353
- confirmations: 0,
354
- coinAmount,
355
- coinName: asset.name,
356
- feeAmount: fee,
357
- feeCoinName: assetName,
358
- selfSend,
359
- data: {
360
- sent: selfSend ? [] : receivers,
361
- rbfEnabled,
362
- feePerKB: size ? fee.div(size / 1000).toBaseNumber() : undefined,
363
- changeAddress: changeOutput ? ourAddress : undefined,
364
- blockHeight,
365
- blocksSeen: 0,
366
- inputs: selectedUtxos.toJSON(),
367
- replacedTxId: replaceTx ? replaceTx.txId : undefined,
368
- nftId: nft ? `${assetName}:${nft.tokenId}` : undefined, // it allows BE to load the nft info while the nft is in transit
369
- inscriptionsIndexed: ordinalsEnabled ? true : undefined,
370
- sentInscriptions: inscriptionIds
371
- ? inscriptionIds.map((inscriptionId) => {
372
- return {
373
- inscriptionId,
374
- offset: 0,
375
- value: 0,
376
- }
377
- })
378
- : undefined,
379
- },
380
- },
381
- ],
240
+ txId,
241
+ fee,
242
+ metadata,
243
+ address,
244
+ amount,
245
+ bumpTxId,
246
+ size,
247
+ blockHeight,
248
+ rbfEnabled,
382
249
  })
383
250
 
384
- // If we are replacing the tx, add the replacedBy info to the previous tx to update UI
385
- // Also, clone the personal note and attach it to the new tx so it is not lost
386
- if (replaceTx) {
387
- replaceTx.data.replacedBy = txId
388
- await assetClientInterface.updateTxLogAndNotify({
389
- assetName,
390
- walletAccount,
391
- txs: [replaceTx],
392
- })
393
- }
394
-
395
251
  return {
396
252
  txId,
397
253
  sendUtxoIndex,
@@ -0,0 +1,188 @@
1
+ import { Address } from '@exodus/models'
2
+
3
+ import { serializeCurrency } from '../fee/fee-utils.js'
4
+
5
+ /**
6
+ * Update account state after transaction is broadcast
7
+ * @param {Object} params
8
+ * @param {Object} params.assetClientInterface - Asset client interface
9
+ * @param {string} params.assetName - Name of the asset
10
+ * @param {Object} params.walletAccount - Wallet account
11
+ * @param {Object} params.accountState - Current account state
12
+ * @param {string} params.txId - Transaction ID
13
+ * @param {Object} params.metadata - Transaction metadata
14
+ * @param {Object} params.tx - Signed transaction object
15
+ * @param {Buffer} params.rawTx - Raw transaction
16
+ * @param {number} params.changeUtxoIndex - Index of change output
17
+ * @param {Object} params.changeOutput - Change output details
18
+ * @param {Object} params.getSizeAndChangeScript - Function to get size and script
19
+ * @param {boolean} params.rbfEnabled - Whether RBF is enabled
20
+ */
21
+ export async function updateAccountState({
22
+ assetClientInterface,
23
+ assetName,
24
+ walletAccount,
25
+ accountState,
26
+ txId,
27
+ metadata,
28
+ tx,
29
+ rawTx,
30
+ changeUtxoIndex,
31
+ getSizeAndChangeScript,
32
+ rbfEnabled,
33
+ }) {
34
+ const { usableUtxos, selectedUtxos, replaceTx, change, ourAddress } = metadata
35
+
36
+ // Get change script and size
37
+ const { script, size } = getSizeAndChangeScript({
38
+ assetName,
39
+ tx,
40
+ rawTx,
41
+ changeUtxoIndex,
42
+ txId,
43
+ })
44
+
45
+ // Update remaining UTXOs
46
+ const knownBalanceUtxoIds = accountState.knownBalanceUtxoIds || []
47
+ let remainingUtxos = usableUtxos.difference(selectedUtxos)
48
+
49
+ // Add change UTXO if present
50
+ if (changeUtxoIndex !== -1 && ourAddress) {
51
+ const address = Address.create(ourAddress.address || ourAddress, ourAddress.meta || {})
52
+ const changeUtxo = {
53
+ txId,
54
+ address,
55
+ vout: changeUtxoIndex,
56
+ script,
57
+ value: change,
58
+ confirmations: 0,
59
+ rbfEnabled,
60
+ }
61
+
62
+ knownBalanceUtxoIds.push(`${changeUtxo.txId}:${changeUtxo.vout}`.toLowerCase())
63
+ remainingUtxos = remainingUtxos.addUtxo(changeUtxo)
64
+ }
65
+
66
+ // Remove replaced transaction UTXOs if present
67
+ if (replaceTx) {
68
+ remainingUtxos = remainingUtxos.difference(remainingUtxos.getTxIdUtxos(replaceTx.txId))
69
+ }
70
+
71
+ // Update account state
72
+ await assetClientInterface.updateAccountState({
73
+ assetName,
74
+ walletAccount,
75
+ newData: {
76
+ utxos: remainingUtxos,
77
+ knownBalanceUtxoIds,
78
+ },
79
+ })
80
+
81
+ return { size }
82
+ }
83
+
84
+ /**
85
+ * Update transaction log with new transaction
86
+ * @param {Object} params
87
+ * @param {Object} params.asset - Asset object
88
+ * @param {Object} params.assetClientInterface - Asset client interface
89
+ * @param {Object} params.walletAccount - Wallet account
90
+ * @param {string} params.txId - Transaction ID
91
+ * @param {Object} params.fee - Transaction fee
92
+ * @param {Object} params.metadata - Transaction metadata
93
+ * @param {string} params.address - Recipient address
94
+ * @param {Object} params.amount - Transaction amount (for regular sends)
95
+ * @param {string} params.bumpTxId - ID of transaction being bumped (if applicable)
96
+ * @param {number} params.size - Transaction size
97
+ * @param {number} params.blockHeight - Block height
98
+ * @param {boolean} params.rbfEnabled - Whether RBF is enabled
99
+ */
100
+ export async function updateTransactionLog({
101
+ asset,
102
+ assetClientInterface,
103
+ walletAccount,
104
+ txId,
105
+ fee,
106
+ metadata,
107
+ address,
108
+ amount,
109
+ bumpTxId,
110
+ size,
111
+ blockHeight,
112
+ rbfEnabled,
113
+ }) {
114
+ const { totalAmount, selectedUtxos, replaceTx, changeOutput, ourAddress } = metadata
115
+ const assetName = asset.name
116
+
117
+ // Check if this is a self-send
118
+ const config = await assetClientInterface.getAssetConfig?.({
119
+ assetName,
120
+ walletAccount,
121
+ })
122
+
123
+ const walletAddressObjects = await assetClientInterface.getReceiveAddresses({
124
+ walletAccount,
125
+ assetName,
126
+ multiAddressMode: config?.multiAddressMode ?? true,
127
+ })
128
+
129
+ // There are two cases of bumping, replacing or chaining a self-send.
130
+ // If we have a bumpTxId, but we aren't replacing, then it is a self-send.
131
+ const selfSend = bumpTxId
132
+ ? !replaceTx
133
+ : walletAddressObjects.some((receiveAddress) => String(receiveAddress) === String(address))
134
+
135
+ const displayReceiveAddress = asset.address.displayAddress?.(address) || address
136
+
137
+ // Build receivers list
138
+ const receivers = bumpTxId
139
+ ? replaceTx
140
+ ? replaceTx.data.sent
141
+ : []
142
+ : replaceTx
143
+ ? [
144
+ ...replaceTx.data.sent,
145
+ { address: displayReceiveAddress, amount: serializeCurrency(amount, asset.currency) },
146
+ ]
147
+ : [{ address: displayReceiveAddress, amount: serializeCurrency(amount, asset.currency) }]
148
+
149
+ // Calculate coin amount
150
+ const coinAmount = selfSend ? asset.currency.ZERO : totalAmount.abs().negate()
151
+
152
+ // Update transaction log
153
+ await assetClientInterface.updateTxLogAndNotify({
154
+ assetName,
155
+ walletAccount,
156
+ txs: [
157
+ {
158
+ txId,
159
+ confirmations: 0,
160
+ coinAmount,
161
+ coinName: asset.name,
162
+ feeAmount: fee,
163
+ feeCoinName: assetName,
164
+ selfSend,
165
+ data: {
166
+ sent: selfSend ? [] : receivers,
167
+ rbfEnabled,
168
+ feePerKB: size ? fee.div(size / 1000).toBaseNumber() : undefined,
169
+ changeAddress: changeOutput ? ourAddress : undefined,
170
+ blockHeight,
171
+ blocksSeen: 0,
172
+ inputs: selectedUtxos.toJSON(),
173
+ replacedTxId: replaceTx ? replaceTx.txId : undefined,
174
+ },
175
+ },
176
+ ],
177
+ })
178
+
179
+ // If replacing a transaction, update the old one
180
+ if (replaceTx) {
181
+ replaceTx.data.replacedBy = txId
182
+ await assetClientInterface.updateTxLogAndNotify({
183
+ assetName,
184
+ walletAccount,
185
+ txs: [replaceTx],
186
+ })
187
+ }
188
+ }