@exodus/bitcoin-api 4.5.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,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.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
+
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)
17
+
18
+
19
+ ### Features
20
+
21
+
22
+ * feat: add PSBT builder infrastructure (#6822)
23
+
24
+ * feat: add PSBT parser functionality (#6823)
25
+
26
+ * feat: integrate PSBT support and legacy chain index (#6819)
27
+
28
+ * feat: integrate PSBT support and legacy chain index (#6819) (#6829)
29
+
30
+
31
+
6
32
  ## [4.5.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.2.1...@exodus/bitcoin-api@4.5.0) (2025-11-12)
7
33
 
8
34
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/bitcoin-api",
3
- "version": "4.5.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": "482aa1dda8d9273c4f1a477ffcef2310f1df9884"
63
+ "gitHead": "e281f3eae247487330a0ae16eb57372fa7982ca2"
64
64
  }
@@ -1,11 +1,17 @@
1
- import { payments, Psbt, Transaction } from '@exodus/bitcoinjs'
1
+ import { payments } from '@exodus/bitcoinjs'
2
2
  import { publicKeyToX } from '@exodus/crypto/secp256k1'
3
3
  import assert from 'minimalistic-assert'
4
4
 
5
5
  import { SubType, writePsbtGlobalField, writePsbtOutputField } from './psbt-proprietary-types.js'
6
- import { getAddressType, getPurposeXPubs, validatePurpose } from './psbt-utils.js'
7
-
8
- function canParseTx(rawTxBuffer) {
6
+ import {
7
+ getAddressType,
8
+ getPurposeXPubs,
9
+ setPsbtVersionIfNotBitcoin,
10
+ validatePurpose,
11
+ } from './psbt-utils.js'
12
+ import { getMaximumFeeRate } from './tx-sign/maximum-fee-rates.js'
13
+
14
+ function canParseTx(rawTxBuffer, Transaction) {
9
15
  try {
10
16
  Transaction.fromBuffer(rawTxBuffer)
11
17
  return true
@@ -70,7 +76,14 @@ function writeGlobalMetadata(psbt, metadata) {
70
76
  }
71
77
  }
72
78
 
73
- function createPsbtInput({ input, asset, addressPathsMap, purposeXPubs, nonWitnessTxs }) {
79
+ function createPsbtInput({
80
+ input,
81
+ asset,
82
+ addressPathsMap,
83
+ purposeXPubs,
84
+ nonWitnessTxs,
85
+ Transaction,
86
+ }) {
74
87
  const psbtInput = {
75
88
  hash: input.txId,
76
89
  index: input.vout,
@@ -111,7 +124,7 @@ function createPsbtInput({ input, asset, addressPathsMap, purposeXPubs, nonWitne
111
124
  )
112
125
 
113
126
  const rawTxBuffer = Buffer.from(rawTx.rawData, 'hex')
114
- if (canParseTx(rawTxBuffer)) {
127
+ if (canParseTx(rawTxBuffer, Transaction)) {
115
128
  psbtInput.nonWitnessUtxo = rawTxBuffer
116
129
  } else {
117
130
  // bitcoinjs can’t parse a handful of edge-case transactions (Litecoin MWEB, odd
@@ -181,6 +194,8 @@ export async function createPsbtWithMetadata({
181
194
  addressPathsMap,
182
195
  metadata,
183
196
  allowedPurposes,
197
+ Psbt,
198
+ Transaction,
184
199
  }) {
185
200
  assert(inputs, 'inputs is required')
186
201
  assert(outputs, 'outputs is required')
@@ -189,8 +204,16 @@ export async function createPsbtWithMetadata({
189
204
  assert(walletAccount, 'walletAccount is required')
190
205
  assert(addressPathsMap, 'addressPathsMap is required')
191
206
  assert(metadata, 'metadata is required')
207
+ assert(Psbt, 'Psbt is required')
208
+ assert(Transaction, 'Transaction is required')
209
+ const psbtOptions = { network: asset.coinInfo.toBitcoinJS() }
210
+ const maximumFeeRate = getMaximumFeeRate(asset.name)
211
+ if (maximumFeeRate !== undefined) {
212
+ psbtOptions.maximumFeeRate = maximumFeeRate
213
+ }
192
214
 
193
- const psbt = new Psbt({ network: asset.coinInfo.toBitcoinJS() })
215
+ const psbt = new Psbt(psbtOptions)
216
+ setPsbtVersionIfNotBitcoin(psbt, asset.name)
194
217
 
195
218
  const purposeXPubs = await getPurposeXPubs({
196
219
  assetClientInterface,
@@ -209,6 +232,7 @@ export async function createPsbtWithMetadata({
209
232
  purposeXPubs,
210
233
  nonWitnessTxs,
211
234
  allowedPurposes,
235
+ Transaction,
212
236
  })
213
237
  psbt.addInput(psbtInput)
214
238
  }
@@ -221,6 +245,7 @@ export async function createPsbtWithMetadata({
221
245
  addressPathsMap,
222
246
  purposeXPubs,
223
247
  allowedPurposes,
248
+ changeOutputIndex: metadata.changeOutputIndex,
224
249
  })
225
250
  psbt.addOutput(psbtOutput)
226
251
  })
@@ -230,5 +255,9 @@ export async function createPsbtWithMetadata({
230
255
  writePsbtOutputField(psbt, sendOutputIndex, SubType.OutputRole, 'primary')
231
256
  }
232
257
 
233
- return psbt.toBuffer()
258
+ if (metadata.changeOutputIndex !== undefined) {
259
+ writePsbtOutputField(psbt, metadata.changeOutputIndex, SubType.OutputRole, 'change')
260
+ }
261
+
262
+ return psbt
234
263
  }
@@ -1,6 +1,5 @@
1
- import { Psbt, Transaction } from '@exodus/bitcoinjs'
2
1
  import { publicKeyToX } from '@exodus/crypto/secp256k1'
3
- import { UtxoCollection } from '@exodus/models'
2
+ import { Address, UtxoCollection } from '@exodus/models'
4
3
  import BipPath from 'bip32-path'
5
4
  import assert from 'minimalistic-assert'
6
5
 
@@ -11,7 +10,7 @@ import { findUnconfirmedSentRbfTxs } from './tx-utils.js'
11
10
  import { getUnconfirmedTxAncestorMap } from './unconfirmed-ancestor-data.js'
12
11
  import { getUsableUtxos, getUtxos } from './utxos-utils.js'
13
12
 
14
- function extractInputUtxoData(psbtInput, txInput, index) {
13
+ function extractInputUtxoData(psbtInput, txInput, index, Transaction) {
15
14
  if (psbtInput.nonWitnessUtxo) {
16
15
  const prevTx = Transaction.fromBuffer(psbtInput.nonWitnessUtxo)
17
16
  const prevOutput = prevTx.outs[txInput.index]
@@ -34,14 +33,14 @@ function extractInputUtxoData(psbtInput, txInput, index) {
34
33
  throw new Error(`Input ${index} has no witnessUtxo or nonWitnessUtxo`)
35
34
  }
36
35
 
37
- function parseSingleInput({ txInput, psbtInput, index, asset, purposeXPubs }) {
36
+ function parseSingleInput({ txInput, psbtInput, index, asset, purposeXPubs, Transaction }) {
38
37
  const input = {
39
38
  txId: Buffer.from(txInput.hash).reverse().toString('hex'),
40
39
  vout: txInput.index,
41
40
  sequence: txInput.sequence,
42
41
  }
43
42
 
44
- const inputUtxoData = extractInputUtxoData(psbtInput, txInput, index)
43
+ const inputUtxoData = extractInputUtxoData(psbtInput, txInput, index, Transaction)
45
44
 
46
45
  input.value = inputUtxoData.value
47
46
  input.script = Buffer.from(inputUtxoData.scriptBuffer).toString('hex')
@@ -71,7 +70,7 @@ function parseSingleInput({ txInput, psbtInput, index, asset, purposeXPubs }) {
71
70
  throw new Error(`Input ${index} has no derivation path`)
72
71
  }
73
72
 
74
- input.address = { address, meta: { path: derivation.path, purpose } }
73
+ input.address = { address, meta: { path: derivation.path } }
75
74
  return input
76
75
  }
77
76
 
@@ -117,9 +116,15 @@ function readGlobalMetadata(psbt) {
117
116
  }
118
117
 
119
118
  function calculateFee(inputs, outputs) {
120
- const inputSum = inputs.reduce((sum, input) => sum + (input.value || 0), 0)
121
- const outputSum = outputs.reduce((sum, output) => sum + (output.amount || 0), 0)
122
- return inputSum - outputSum
119
+ const inputSum = inputs.reduce((sum, input) => {
120
+ const value = BigInt(input.value || 0)
121
+ return sum + value
122
+ }, BigInt(0))
123
+ const outputSum = outputs.reduce((sum, output) => {
124
+ const amount = BigInt(output.amount || 0)
125
+ return sum + amount
126
+ }, BigInt(0))
127
+ return inputSum - outputSum // Return BigInt directly
123
128
  }
124
129
 
125
130
  function getBip32Derivation(hdkey, bip32Derivations, ignoreY) {
@@ -217,32 +222,22 @@ function getSelectedUtxos({ parsedInputs, utxos, asset, txSet }) {
217
222
  async function getChangeOutputData({ outputs, assetClientInterface, walletAccount, asset }) {
218
223
  let changeOutputData = null
219
224
  for (const [i, output] of outputs.entries()) {
220
- // Outputs marked “primary” are treated as sends even if they use one of our paths
221
- // (common for self‑sends, consolidations, etc.), so skip them during change detection.
222
- if (output.metadata?.outputRole === 'primary') {
223
- continue
224
- }
225
-
226
- // We only treat addresses we can re-derive from the wallet xpub as potential
227
- // change outputs.
228
- if (!output.address?.meta?.path) {
225
+ if (output.metadata?.outputRole !== 'change') {
229
226
  continue
230
227
  }
231
228
 
232
- // At this point we already know the output was derived by our wallet and it is
233
- // not explicitly flagged as a primary/send output, so we tentatively treat it as
234
- // change and confirm by re-deriving the on-chain address.
235
229
  if (changeOutputData) {
236
- // Multiple change outputs are a smell. This can happen if an external PSBT
237
- // marks a send output of the same wallet with its derivation path, so we fail fast instead of
238
- // silently mislabelling funds.
239
230
  throw new Error('Multiple change outputs are not allowed')
240
231
  }
241
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
+
242
237
  const { path, purpose } = output.address.meta
243
238
  const [chainIndex, addressIndex] = BipPath.fromString(path).toPathArray()
244
239
 
245
- const verifyAddr = await assetClientInterface.getAddress({
240
+ let ourAddress = await assetClientInterface.getAddress({
246
241
  assetName: asset.name,
247
242
  walletAccount: walletAccount.toString(),
248
243
  purpose,
@@ -250,10 +245,15 @@ async function getChangeOutputData({ outputs, assetClientInterface, walletAccoun
250
245
  addressIndex,
251
246
  })
252
247
 
253
- 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) {
254
254
  changeOutputData = {
255
- address: verifyAddr,
256
- amount: output.amount,
255
+ address: ourAddress,
256
+ amount: toBigIntAmount(output.amount),
257
257
  index: i,
258
258
  }
259
259
  } else {
@@ -311,6 +311,17 @@ function buildAddressPathsMap(selectedUtxos, changeOutputData) {
311
311
  return addressPathsMap
312
312
  }
313
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
+
314
325
  function extractRawTransactions(parsedInputs) {
315
326
  const rawTxsData = parsedInputs
316
327
  .filter((parsedInput) => parsedInput.prevTxId)
@@ -329,11 +340,15 @@ export async function parsePsbt({
329
340
  assetClientInterface,
330
341
  walletAccount,
331
342
  allowedPurposes,
343
+ Psbt,
344
+ Transaction,
332
345
  }) {
333
346
  assert(psbtBuffer, 'psbtBuffer is required')
334
347
  assert(asset, 'asset is required')
335
348
  assert(assetClientInterface, 'assetClientInterface is required')
336
349
  assert(walletAccount, 'walletAccount is required')
350
+ assert(Psbt, 'Psbt is required')
351
+ assert(Transaction, 'Transaction is required')
337
352
 
338
353
  const purposeXPubs = await getPurposeXPubs({
339
354
  assetClientInterface,
@@ -353,6 +368,7 @@ export async function parsePsbt({
353
368
  asset,
354
369
  purposeXPubs,
355
370
  allowedPurposes,
371
+ Transaction,
356
372
  })
357
373
  inputs.push(input)
358
374
  }
@@ -382,30 +398,159 @@ export async function parsePsbt({
382
398
  }
383
399
  }
384
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
+
385
520
  export async function extractTransactionContext({
386
- psbtBuffer,
521
+ unsignedTx,
387
522
  asset,
388
523
  assetClientInterface,
389
524
  walletAccount,
390
525
  allowedPurposes,
526
+ Psbt,
527
+ Transaction,
391
528
  }) {
392
- assert(psbtBuffer, 'psbtBuffer is required')
529
+ assert(unsignedTx, 'unsignedTx is required')
393
530
  assert(asset, 'asset is required')
394
531
  assert(assetClientInterface, 'assetClientInterface is required')
395
532
  assert(walletAccount, 'walletAccount is required')
396
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 })
547
+
397
548
  const {
398
549
  inputs: parsedInputs,
399
550
  outputs: parsedOutputs,
400
551
  fee: calculatedFee,
401
552
  globalMetadata,
402
- } = await parsePsbt({
403
- psbtBuffer,
404
- asset,
405
- assetClientInterface,
406
- walletAccount,
407
- allowedPurposes,
408
- })
553
+ } = parsed
409
554
 
410
555
  const changeOutputData = await getChangeOutputData({
411
556
  outputs: parsedOutputs,
@@ -466,6 +611,7 @@ export async function extractTransactionContext({
466
611
  )
467
612
 
468
613
  const addressPathsMap = buildAddressPathsMap(selectedUtxos, changeOutputData)
614
+ const outputAddressPurposesMap = buildOutputAddressPurposesMap(parsedOutputs)
469
615
  const rawTxs = extractRawTransactions(parsedInputs)
470
616
 
471
617
  return {
@@ -476,6 +622,7 @@ export async function extractTransactionContext({
476
622
  fee: asset.currency.baseUnit(calculatedFee),
477
623
  sendOutputIndexes: primaryOutputIndexes,
478
624
  changeOutputIndex: changeOutputData?.index,
625
+ outputAddressPurposesMap,
479
626
  sendAmounts,
480
627
  changeAmount,
481
628
  totalSendAmount,
@@ -489,5 +636,9 @@ export async function extractTransactionContext({
489
636
  changeOutput: changeOutputData ? outputs[changeOutputData.index] : undefined,
490
637
  rbfEnabled: globalMetadata.rbfEnabled,
491
638
  blockHeight: globalMetadata.blockHeight,
639
+ bumpTxId: globalMetadata.bumpTxId,
640
+ txType: globalMetadata.txType,
641
+ useCashAddress: unsignedTx?.txMeta?.useCashAddress,
642
+ psbtBuffer,
492
643
  }
493
644
  }
package/src/psbt-utils.js CHANGED
@@ -131,3 +131,7 @@ export async function withUnsafeNonSegwit({ psbt, fn, unsafe = true }) {
131
131
  cache.__UNSAFE_SIGN_NONSEGWIT = prevValue
132
132
  }
133
133
  }
134
+
135
+ export function setPsbtVersionIfNotBitcoin(psbt, assetName) {
136
+ if (!['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)) psbt.setVersion(1)
137
+ }
@@ -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,7 +274,10 @@ async function createUnsignedTx({
275
274
  txType = 'transfer',
276
275
  sendOutputIndex,
277
276
  changeOutputIndex,
277
+ outputAddressPurposesMap,
278
278
  allowedPurposes,
279
+ Psbt,
280
+ Transaction,
279
281
  }) {
280
282
  const nonWitnessTxs = await getNonWitnessTxs(asset, selectedUtxos, insightClient)
281
283
 
@@ -289,14 +291,21 @@ async function createUnsignedTx({
289
291
  addressPathsMap,
290
292
  blockHeight,
291
293
  rawTxs: nonWitnessTxs,
294
+ sendOutputIndexes:
295
+ sendOutputIndex === undefined || sendOutputIndex === null ? [] : [sendOutputIndex],
296
+ changeOutputIndex,
297
+ outputAddressPurposesMap,
298
+ rbfEnabled,
299
+ bumpTxId,
300
+ txType,
292
301
  },
293
302
  }
294
303
 
295
- // Only attach PSBT metadata for Bitcoin transfer flows for now; support for other coins and
296
- // transaction types will come later.
297
- const isBitcoin = ['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(asset.name)
298
- if (isBitcoin) {
299
- const psbtBuffer = await createPsbtWithMetadata({
304
+ // Create PSBT for all UTXO coins that support it
305
+ // Note: Some coins (like BCash) may not use PSBT in their signing flow,
306
+ // but creating it here allows for a unified transaction flow
307
+ if (Psbt) {
308
+ const psbt = await createPsbtWithMetadata({
300
309
  inputs,
301
310
  outputs,
302
311
  asset,
@@ -313,9 +322,11 @@ async function createUnsignedTx({
313
322
  bumpTxId,
314
323
  blockHeight,
315
324
  },
325
+ Psbt,
326
+ Transaction,
316
327
  })
317
328
 
318
- result.txData.psbtBuffer = psbtBuffer
329
+ result.txData.psbtBuffer = psbt.toBuffer()
319
330
  }
320
331
 
321
332
  return result
@@ -347,6 +358,8 @@ const transferHandler = {
347
358
  assetClientInterface,
348
359
  changeAddressType,
349
360
  allowedPurposes,
361
+ Psbt,
362
+ Transaction,
350
363
  }) => {
351
364
  const assetName = asset.name
352
365
  const insightClient = asset.baseAsset.insightClient
@@ -444,10 +457,10 @@ const transferHandler = {
444
457
  outputs,
445
458
  sendOutput,
446
459
  changeOutput,
447
- totalAmount,
448
- adjustedFee,
449
- changeAddressKeypath,
460
+ changeAddressPath,
461
+ changeAddressPurpose,
450
462
  ourAddress,
463
+ adjustedFee,
451
464
  } = createTransactionOutputs({
452
465
  replaceTx: processedReplaceTx,
453
466
  processedAddress,
@@ -458,11 +471,15 @@ const transferHandler = {
458
471
  changeAddress: context.changeAddress,
459
472
  })
460
473
 
474
+ // Create a map of wallet's own output addresses and their purposes.
475
+ const outputAddressPurposesMap = Object.create(null)
476
+
461
477
  // Add the keypath of change address to support Trezor detect the change output.
462
478
  // Output is change and does not need approval from user which shows the strange address that user never seen.
463
- if (changeAddressKeypath) {
479
+ if (changeAddressPath && changeAddressPurpose) {
464
480
  const changeKey = ourAddress?.address ?? String(ourAddress)
465
- addressPathsMap[changeKey] = changeAddressKeypath
481
+ addressPathsMap[changeKey] = changeAddressPath
482
+ outputAddressPurposesMap[changeKey] = changeAddressPurpose
466
483
  }
467
484
 
468
485
  // Create unsigned transaction
@@ -471,6 +488,7 @@ const transferHandler = {
471
488
  outputs,
472
489
  useCashAddress,
473
490
  addressPathsMap,
491
+ outputAddressPurposesMap,
474
492
  blockHeight: context.blockHeight,
475
493
  asset,
476
494
  selectedUtxos,
@@ -483,28 +501,11 @@ const transferHandler = {
483
501
  sendOutputIndex: sendOutput ? outputs.indexOf(sendOutput) : undefined,
484
502
  changeOutputIndex: changeOutput ? outputs.indexOf(changeOutput) : undefined,
485
503
  allowedPurposes,
504
+ Psbt,
505
+ Transaction,
486
506
  })
487
507
 
488
- return {
489
- unsignedTx,
490
- metadata: {
491
- fee: adjustedFee,
492
- amount,
493
- change: selectedUtxos.value.sub(totalAmount).sub(adjustedFee),
494
- totalAmount,
495
- address: processedAddress,
496
- ourAddress,
497
- receiveAddress: utxoParams.receiveAddress,
498
- sendAmount: utxoParams.sendAmount,
499
- usableUtxos: context.usableUtxos,
500
- selectedUtxos,
501
- replaceTx: processedReplaceTx,
502
- sendOutput,
503
- changeOutput,
504
- blockHeight: context.blockHeight,
505
- rbfEnabled: context.rbfEnabled,
506
- },
507
- }
508
+ return { unsignedTx, fee: adjustedFee }
508
509
  },
509
510
  }
510
511
 
@@ -516,6 +517,8 @@ export const createTxFactory =
516
517
  assetClientInterface,
517
518
  changeAddressType,
518
519
  allowedPurposes,
520
+ Psbt,
521
+ Transaction,
519
522
  }) =>
520
523
  async ({
521
524
  asset,
@@ -562,5 +565,7 @@ export const createTxFactory =
562
565
  assetClientInterface,
563
566
  changeAddressType,
564
567
  allowedPurposes,
568
+ Psbt,
569
+ Transaction,
565
570
  })
566
571
  }
@@ -1,4 +1,5 @@
1
1
  import * as defaultBitcoinjsLib from '@exodus/bitcoinjs'
2
+ import { Psbt as DefaultPsbt, Transaction as DefaultTransaction } from '@exodus/bitcoinjs'
2
3
  import assert from 'minimalistic-assert'
3
4
 
4
5
  import { extractTransactionContext } from '../psbt-parser.js'
@@ -98,61 +99,21 @@ export async function signTransaction({
98
99
  return { rawTx, txId, tx }
99
100
  }
100
101
 
101
- const getPrepareSendTransaction = async ({
102
- address,
103
- allowUnconfirmedRbfEnabledUtxos,
104
- amount,
105
- asset,
106
- assetClientInterface,
107
- changeAddressType,
108
- getFeeEstimator,
109
- options,
110
- utxosDescendingOrder,
111
- walletAccount,
112
- allowedPurposes,
113
- }) => {
114
- const createTx = createTxFactory({
115
- getFeeEstimator,
116
- allowUnconfirmedRbfEnabledUtxos,
117
- utxosDescendingOrder,
118
- assetClientInterface,
119
- changeAddressType,
120
- allowedPurposes,
121
- })
122
-
123
- // Set default values for options
124
- const { isRbfAllowed = true, ...restOptions } = options || Object.create(null)
125
-
126
- return createTx({
127
- asset,
128
- walletAccount,
129
- type: 'transfer',
130
- toAddress: address,
131
- amount,
132
- isRbfAllowed,
133
- ...restOptions,
134
- })
135
- }
136
-
137
- function toTransactionDescriptor(txContext, psbtBuffer) {
102
+ function getTransferUnsignedTx(txContext) {
138
103
  const {
139
104
  inputs,
140
105
  outputs,
106
+ psbtBuffer,
107
+ useCashAddress,
141
108
  addressPathsMap,
109
+ outputAddressPurposesMap,
142
110
  blockHeight,
143
111
  rawTxs,
144
- fee,
145
- totalSendAmount,
146
- changeAmount,
147
- totalAmount,
148
- primaryAddresses,
149
- ourAddress,
150
- usableUtxos,
151
- selectedUtxos,
152
- replaceTx,
153
- sendOutputs,
154
- changeOutput,
112
+ txType,
155
113
  rbfEnabled,
114
+ bumpTxId,
115
+ changeOutputIndex,
116
+ sendOutputIndexes,
156
117
  } = txContext
157
118
 
158
119
  return {
@@ -163,27 +124,47 @@ function toTransactionDescriptor(txContext, psbtBuffer) {
163
124
  psbtBuffer,
164
125
  },
165
126
  txMeta: {
127
+ useCashAddress,
166
128
  addressPathsMap,
129
+ outputAddressPurposesMap,
167
130
  blockHeight,
168
131
  rawTxs,
132
+ txType,
133
+ rbfEnabled,
134
+ bumpTxId,
135
+ changeOutputIndex,
136
+ sendOutputIndexes,
169
137
  },
170
138
  },
171
- metadata: {
172
- fee,
173
- amount: totalSendAmount.isZero ? undefined : totalSendAmount,
174
- sendAmount: totalSendAmount,
175
- change: changeAmount,
176
- totalAmount,
177
- address: primaryAddresses[0]?.address,
178
- ourAddress,
179
- usableUtxos,
180
- selectedUtxos,
181
- replaceTx,
182
- sendOutput: sendOutputs[0],
183
- changeOutput,
184
- rbfEnabled,
185
- blockHeight,
186
- },
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,
187
168
  }
188
169
  }
189
170
 
@@ -198,50 +179,80 @@ export const createAndBroadcastTXFactory =
198
179
  assetClientInterface,
199
180
  changeAddressType,
200
181
  allowedPurposes,
182
+ Psbt = DefaultPsbt,
183
+ Transaction = DefaultTransaction,
201
184
  }) =>
202
- async ({ asset, walletAccount, address, amount, options }) => {
203
- // Prepare transaction
204
- const { bumpTxId } = options
205
-
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
+ }) => {
206
199
  const assetName = asset.name
207
200
  const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
208
201
 
209
- let transactionDescriptor = await getPrepareSendTransaction({
210
- address,
202
+ const createTx = createTxFactory({
203
+ getFeeEstimator,
211
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,
212
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,
213
231
  asset,
214
232
  assetClientInterface,
215
- changeAddressType,
216
- getFeeEstimator,
217
- options,
218
- utxosDescendingOrder,
219
233
  walletAccount,
220
234
  allowedPurposes,
235
+ Psbt,
236
+ Transaction,
221
237
  })
222
238
 
223
- // If we already created a PSBT for Bitcoin, hydrate the full transaction
224
- // context from it before signing.
225
- let unsignedTx, metadata
226
- if (transactionDescriptor.unsignedTx?.txData?.psbtBuffer) {
227
- const psbtBuffer = transactionDescriptor.unsignedTx.txData.psbtBuffer
228
- const txContext = await extractTransactionContext({
229
- psbtBuffer,
230
- asset,
231
- assetClientInterface,
232
- walletAccount,
233
- allowedPurposes,
234
- })
235
- transactionDescriptor = toTransactionDescriptor(txContext, psbtBuffer)
236
- unsignedTx = transactionDescriptor.unsignedTx
237
- metadata = transactionDescriptor.metadata
238
- } else {
239
- // Legacy/non-PSBT flows stick with the original descriptor shape.
240
- unsignedTx = transactionDescriptor.unsignedTx
241
- metadata = transactionDescriptor.metadata
242
- }
239
+ const { unsignedTx } = getTransferUnsignedTx(txContext)
243
240
 
244
- const { sendAmount, usableUtxos, replaceTx, sendOutput, changeOutput } = metadata
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)
253
+
254
+ const { sendOutputIndexes, changeOutputIndex, bumpTxId, rbfEnabled, blockHeight } =
255
+ unsignedTx.txMeta
245
256
 
246
257
  // Sign transaction
247
258
  const { rawTx, txId, tx } = await signTransaction({
@@ -258,7 +269,7 @@ export const createAndBroadcastTXFactory =
258
269
  if (/insight broadcast http error.*missing inputs/i.test(err.message)) {
259
270
  err.txInfo = JSON.stringify({
260
271
  amount: sendAmount.toDefaultString({ unit: true }),
261
- fee: ((metadata.fee && metadata.fee.toDefaultString({ unit: true })) || 0).toString(),
272
+ fee: ((fee && fee.toDefaultString({ unit: true })) || 0).toString(),
262
273
  allUtxos: usableUtxos.toJSON(),
263
274
  })
264
275
  }
@@ -266,34 +277,31 @@ export const createAndBroadcastTXFactory =
266
277
  throw err
267
278
  }
268
279
 
269
- function findUtxoIndex(output) {
270
- let utxoIndex = -1
271
- if (output) {
272
- for (const [i, [address, amount]] of unsignedTx.txData.outputs.entries()) {
273
- if (output[0] === address && output[1] === amount) {
274
- utxoIndex = i
275
- break
276
- }
277
- }
278
- }
279
-
280
- return utxoIndex
281
- }
280
+ const changeUtxoIndex = changeOutputIndex ?? -1
281
+ const sendUtxoIndex = sendOutputIndexes?.[0] ?? -1
282
282
 
283
- const changeUtxoIndex = findUtxoIndex(changeOutput)
284
- const sendUtxoIndex = findUtxoIndex(sendOutput)
283
+ const { script, size } = getSizeAndChangeScript({
284
+ assetName,
285
+ tx,
286
+ rawTx,
287
+ changeUtxoIndex,
288
+ txId,
289
+ })
285
290
 
286
- const { size } = await updateAccountState({
291
+ await updateAccountState({
287
292
  assetClientInterface,
288
293
  assetName,
289
294
  walletAccount,
290
295
  accountState,
291
296
  txId,
292
- metadata,
293
- tx,
294
- rawTx,
295
297
  changeUtxoIndex,
296
- getSizeAndChangeScript,
298
+ script,
299
+ usableUtxos,
300
+ selectedUtxos,
301
+ replaceTx,
302
+ changeAmount,
303
+ ourAddress,
304
+ rbfEnabled,
297
305
  })
298
306
 
299
307
  await updateTransactionLog({
@@ -301,7 +309,16 @@ export const createAndBroadcastTXFactory =
301
309
  assetClientInterface,
302
310
  walletAccount,
303
311
  txId,
304
- metadata,
312
+ totalAmount,
313
+ selectedUtxos,
314
+ replaceTx,
315
+ changeOutput,
316
+ ourAddress,
317
+ fee,
318
+ blockHeight,
319
+ rbfEnabled,
320
+ toAddress,
321
+ sendAmount,
305
322
  bumpTxId,
306
323
  size,
307
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
  }
@@ -27,3 +27,12 @@ export const serializeTx = ({ tx }) => {
27
27
  })),
28
28
  }
29
29
  }
30
+
31
+ export function shouldSkipFinalize(unsignedTx) {
32
+ const isExternalPsbt =
33
+ unsignedTx.txData.psbtBuffer &&
34
+ unsignedTx.txMeta.addressPathsMap &&
35
+ unsignedTx.txMeta.inputsToSign
36
+
37
+ return isExternalPsbt || unsignedTx.txMeta.returnPsbt
38
+ }
@@ -1,7 +1,7 @@
1
1
  import { Psbt as DefaultPsbt, Transaction as DefaultTransaction } from '@exodus/bitcoinjs'
2
2
  import assert from 'minimalistic-assert'
3
3
 
4
- import { extractTransaction } from './common.js'
4
+ import { extractTransaction, shouldSkipFinalize } from './common.js'
5
5
  import { createSignWithWallet } from './create-sign-with-wallet.js'
6
6
  import { createPrepareForSigning } from './default-prepare-for-signing.js'
7
7
 
@@ -57,11 +57,7 @@ export const signTxFactory = ({
57
57
  },
58
58
  })
59
59
 
60
- const isExternalPsbt =
61
- unsignedTx.txData.psbtBuffer &&
62
- unsignedTx.txMeta.addressPathsMap &&
63
- unsignedTx.txMeta.inputsToSign
64
- const skipFinalize = isExternalPsbt || unsignedTx.txMeta.returnPsbt
60
+ const skipFinalize = shouldSkipFinalize(unsignedTx)
65
61
  await signWithWallet(psbt, inputsToSign, skipFinalize)
66
62
  return extractTransaction({ psbt, skipFinalize })
67
63
  }
@@ -2,11 +2,8 @@ import { Psbt as DefaultPsbt, Transaction as DefaultTransaction } from '@exodus/
2
2
  import assert from 'minimalistic-assert'
3
3
 
4
4
  import { writePsbtBlockHeight } from '../psbt-proprietary-types.js'
5
-
6
- const _MAXIMUM_FEE_RATES = {
7
- qtumignition: 25_000,
8
- ravencoin: 1_000_000,
9
- }
5
+ import { setPsbtVersionIfNotBitcoin } from '../psbt-utils.js'
6
+ import { getMaximumFeeRate } from './maximum-fee-rates.js'
10
7
 
11
8
  /**
12
9
  * Factory function that create the prepareForSigning function for a bitcoin-like asset.
@@ -26,9 +23,19 @@ export function createPrepareForSigning({
26
23
 
27
24
  return ({ unsignedTx }) => {
28
25
  const networkInfo = coinInfo.toBitcoinJS()
26
+ const maximumFeeRate = getMaximumFeeRate(assetName)
27
+
28
+ const psbtOptions = { network: networkInfo }
29
+ if (maximumFeeRate !== undefined) {
30
+ psbtOptions.maximumFeeRate = maximumFeeRate
31
+ }
29
32
 
30
33
  if (unsignedTx.txData.psbtBuffer) {
31
- return createPsbtFromBuffer({ Psbt, psbtBuffer: unsignedTx.txData.psbtBuffer, networkInfo })
34
+ return createPsbtFromBuffer({
35
+ Psbt,
36
+ psbtBuffer: unsignedTx.txData.psbtBuffer,
37
+ psbtOptions,
38
+ })
32
39
  }
33
40
 
34
41
  // Create PSBT based on internal Exodus data structure
@@ -41,15 +48,17 @@ export function createPrepareForSigning({
41
48
  Psbt,
42
49
  Transaction,
43
50
  })
44
- if (!['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)) psbt.setVersion(1)
51
+ setPsbtVersionIfNotBitcoin(psbt, assetName)
45
52
 
46
53
  return psbt
47
54
  }
48
55
  }
49
56
 
50
57
  // Creates a PSBT instance from the passed transaction buffer provided by 3rd parties (e.g. dApps).
51
- function createPsbtFromBuffer({ Psbt, psbtBuffer, ecc, networkInfo }) {
52
- return Psbt.fromBuffer(psbtBuffer, { eccLib: ecc, network: networkInfo })
58
+ function createPsbtFromBuffer({ Psbt, psbtBuffer, ecc, psbtOptions = {} }) {
59
+ const fromBufferOptions = { ...psbtOptions }
60
+ if (ecc) fromBufferOptions.eccLib = ecc
61
+ return Psbt.fromBuffer(psbtBuffer, fromBufferOptions)
53
62
  }
54
63
 
55
64
  // Creates a PSBT instance from the passed inputs, outputs etc. The wallet itself provides this data.
@@ -64,11 +73,18 @@ function createPsbtFromTxData({
64
73
  Transaction,
65
74
  blockHeight,
66
75
  }) {
67
- // use harcoded max fee rates for specific assets
68
- // if undefined, will be set to default value by PSBT (2500)
69
- const maximumFeeRate = _MAXIMUM_FEE_RATES[assetName]
76
+ // use hardcoded max fee rates for specific assets
77
+ // if undefined, the PSBT default (5000 sat/vB) is used. Passing `undefined`
78
+ // directly into the constructor used to clobber the default (buggy behavior),
79
+ // so we now only set the override when we actually have one.
80
+ const maximumFeeRate = getMaximumFeeRate(assetName)
81
+
82
+ const psbtOptions = { network: networkInfo }
83
+ if (maximumFeeRate !== undefined) {
84
+ psbtOptions.maximumFeeRate = maximumFeeRate
85
+ }
70
86
 
71
- const psbt = new Psbt({ maximumFeeRate, network: networkInfo })
87
+ const psbt = new Psbt(psbtOptions)
72
88
 
73
89
  // If present, add blockHeight as a proprietary field
74
90
  if (blockHeight !== undefined) {
@@ -1,6 +1,6 @@
1
1
  import assert from 'minimalistic-assert'
2
2
 
3
- import { extractTransaction } from './common.js'
3
+ import { extractTransaction, shouldSkipFinalize } from './common.js'
4
4
  import { createPrepareForSigning } from './default-prepare-for-signing.js'
5
5
 
6
6
  export const signHardwareFactory = ({ assetName, resolvePurpose, keys, coinInfo }) => {
@@ -40,11 +40,7 @@ export const signHardwareFactory = ({ assetName, resolvePurpose, keys, coinInfo
40
40
  multisigData,
41
41
  })
42
42
 
43
- const isExternalPsbt =
44
- unsignedTx.txData.psbtBuffer &&
45
- unsignedTx.txMeta.addressPathsMap &&
46
- unsignedTx.txMeta.inputsToSign
47
- const skipFinalize = isExternalPsbt || unsignedTx.txMeta.returnPsbt
43
+ const skipFinalize = shouldSkipFinalize(unsignedTx)
48
44
  return extractTransaction({ psbt, skipFinalize })
49
45
  }
50
46
  }
@@ -0,0 +1,13 @@
1
+ // We keep hardcoded fee caps here so both signing and PSBT builders reuse the
2
+ // same per-asset overrides (e.g. Dogecoin needs a higher ceiling than the
3
+ // bitcoinjs default). If an asset is missing, the library fallback (5000 sat/vB)
4
+ // will apply instead.
5
+ const MAXIMUM_FEE_RATES = {
6
+ dogecoin: 12_000,
7
+ qtumignition: 25_000,
8
+ ravencoin: 1_000_000,
9
+ }
10
+
11
+ export function getMaximumFeeRate(assetName) {
12
+ return MAXIMUM_FEE_RATES[assetName]
13
+ }