@exodus/bitcoin-api 4.6.0 → 4.7.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,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.7.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.6.0...@exodus/bitcoin-api@4.7.0) (2025-11-17)
7
+
8
+
9
+ ### Features
10
+
11
+
12
+ * feat: remove giant metadata from createTx result (#6860)
13
+
14
+
15
+
6
16
  ## [4.6.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.2.1...@exodus/bitcoin-api@4.6.0) (2025-11-14)
7
17
 
8
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/bitcoin-api",
3
- "version": "4.6.0",
3
+ "version": "4.7.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",
@@ -60,5 +60,5 @@
60
60
  "type": "git",
61
61
  "url": "git+https://github.com/ExodusMovement/assets.git"
62
62
  },
63
- "gitHead": "660b300377800da2edc9c250d61736b416db73d1"
63
+ "gitHead": "e281f3eae247487330a0ae16eb57372fa7982ca2"
64
64
  }
@@ -245,6 +245,7 @@ export async function createPsbtWithMetadata({
245
245
  addressPathsMap,
246
246
  purposeXPubs,
247
247
  allowedPurposes,
248
+ changeOutputIndex: metadata.changeOutputIndex,
248
249
  })
249
250
  psbt.addOutput(psbtOutput)
250
251
  })
@@ -254,5 +255,9 @@ export async function createPsbtWithMetadata({
254
255
  writePsbtOutputField(psbt, sendOutputIndex, SubType.OutputRole, 'primary')
255
256
  }
256
257
 
258
+ if (metadata.changeOutputIndex !== undefined) {
259
+ writePsbtOutputField(psbt, metadata.changeOutputIndex, SubType.OutputRole, 'change')
260
+ }
261
+
257
262
  return psbt
258
263
  }
@@ -1,5 +1,5 @@
1
1
  import { publicKeyToX } from '@exodus/crypto/secp256k1'
2
- import { UtxoCollection } from '@exodus/models'
2
+ import { Address, UtxoCollection } from '@exodus/models'
3
3
  import BipPath from 'bip32-path'
4
4
  import assert from 'minimalistic-assert'
5
5
 
@@ -70,7 +70,7 @@ function parseSingleInput({ txInput, psbtInput, index, asset, purposeXPubs, Tran
70
70
  throw new Error(`Input ${index} has no derivation path`)
71
71
  }
72
72
 
73
- input.address = { address, meta: { path: derivation.path, purpose } }
73
+ input.address = { address, meta: { path: derivation.path } }
74
74
  return input
75
75
  }
76
76
 
@@ -222,32 +222,22 @@ function getSelectedUtxos({ parsedInputs, utxos, asset, txSet }) {
222
222
  async function getChangeOutputData({ outputs, assetClientInterface, walletAccount, asset }) {
223
223
  let changeOutputData = null
224
224
  for (const [i, output] of outputs.entries()) {
225
- // Outputs marked “primary” are treated as sends even if they use one of our paths
226
- // (common for self‑sends, consolidations, etc.), so skip them during change detection.
227
- if (output.metadata?.outputRole === 'primary') {
225
+ if (output.metadata?.outputRole !== 'change') {
228
226
  continue
229
227
  }
230
228
 
231
- // We only treat addresses we can re-derive from the wallet xpub as potential
232
- // change outputs.
233
- if (!output.address?.meta?.path) {
234
- continue
235
- }
236
-
237
- // At this point we already know the output was derived by our wallet and it is
238
- // not explicitly flagged as a primary/send output, so we tentatively treat it as
239
- // change and confirm by re-deriving the on-chain address.
240
229
  if (changeOutputData) {
241
- // Multiple change outputs are a smell. This can happen if an external PSBT
242
- // marks a send output of the same wallet with its derivation path, so we fail fast instead of
243
- // silently mislabelling funds.
244
230
  throw new Error('Multiple change outputs are not allowed')
245
231
  }
246
232
 
233
+ if (!output.address?.meta?.path || !output.address?.meta?.purpose) {
234
+ throw new Error('Change output has no derivation path or purpose')
235
+ }
236
+
247
237
  const { path, purpose } = output.address.meta
248
238
  const [chainIndex, addressIndex] = BipPath.fromString(path).toPathArray()
249
239
 
250
- const verifyAddr = await assetClientInterface.getAddress({
240
+ let ourAddress = await assetClientInterface.getAddress({
251
241
  assetName: asset.name,
252
242
  walletAccount: walletAccount.toString(),
253
243
  purpose,
@@ -255,10 +245,15 @@ async function getChangeOutputData({ outputs, assetClientInterface, walletAccoun
255
245
  addressIndex,
256
246
  })
257
247
 
258
- if (verifyAddr.toString() === output.address.address) {
248
+ if (asset.address.toLegacyAddress) {
249
+ const legacyAddress = asset.address.toLegacyAddress(ourAddress.address)
250
+ ourAddress = Address.create(legacyAddress, ourAddress.meta)
251
+ }
252
+
253
+ if (ourAddress.toString() === output.address.address) {
259
254
  changeOutputData = {
260
- address: verifyAddr,
261
- amount: output.amount,
255
+ address: ourAddress,
256
+ amount: toBigIntAmount(output.amount),
262
257
  index: i,
263
258
  }
264
259
  } else {
@@ -316,6 +311,17 @@ function buildAddressPathsMap(selectedUtxos, changeOutputData) {
316
311
  return addressPathsMap
317
312
  }
318
313
 
314
+ function buildOutputAddressPurposesMap(outputs) {
315
+ const map = Object.create(null)
316
+ for (const output of outputs) {
317
+ if (output.address?.meta?.purpose) {
318
+ map[output.address.address] = output.address.meta.purpose
319
+ }
320
+ }
321
+
322
+ return map
323
+ }
324
+
319
325
  function extractRawTransactions(parsedInputs) {
320
326
  const rawTxsData = parsedInputs
321
327
  .filter((parsedInput) => parsedInput.prevTxId)
@@ -392,8 +398,127 @@ export async function parsePsbt({
392
398
  }
393
399
  }
394
400
 
401
+ function toBigIntAmount(value) {
402
+ return Buffer.isBuffer(value) ? value.readBigInt64LE(0) : BigInt(value)
403
+ }
404
+
405
+ function parseUnsignedTx({ unsignedTx }) {
406
+ const { txData, txMeta } = unsignedTx
407
+ assert(txData, 'txData is required')
408
+ assert(txMeta, 'txMeta is required')
409
+
410
+ const {
411
+ addressPathsMap = Object.create(null),
412
+ outputAddressPurposesMap = Object.create(null),
413
+ changeOutputIndex,
414
+ rawTxs = [],
415
+ blockHeight,
416
+ sendOutputIndexes = [],
417
+ rbfEnabled = false,
418
+ bumpTxId,
419
+ txType,
420
+ } = txMeta
421
+
422
+ const rawTxMap = new Map(rawTxs.map((tx) => [tx.txId, tx.rawData]))
423
+
424
+ const inputs = (txData.inputs || []).map((input) =>
425
+ parseUnsignedInput({
426
+ input,
427
+ addressPathsMap,
428
+ rawTxMap,
429
+ })
430
+ )
431
+
432
+ const outputs = (txData.outputs || []).map((output, index) =>
433
+ parseUnsignedOutput({
434
+ index,
435
+ output,
436
+ addressPathsMap,
437
+ outputAddressPurposesMap,
438
+ sendOutputIndexes,
439
+ changeOutputIndex,
440
+ })
441
+ )
442
+
443
+ const fee = computeFee(inputs, outputs)
444
+
445
+ const globalMetadata = {
446
+ blockHeight,
447
+ rbfEnabled,
448
+ bumpTxId,
449
+ txType,
450
+ }
451
+
452
+ return { inputs, outputs, fee, globalMetadata }
453
+ }
454
+
455
+ function computeFee(inputs, outputs) {
456
+ const inputSum = inputs.reduce((sum, input) => sum + toBigIntAmount(input.value), BigInt(0))
457
+ const outputSum = outputs.reduce((sum, output) => sum + toBigIntAmount(output.amount), BigInt(0))
458
+ return inputSum - outputSum
459
+ }
460
+
461
+ function parseUnsignedInput({ input, addressPathsMap, rawTxMap }) {
462
+ const { txId, vout, sequence, value, script, address: addressString } = input
463
+
464
+ const path = addressPathsMap[addressString]
465
+
466
+ const parsedInput = {
467
+ txId,
468
+ vout,
469
+ sequence,
470
+ value,
471
+ script,
472
+ address: { address: addressString, meta: { path } },
473
+ }
474
+
475
+ const rawTxHex = rawTxMap.get(txId)
476
+ if (rawTxHex) {
477
+ parsedInput.prevTxHex = rawTxHex
478
+ parsedInput.prevTxId = txId
479
+ }
480
+
481
+ return parsedInput
482
+ }
483
+
484
+ function parseUnsignedOutput({
485
+ index,
486
+ output,
487
+ addressPathsMap,
488
+ outputAddressPurposesMap,
489
+ sendOutputIndexes,
490
+ changeOutputIndex,
491
+ }) {
492
+ const [addressString, amount] = output
493
+
494
+ const purpose = outputAddressPurposesMap[addressString]
495
+ const path = addressPathsMap[addressString]
496
+
497
+ const addressObject = { address: addressString }
498
+ if (purpose !== undefined) {
499
+ addressObject.meta = { purpose }
500
+ }
501
+
502
+ if (path !== undefined) {
503
+ addressObject.meta = { ...addressObject.meta, path }
504
+ }
505
+
506
+ const parsedOutput = {
507
+ amount,
508
+ address: addressObject,
509
+ }
510
+
511
+ if (sendOutputIndexes.includes(index)) {
512
+ parsedOutput.metadata = { outputRole: 'primary' }
513
+ } else if (changeOutputIndex === index) {
514
+ parsedOutput.metadata = { outputRole: 'change' }
515
+ }
516
+
517
+ return parsedOutput
518
+ }
519
+
395
520
  export async function extractTransactionContext({
396
- psbtBuffer,
521
+ unsignedTx,
397
522
  asset,
398
523
  assetClientInterface,
399
524
  walletAccount,
@@ -401,27 +526,31 @@ export async function extractTransactionContext({
401
526
  Psbt,
402
527
  Transaction,
403
528
  }) {
404
- assert(psbtBuffer, 'psbtBuffer is required')
529
+ assert(unsignedTx, 'unsignedTx is required')
405
530
  assert(asset, 'asset is required')
406
531
  assert(assetClientInterface, 'assetClientInterface is required')
407
532
  assert(walletAccount, 'walletAccount is required')
408
- assert(Psbt, 'Psbt is required')
409
- assert(Transaction, 'Transaction is required')
533
+
534
+ const psbtBuffer = unsignedTx?.txData?.psbtBuffer
535
+
536
+ const parsed = psbtBuffer
537
+ ? await parsePsbt({
538
+ psbtBuffer,
539
+ asset,
540
+ assetClientInterface,
541
+ walletAccount,
542
+ allowedPurposes,
543
+ Psbt,
544
+ Transaction,
545
+ })
546
+ : parseUnsignedTx({ unsignedTx })
410
547
 
411
548
  const {
412
549
  inputs: parsedInputs,
413
550
  outputs: parsedOutputs,
414
551
  fee: calculatedFee,
415
552
  globalMetadata,
416
- } = await parsePsbt({
417
- psbtBuffer,
418
- asset,
419
- assetClientInterface,
420
- walletAccount,
421
- allowedPurposes,
422
- Psbt,
423
- Transaction,
424
- })
553
+ } = parsed
425
554
 
426
555
  const changeOutputData = await getChangeOutputData({
427
556
  outputs: parsedOutputs,
@@ -482,6 +611,7 @@ export async function extractTransactionContext({
482
611
  )
483
612
 
484
613
  const addressPathsMap = buildAddressPathsMap(selectedUtxos, changeOutputData)
614
+ const outputAddressPurposesMap = buildOutputAddressPurposesMap(parsedOutputs)
485
615
  const rawTxs = extractRawTransactions(parsedInputs)
486
616
 
487
617
  return {
@@ -492,6 +622,7 @@ export async function extractTransactionContext({
492
622
  fee: asset.currency.baseUnit(calculatedFee),
493
623
  sendOutputIndexes: primaryOutputIndexes,
494
624
  changeOutputIndex: changeOutputData?.index,
625
+ outputAddressPurposesMap,
495
626
  sendAmounts,
496
627
  changeAmount,
497
628
  totalSendAmount,
@@ -505,5 +636,9 @@ export async function extractTransactionContext({
505
636
  changeOutput: changeOutputData ? outputs[changeOutputData.index] : undefined,
506
637
  rbfEnabled: globalMetadata.rbfEnabled,
507
638
  blockHeight: globalMetadata.blockHeight,
639
+ bumpTxId: globalMetadata.bumpTxId,
640
+ txType: globalMetadata.txType,
641
+ useCashAddress: unsignedTx?.txMeta?.useCashAddress,
642
+ psbtBuffer,
508
643
  }
509
644
  }
@@ -174,6 +174,7 @@ function createTransactionOutputs({
174
174
  const assetName = asset.name
175
175
  let outputs = []
176
176
  let sendOutput
177
+ let adjustedFee
177
178
 
178
179
  // Add existing outputs from replacement transaction
179
180
  if (replaceTx) {
@@ -196,8 +197,7 @@ function createTransactionOutputs({
196
197
  )
197
198
  : sendAmount
198
199
 
199
- // Handle change output
200
- const { changeOutput, adjustedFee, changeAddressKeypath, ourAddress } = createChangeOutput({
200
+ const changeOutputData = createChangeOutput({
201
201
  selectedUtxos,
202
202
  totalAmount,
203
203
  fee,
@@ -206,18 +206,23 @@ function createTransactionOutputs({
206
206
  asset,
207
207
  })
208
208
 
209
- if (changeOutput) {
210
- outputs.push(changeOutput)
209
+ if (changeOutputData) {
210
+ outputs.push(changeOutputData.changeOutput)
211
+ adjustedFee = fee
212
+ } else {
213
+ // If we don't have enough for a change output, then all remaining dust is just added to fee
214
+ const changeAmount = selectedUtxos.value.sub(totalAmount).sub(fee)
215
+ adjustedFee = fee.add(changeAmount)
211
216
  }
212
217
 
213
218
  return {
214
219
  outputs: replaceTx ? outputs : shuffle(outputs),
215
220
  sendOutput,
216
- changeOutput,
217
- totalAmount,
221
+ ourAddress: changeOutputData?.ourAddress,
222
+ changeOutput: changeOutputData?.changeOutput,
223
+ changeAddressPath: changeOutputData?.changeAddressPath,
224
+ changeAddressPurpose: changeOutputData?.changeAddressPurpose,
218
225
  adjustedFee,
219
- changeAddressKeypath,
220
- ourAddress,
221
226
  }
222
227
  }
223
228
 
@@ -233,7 +238,7 @@ function createChangeOutput({ selectedUtxos, totalAmount, fee, replaceTx, change
233
238
  ourAddress = Address.create(legacyAddress, ourAddress.meta)
234
239
  }
235
240
 
236
- // Create change output if above dust threshold
241
+ // Create change output if above dust threshold.
237
242
  if (change.gte(dust)) {
238
243
  const changeOutput = createOutput(
239
244
  asset.name,
@@ -241,22 +246,16 @@ function createChangeOutput({ selectedUtxos, totalAmount, fee, replaceTx, change
241
246
  change
242
247
  )
243
248
 
244
- // Return the keypath for hardware wallet change detection
249
+ // Return the path for hardware wallet change detection
245
250
  return {
246
- changeOutput,
247
- adjustedFee: fee,
248
- changeAddressKeypath: ourAddress.meta.path,
249
251
  ourAddress,
252
+ changeOutput,
253
+ changeAddressPath: ourAddress.meta.path,
254
+ changeAddressPurpose: ourAddress.meta.purpose,
250
255
  }
251
256
  }
252
257
 
253
- // Add dust to fee if not enough for change output
254
- return {
255
- changeOutput: undefined,
256
- adjustedFee: fee.add(change),
257
- changeAddressKeypath: undefined,
258
- ourAddress,
259
- }
258
+ return null
260
259
  }
261
260
 
262
261
  async function createUnsignedTx({
@@ -275,6 +274,7 @@ async function createUnsignedTx({
275
274
  txType = 'transfer',
276
275
  sendOutputIndex,
277
276
  changeOutputIndex,
277
+ outputAddressPurposesMap,
278
278
  allowedPurposes,
279
279
  Psbt,
280
280
  Transaction,
@@ -291,6 +291,13 @@ async function createUnsignedTx({
291
291
  addressPathsMap,
292
292
  blockHeight,
293
293
  rawTxs: nonWitnessTxs,
294
+ sendOutputIndexes:
295
+ sendOutputIndex === undefined || sendOutputIndex === null ? [] : [sendOutputIndex],
296
+ changeOutputIndex,
297
+ outputAddressPurposesMap,
298
+ rbfEnabled,
299
+ bumpTxId,
300
+ txType,
294
301
  },
295
302
  }
296
303
 
@@ -450,10 +457,10 @@ const transferHandler = {
450
457
  outputs,
451
458
  sendOutput,
452
459
  changeOutput,
453
- totalAmount,
454
- adjustedFee,
455
- changeAddressKeypath,
460
+ changeAddressPath,
461
+ changeAddressPurpose,
456
462
  ourAddress,
463
+ adjustedFee,
457
464
  } = createTransactionOutputs({
458
465
  replaceTx: processedReplaceTx,
459
466
  processedAddress,
@@ -464,11 +471,15 @@ const transferHandler = {
464
471
  changeAddress: context.changeAddress,
465
472
  })
466
473
 
474
+ // Create a map of wallet's own output addresses and their purposes.
475
+ const outputAddressPurposesMap = Object.create(null)
476
+
467
477
  // Add the keypath of change address to support Trezor detect the change output.
468
478
  // Output is change and does not need approval from user which shows the strange address that user never seen.
469
- if (changeAddressKeypath) {
479
+ if (changeAddressPath && changeAddressPurpose) {
470
480
  const changeKey = ourAddress?.address ?? String(ourAddress)
471
- addressPathsMap[changeKey] = changeAddressKeypath
481
+ addressPathsMap[changeKey] = changeAddressPath
482
+ outputAddressPurposesMap[changeKey] = changeAddressPurpose
472
483
  }
473
484
 
474
485
  // Create unsigned transaction
@@ -477,6 +488,7 @@ const transferHandler = {
477
488
  outputs,
478
489
  useCashAddress,
479
490
  addressPathsMap,
491
+ outputAddressPurposesMap,
480
492
  blockHeight: context.blockHeight,
481
493
  asset,
482
494
  selectedUtxos,
@@ -493,26 +505,7 @@ const transferHandler = {
493
505
  Transaction,
494
506
  })
495
507
 
496
- return {
497
- unsignedTx,
498
- metadata: {
499
- fee: adjustedFee,
500
- amount,
501
- change: selectedUtxos.value.sub(totalAmount).sub(adjustedFee),
502
- totalAmount,
503
- address: processedAddress,
504
- ourAddress,
505
- receiveAddress: utxoParams.receiveAddress,
506
- sendAmount: utxoParams.sendAmount,
507
- usableUtxos: context.usableUtxos,
508
- selectedUtxos,
509
- replaceTx: processedReplaceTx,
510
- sendOutput,
511
- changeOutput,
512
- blockHeight: context.blockHeight,
513
- rbfEnabled: context.rbfEnabled,
514
- },
515
- }
508
+ return { unsignedTx, fee: adjustedFee }
516
509
  },
517
510
  }
518
511
 
@@ -99,65 +99,21 @@ export async function signTransaction({
99
99
  return { rawTx, txId, tx }
100
100
  }
101
101
 
102
- const getPrepareSendTransaction = async ({
103
- address,
104
- allowUnconfirmedRbfEnabledUtxos,
105
- amount,
106
- asset,
107
- assetClientInterface,
108
- changeAddressType,
109
- getFeeEstimator,
110
- options,
111
- utxosDescendingOrder,
112
- walletAccount,
113
- allowedPurposes,
114
- Psbt,
115
- Transaction,
116
- }) => {
117
- const createTx = createTxFactory({
118
- getFeeEstimator,
119
- allowUnconfirmedRbfEnabledUtxos,
120
- utxosDescendingOrder,
121
- assetClientInterface,
122
- changeAddressType,
123
- allowedPurposes,
124
- Psbt,
125
- Transaction,
126
- })
127
-
128
- // Set default values for options
129
- const { isRbfAllowed = true, ...restOptions } = options || Object.create(null)
130
-
131
- return createTx({
132
- asset,
133
- walletAccount,
134
- type: 'transfer',
135
- toAddress: address,
136
- amount,
137
- isRbfAllowed,
138
- ...restOptions,
139
- })
140
- }
141
-
142
- function toTransactionDescriptor({ txContext, psbtBuffer }) {
102
+ function getTransferUnsignedTx(txContext) {
143
103
  const {
144
104
  inputs,
145
105
  outputs,
106
+ psbtBuffer,
107
+ useCashAddress,
146
108
  addressPathsMap,
109
+ outputAddressPurposesMap,
147
110
  blockHeight,
148
111
  rawTxs,
149
- fee,
150
- totalSendAmount,
151
- changeAmount,
152
- totalAmount,
153
- primaryAddresses,
154
- ourAddress,
155
- usableUtxos,
156
- selectedUtxos,
157
- replaceTx,
158
- sendOutputs,
159
- changeOutput,
112
+ txType,
160
113
  rbfEnabled,
114
+ bumpTxId,
115
+ changeOutputIndex,
116
+ sendOutputIndexes,
161
117
  } = txContext
162
118
 
163
119
  return {
@@ -168,27 +124,47 @@ function toTransactionDescriptor({ txContext, psbtBuffer }) {
168
124
  psbtBuffer,
169
125
  },
170
126
  txMeta: {
127
+ useCashAddress,
171
128
  addressPathsMap,
129
+ outputAddressPurposesMap,
172
130
  blockHeight,
173
131
  rawTxs,
132
+ txType,
133
+ rbfEnabled,
134
+ bumpTxId,
135
+ changeOutputIndex,
136
+ sendOutputIndexes,
174
137
  },
175
138
  },
176
- metadata: {
177
- fee,
178
- amount: totalSendAmount.isZero ? undefined : totalSendAmount,
179
- sendAmount: totalSendAmount,
180
- change: changeAmount,
181
- totalAmount,
182
- address: primaryAddresses[0]?.address,
183
- ourAddress,
184
- usableUtxos,
185
- selectedUtxos,
186
- replaceTx,
187
- sendOutput: sendOutputs[0],
188
- changeOutput,
189
- rbfEnabled,
190
- blockHeight,
191
- },
139
+ }
140
+ }
141
+
142
+ // Get additional transaction metadata that are not part of the unsignedTx.txMeta
143
+ function getExtendedTxMeta(txContext) {
144
+ const {
145
+ fee,
146
+ totalSendAmount,
147
+ changeAmount,
148
+ totalAmount,
149
+ primaryAddresses,
150
+ ourAddress,
151
+ usableUtxos,
152
+ selectedUtxos,
153
+ replaceTx,
154
+ changeOutput,
155
+ } = txContext
156
+
157
+ return {
158
+ fee,
159
+ sendAmount: totalSendAmount,
160
+ changeAmount,
161
+ totalAmount,
162
+ toAddress: primaryAddresses[0]?.address,
163
+ ourAddress,
164
+ usableUtxos,
165
+ selectedUtxos,
166
+ replaceTx,
167
+ changeOutput,
192
168
  }
193
169
  }
194
170
 
@@ -206,53 +182,77 @@ export const createAndBroadcastTXFactory =
206
182
  Psbt = DefaultPsbt,
207
183
  Transaction = DefaultTransaction,
208
184
  }) =>
209
- async ({ asset, walletAccount, address, amount, ...options }) => {
210
- // Prepare transaction
211
- const { bumpTxId } = options
212
-
185
+ async ({
186
+ asset,
187
+ walletAccount,
188
+ address,
189
+ amount,
190
+ multipleAddressesEnabled,
191
+ feePerKB,
192
+ customFee,
193
+ isSendAll,
194
+ bumpTxId: bumpTxIdProvided,
195
+ isExchange,
196
+ isRbfAllowed = true,
197
+ taprootInputWitnessSize,
198
+ }) => {
213
199
  const assetName = asset.name
214
200
  const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
215
201
 
216
- let transactionDescriptor = await getPrepareSendTransaction({
217
- address,
202
+ const createTx = createTxFactory({
203
+ getFeeEstimator,
218
204
  allowUnconfirmedRbfEnabledUtxos,
205
+ utxosDescendingOrder,
206
+ assetClientInterface,
207
+ changeAddressType,
208
+ allowedPurposes,
209
+ Psbt,
210
+ Transaction,
211
+ })
212
+
213
+ const { unsignedTx: unsignedTxByCreateTx } = await createTx({
214
+ asset,
215
+ walletAccount,
216
+ type: 'transfer',
217
+ toAddress: address,
219
218
  amount,
219
+ isRbfAllowed,
220
+ multipleAddressesEnabled,
221
+ feePerKB,
222
+ customFee,
223
+ isSendAll,
224
+ bumpTxId: bumpTxIdProvided,
225
+ isExchange,
226
+ taprootInputWitnessSize,
227
+ })
228
+
229
+ const txContext = await extractTransactionContext({
230
+ unsignedTx: unsignedTxByCreateTx,
220
231
  asset,
221
232
  assetClientInterface,
222
- changeAddressType,
223
- getFeeEstimator,
224
- options,
225
- utxosDescendingOrder,
226
233
  walletAccount,
227
234
  allowedPurposes,
228
235
  Psbt,
229
236
  Transaction,
230
237
  })
231
238
 
232
- // If we already created a PSBT for Bitcoin, hydrate the full transaction
233
- // context from it before signing.
234
- let unsignedTx, metadata
235
- if (transactionDescriptor.unsignedTx?.txData?.psbtBuffer) {
236
- const psbtBuffer = transactionDescriptor.unsignedTx.txData.psbtBuffer
237
- const txContext = await extractTransactionContext({
238
- psbtBuffer,
239
- asset,
240
- assetClientInterface,
241
- walletAccount,
242
- allowedPurposes,
243
- Psbt,
244
- Transaction,
245
- })
246
- transactionDescriptor = toTransactionDescriptor({ txContext, psbtBuffer })
247
- unsignedTx = transactionDescriptor.unsignedTx
248
- metadata = transactionDescriptor.metadata
249
- } else {
250
- // Legacy/non-PSBT flows stick with the original descriptor shape.
251
- unsignedTx = transactionDescriptor.unsignedTx
252
- metadata = transactionDescriptor.metadata
253
- }
239
+ const { unsignedTx } = getTransferUnsignedTx(txContext)
240
+
241
+ const {
242
+ fee,
243
+ sendAmount,
244
+ changeAmount,
245
+ totalAmount,
246
+ toAddress,
247
+ ourAddress,
248
+ usableUtxos,
249
+ selectedUtxos,
250
+ replaceTx,
251
+ changeOutput,
252
+ } = getExtendedTxMeta(txContext)
254
253
 
255
- const { sendAmount, usableUtxos, replaceTx, sendOutput, changeOutput } = metadata
254
+ const { sendOutputIndexes, changeOutputIndex, bumpTxId, rbfEnabled, blockHeight } =
255
+ unsignedTx.txMeta
256
256
 
257
257
  // Sign transaction
258
258
  const { rawTx, txId, tx } = await signTransaction({
@@ -269,7 +269,7 @@ export const createAndBroadcastTXFactory =
269
269
  if (/insight broadcast http error.*missing inputs/i.test(err.message)) {
270
270
  err.txInfo = JSON.stringify({
271
271
  amount: sendAmount.toDefaultString({ unit: true }),
272
- fee: ((metadata.fee && metadata.fee.toDefaultString({ unit: true })) || 0).toString(),
272
+ fee: ((fee && fee.toDefaultString({ unit: true })) || 0).toString(),
273
273
  allUtxos: usableUtxos.toJSON(),
274
274
  })
275
275
  }
@@ -277,34 +277,31 @@ export const createAndBroadcastTXFactory =
277
277
  throw err
278
278
  }
279
279
 
280
- function findUtxoIndex(output) {
281
- let utxoIndex = -1
282
- if (output) {
283
- for (const [i, [address, amount]] of unsignedTx.txData.outputs.entries()) {
284
- if (output[0] === address && output[1] === amount) {
285
- utxoIndex = i
286
- break
287
- }
288
- }
289
- }
290
-
291
- return utxoIndex
292
- }
280
+ const changeUtxoIndex = changeOutputIndex ?? -1
281
+ const sendUtxoIndex = sendOutputIndexes?.[0] ?? -1
293
282
 
294
- const changeUtxoIndex = findUtxoIndex(changeOutput)
295
- const sendUtxoIndex = findUtxoIndex(sendOutput)
283
+ const { script, size } = getSizeAndChangeScript({
284
+ assetName,
285
+ tx,
286
+ rawTx,
287
+ changeUtxoIndex,
288
+ txId,
289
+ })
296
290
 
297
- const { size } = await updateAccountState({
291
+ await updateAccountState({
298
292
  assetClientInterface,
299
293
  assetName,
300
294
  walletAccount,
301
295
  accountState,
302
296
  txId,
303
- metadata,
304
- tx,
305
- rawTx,
306
297
  changeUtxoIndex,
307
- getSizeAndChangeScript,
298
+ script,
299
+ usableUtxos,
300
+ selectedUtxos,
301
+ replaceTx,
302
+ changeAmount,
303
+ ourAddress,
304
+ rbfEnabled,
308
305
  })
309
306
 
310
307
  await updateTransactionLog({
@@ -312,7 +309,16 @@ export const createAndBroadcastTXFactory =
312
309
  assetClientInterface,
313
310
  walletAccount,
314
311
  txId,
315
- metadata,
312
+ totalAmount,
313
+ selectedUtxos,
314
+ replaceTx,
315
+ changeOutput,
316
+ ourAddress,
317
+ fee,
318
+ blockHeight,
319
+ rbfEnabled,
320
+ toAddress,
321
+ sendAmount,
316
322
  bumpTxId,
317
323
  size,
318
324
  })
@@ -2,44 +2,21 @@ import { Address } from '@exodus/models'
2
2
 
3
3
  import { serializeCurrency } from '../fee/fee-utils.js'
4
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
- */
20
5
  export async function updateAccountState({
21
6
  assetClientInterface,
22
7
  assetName,
23
8
  walletAccount,
24
9
  accountState,
25
10
  txId,
26
- metadata,
27
- tx,
28
- rawTx,
29
11
  changeUtxoIndex,
30
- getSizeAndChangeScript,
12
+ script,
13
+ usableUtxos,
14
+ selectedUtxos,
15
+ replaceTx,
16
+ changeAmount,
17
+ ourAddress,
18
+ rbfEnabled,
31
19
  }) {
32
- const { usableUtxos, selectedUtxos, replaceTx, change, ourAddress, rbfEnabled } = metadata
33
-
34
- // Get change script and size
35
- const { script, size } = getSizeAndChangeScript({
36
- assetName,
37
- tx,
38
- rawTx,
39
- changeUtxoIndex,
40
- txId,
41
- })
42
-
43
20
  // Update remaining UTXOs
44
21
  const knownBalanceUtxoIds = accountState.knownBalanceUtxoIds || []
45
22
  let remainingUtxos = usableUtxos.difference(selectedUtxos)
@@ -52,7 +29,7 @@ export async function updateAccountState({
52
29
  address,
53
30
  vout: changeUtxoIndex,
54
31
  script,
55
- value: change,
32
+ value: changeAmount,
56
33
  confirmations: 0,
57
34
  rbfEnabled,
58
35
  }
@@ -75,42 +52,26 @@ export async function updateAccountState({
75
52
  knownBalanceUtxoIds,
76
53
  },
77
54
  })
78
-
79
- return { size }
80
55
  }
81
56
 
82
- /**
83
- * Update transaction log with new transaction
84
- * @param {Object} params
85
- * @param {Object} params.asset - Asset object
86
- * @param {Object} params.assetClientInterface - Asset client interface
87
- * @param {Object} params.walletAccount - Wallet account
88
- * @param {string} params.txId - Transaction ID
89
- * @param {Object} params.metadata - Transaction metadata
90
- * @param {string} params.bumpTxId - ID of transaction being bumped (if applicable)
91
- * @param {number} params.size - Transaction size
92
- */
93
57
  export async function updateTransactionLog({
94
58
  asset,
95
59
  assetClientInterface,
96
60
  walletAccount,
97
61
  txId,
98
- metadata,
99
62
  bumpTxId,
100
63
  size,
64
+ totalAmount,
65
+ selectedUtxos,
66
+ replaceTx,
67
+ changeOutput,
68
+ ourAddress,
69
+ fee,
70
+ blockHeight,
71
+ rbfEnabled,
72
+ toAddress,
73
+ sendAmount,
101
74
  }) {
102
- const {
103
- totalAmount,
104
- selectedUtxos,
105
- replaceTx,
106
- changeOutput,
107
- ourAddress,
108
- fee,
109
- blockHeight,
110
- rbfEnabled,
111
- address,
112
- amount,
113
- } = metadata
114
75
  const assetName = asset.name
115
76
 
116
77
  // Check if this is a self-send
@@ -129,9 +90,11 @@ export async function updateTransactionLog({
129
90
  // If we have a bumpTxId, but we aren't replacing, then it is a self-send.
130
91
  const selfSend = bumpTxId
131
92
  ? !replaceTx
132
- : walletAddressObjects.some((receiveAddress) => String(receiveAddress) === String(address))
93
+ : walletAddressObjects.some((receiveAddress) => String(receiveAddress) === String(toAddress))
133
94
 
134
- const displayReceiveAddress = asset.address.displayAddress?.(address) || address
95
+ const displayReceiveAddress = asset.address.displayAddress?.(toAddress) || toAddress
96
+
97
+ const amountToSerialize = sendAmount.isZero ? undefined : sendAmount
135
98
 
136
99
  // Build receivers list
137
100
  const receivers = bumpTxId
@@ -141,9 +104,17 @@ export async function updateTransactionLog({
141
104
  : replaceTx
142
105
  ? [
143
106
  ...replaceTx.data.sent,
144
- { address: displayReceiveAddress, amount: serializeCurrency(amount, asset.currency) },
107
+ {
108
+ address: displayReceiveAddress,
109
+ amount: serializeCurrency(amountToSerialize, asset.currency),
110
+ },
111
+ ]
112
+ : [
113
+ {
114
+ address: displayReceiveAddress,
115
+ amount: serializeCurrency(amountToSerialize, asset.currency),
116
+ },
145
117
  ]
146
- : [{ address: displayReceiveAddress, amount: serializeCurrency(amount, asset.currency) }]
147
118
 
148
119
  // Calculate coin amount
149
120
  const coinAmount = selfSend ? asset.currency.ZERO : totalAmount.abs().negate()
@@ -177,11 +148,17 @@ export async function updateTransactionLog({
177
148
 
178
149
  // If replacing a transaction, update the old one
179
150
  if (replaceTx) {
180
- replaceTx.data.replacedBy = txId
151
+ const updatedReplaceTx = {
152
+ ...replaceTx,
153
+ data: {
154
+ ...replaceTx.data,
155
+ replacedBy: txId,
156
+ },
157
+ }
181
158
  await assetClientInterface.updateTxLogAndNotify({
182
159
  assetName,
183
160
  walletAccount,
184
- txs: [replaceTx],
161
+ txs: [updatedReplaceTx],
185
162
  })
186
163
  }
187
164
  }