@exodus/bitcoin-api 4.1.1 → 4.1.3

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,30 @@
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.3](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.1.1...@exodus/bitcoin-api@4.1.3) (2025-10-14)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+
12
+ * fix: remove isBip70 from bitcoin libs (#6660)
13
+
14
+ * fix: remove unused bitcoin.api.prepareSendTx (#6662)
15
+
16
+
17
+
18
+ ## [4.1.2](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.1.1...@exodus/bitcoin-api@4.1.2) (2025-10-09)
19
+
20
+
21
+ ### Bug Fixes
22
+
23
+
24
+ * fix: remove isBip70 from bitcoin libs (#6660)
25
+
26
+ * fix: remove unused bitcoin.api.prepareSendTx (#6662)
27
+
28
+
29
+
6
30
  ## [4.1.1](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.1.0...@exodus/bitcoin-api@4.1.1) (2025-09-30)
7
31
 
8
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/bitcoin-api",
3
- "version": "4.1.1",
3
+ "version": "4.1.3",
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",
@@ -12,7 +12,8 @@
12
12
  "author": "Exodus Movement, Inc.",
13
13
  "license": "MIT",
14
14
  "publishConfig": {
15
- "access": "public"
15
+ "access": "public",
16
+ "provenance": false
16
17
  },
17
18
  "scripts": {
18
19
  "test": "run -T exodus-test --jest",
@@ -59,5 +60,5 @@
59
60
  "type": "git",
60
61
  "url": "git+https://github.com/ExodusMovement/assets.git"
61
62
  },
62
- "gitHead": "0d64e50fa0875dc526ce20e3be748f50c8483e9e"
63
+ "gitHead": "48e0d08e2fe044fb98d2fb16fc790f642a3c1eb2"
63
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,
package/src/move-funds.js CHANGED
@@ -3,7 +3,7 @@ import { Address, UtxoCollection } from '@exodus/models'
3
3
  import assert from 'minimalistic-assert'
4
4
  import wif from 'wif'
5
5
 
6
- import { createInputs, createOutput, getNonWitnessTxs } from './tx-send/index.js'
6
+ import { createInputs, createOutput, getNonWitnessTxs } from './tx-create/tx-create-utils.js'
7
7
 
8
8
  const isValidPrivateKey = (privateKey) => {
9
9
  try {
@@ -5,16 +5,6 @@ import { getSendDustValue as getDustValue } from './dust.js'
5
5
 
6
6
  const { createValidator, FIELDS, PRIORITY_LEVELS, VALIDATION_TYPES } = sendValidationModel
7
7
 
8
- const bip70Validator = createValidator({
9
- id: 'BIP70',
10
- type: VALIDATION_TYPES.ERROR,
11
- priority: PRIORITY_LEVELS.MIDDLE,
12
- field: FIELDS.ADDRESS,
13
- shouldValidate: ({ bip70 }) => !!bip70,
14
- isValid: async ({ bip70 }) => !bip70.isInvalid(),
15
- getMessage: () => t(`The payment request is invalid.`),
16
- })
17
-
18
8
  const bcnLegacyAddressValidator = createValidator({
19
9
  type: VALIDATION_TYPES.WARN,
20
10
  priority: PRIORITY_LEVELS.BASE,
@@ -80,9 +70,4 @@ const bitcoinCpfpWarning = createValidator({
80
70
  },
81
71
  })
82
72
 
83
- export default [
84
- bitcoinCpfpWarning,
85
- bip70Validator,
86
- bcnLegacyAddressValidator,
87
- notEnoughOutputValidator,
88
- ]
73
+ export default [bitcoinCpfpWarning, bcnLegacyAddressValidator, notEnoughOutputValidator]
@@ -0,0 +1,321 @@
1
+ import { Address, UtxoCollection } from '@exodus/models'
2
+ import lodash from 'lodash'
3
+ import assert from 'minimalistic-assert'
4
+
5
+ import { getChangeDustValue } from '../dust.js'
6
+ import { parseCurrency, serializeCurrency } from '../fee/fee-utils.js'
7
+ import { selectUtxos } from '../fee/utxo-selector.js'
8
+ import { findUnconfirmedSentRbfTxs } from '../tx-utils.js'
9
+ import { getUnconfirmedTxAncestorMap } from '../unconfirmed-ancestor-data.js'
10
+ import { getUsableUtxos, getUtxos } from '../utxos-utils.js'
11
+ import { createInputs, createOutput, getBlockHeight, getNonWitnessTxs } from './tx-create-utils.js'
12
+
13
+ async function createUnsignedTx({
14
+ inputs,
15
+ outputs,
16
+ useCashAddress,
17
+ addressPathsMap,
18
+ blockHeight,
19
+ asset,
20
+ selectedUtxos,
21
+ insightClient,
22
+ }) {
23
+ const nonWitnessTxs = await getNonWitnessTxs(asset, selectedUtxos, insightClient)
24
+
25
+ return {
26
+ txData: {
27
+ inputs,
28
+ outputs,
29
+ },
30
+ txMeta: {
31
+ useCashAddress, // for trezor to show the receiver cash address
32
+ addressPathsMap,
33
+ blockHeight,
34
+ rawTxs: nonWitnessTxs,
35
+ },
36
+ }
37
+ }
38
+
39
+ const getTxHandler = (type) => {
40
+ switch (type) {
41
+ case 'transfer':
42
+ return transferHandler
43
+ default:
44
+ throw new Error(`Unknown transaction type: ${type}`)
45
+ }
46
+ }
47
+
48
+ const transferHandler = {
49
+ buildTransaction: async ({
50
+ asset,
51
+ walletAccount,
52
+ toAddress,
53
+ amount,
54
+ blockHeight: providedBlockHeight,
55
+ rbfEnabled: providedRbfEnabled,
56
+ multipleAddressesEnabled,
57
+ feePerKB,
58
+ customFee,
59
+ isSendAll,
60
+ bumpTxId,
61
+ isExchange,
62
+ isRbfAllowed,
63
+ taprootInputWitnessSize,
64
+ accountState,
65
+ feeData,
66
+ getFeeEstimator,
67
+ allowUnconfirmedRbfEnabledUtxos,
68
+ utxosDescendingOrder,
69
+ assetClientInterface,
70
+ changeAddressType,
71
+ }) => {
72
+ const assetName = asset.name
73
+ const updatedFeeData = { ...feeData, feePerKB: feePerKB ?? feeData.feePerKB }
74
+ const insightClient = asset.baseAsset.insightClient
75
+
76
+ const blockHeight = providedBlockHeight || (await getBlockHeight({ assetName, insightClient }))
77
+
78
+ const rbfEnabled =
79
+ providedRbfEnabled || (updatedFeeData.rbfEnabled && !isExchange && isRbfAllowed)
80
+
81
+ const shuffle = (list) => {
82
+ // Using full lodash.shuffle notation so it can be mocked with spyOn in tests
83
+ return lodash.shuffle(list)
84
+ }
85
+
86
+ assert(
87
+ assetClientInterface,
88
+ `assetClientInterface must be supplied in sendTx for ${asset.name}`
89
+ )
90
+ assert(
91
+ toAddress || bumpTxId,
92
+ 'should not be called without either a receiving toAddress or to bump a tx'
93
+ )
94
+
95
+ const useCashAddress = asset.address.isCashAddress?.(toAddress)
96
+
97
+ const changeAddress = multipleAddressesEnabled
98
+ ? await assetClientInterface.getNextChangeAddress({ assetName, walletAccount })
99
+ : await assetClientInterface.getReceiveAddressObject({ assetName, walletAccount })
100
+
101
+ const txSet = await assetClientInterface.getTxLog({ assetName, walletAccount })
102
+
103
+ const unconfirmedTxAncestor = getUnconfirmedTxAncestorMap({ accountState })
104
+ const usableUtxos = getUsableUtxos({
105
+ asset,
106
+ utxos: getUtxos({ accountState, asset }),
107
+ feeData: updatedFeeData,
108
+ txSet,
109
+ unconfirmedTxAncestor,
110
+ })
111
+
112
+ let replaceableTxs = findUnconfirmedSentRbfTxs(txSet)
113
+
114
+ let processedAddress = toAddress
115
+ if (asset.address.toLegacyAddress) {
116
+ processedAddress = asset.address.toLegacyAddress(toAddress)
117
+ }
118
+
119
+ if (assetName === 'digibyte' && asset.address.isP2SH2(processedAddress)) {
120
+ processedAddress = asset.address.P2SH2ToP2SH(processedAddress)
121
+ }
122
+
123
+ let utxosToBump
124
+ if (bumpTxId) {
125
+ const bumpTx = replaceableTxs.find(({ txId }) => txId === bumpTxId)
126
+ if (bumpTx) {
127
+ replaceableTxs = [bumpTx]
128
+ } else {
129
+ utxosToBump = usableUtxos.getTxIdUtxos(bumpTxId)
130
+ if (utxosToBump.size === 0) {
131
+ throw new Error(`Cannot bump transaction ${bumpTxId}`)
132
+ }
133
+
134
+ replaceableTxs = []
135
+ }
136
+ }
137
+
138
+ const sendAmount = bumpTxId ? asset.currency.ZERO : amount
139
+ const receiveAddress = bumpTxId
140
+ ? replaceableTxs.length > 0
141
+ ? null
142
+ : changeAddressType
143
+ : processedAddress
144
+ const feeRate = updatedFeeData.feePerKB
145
+ const resolvedIsSendAll = !rbfEnabled && feePerKB ? false : isSendAll
146
+
147
+ let { selectedUtxos, fee, replaceTx } = selectUtxos({
148
+ asset,
149
+ usableUtxos,
150
+ replaceableTxs,
151
+ amount: sendAmount,
152
+ feeRate: customFee || feeRate,
153
+ receiveAddress,
154
+ isSendAll: resolvedIsSendAll,
155
+ getFeeEstimator: (asset, { feePerKB, ...options }) =>
156
+ getFeeEstimator(asset, feePerKB, options),
157
+ mustSpendUtxos: utxosToBump,
158
+ allowUnconfirmedRbfEnabledUtxos,
159
+ unconfirmedTxAncestor,
160
+ utxosDescendingOrder,
161
+ taprootInputWitnessSize,
162
+ changeAddressType,
163
+ })
164
+
165
+ if (!selectedUtxos && !replaceTx) throw new Error('Not enough funds.')
166
+
167
+ // When bumping a tx, we can either replace the tx with RBF or spend its selected change.
168
+ // If there is no selected UTXO or the tx to replace is not the tx we want to bump,
169
+ // then something is wrong because we can't actually bump the tx.
170
+ // This shouldn't happen but might due to either the tx confirming before accelerate was
171
+ // pressed, or if the change was already spent from another wallet.
172
+ if (bumpTxId && (!selectedUtxos || (replaceTx && replaceTx.txId !== bumpTxId))) {
173
+ throw new Error(`Unable to bump ${bumpTxId}`)
174
+ }
175
+
176
+ if (replaceTx) {
177
+ replaceTx = replaceTx.clone()
178
+ replaceTx = replaceTx.update({ data: { ...replaceTx.data } })
179
+ replaceTx.data.sent = replaceTx.data.sent.map((to) => {
180
+ return { ...to, amount: serializeCurrency(to.amount, asset.currency) }
181
+ })
182
+ selectedUtxos = selectedUtxos.union(
183
+ UtxoCollection.fromJSON(replaceTx.data.inputs, { currency: asset.currency })
184
+ )
185
+ }
186
+
187
+ const addressPathsMap = selectedUtxos.getAddressPathsMap()
188
+
189
+ // Inputs and Outputs
190
+ const inputs = shuffle(createInputs(assetName, selectedUtxos.toArray(), rbfEnabled))
191
+ let outputs = replaceTx
192
+ ? replaceTx.data.sent.map(({ address, amount }) =>
193
+ createOutput(assetName, address, parseCurrency(amount, asset.currency))
194
+ )
195
+ : []
196
+
197
+ // Send output
198
+ let sendOutput
199
+ if (processedAddress) {
200
+ sendOutput = createOutput(assetName, processedAddress, sendAmount)
201
+ outputs.push(sendOutput)
202
+ }
203
+
204
+ const totalAmount = replaceTx
205
+ ? replaceTx.data.sent.reduce(
206
+ (total, { amount }) => total.add(parseCurrency(amount, asset.currency)),
207
+ sendAmount
208
+ )
209
+ : sendAmount
210
+
211
+ const change = selectedUtxos.value.sub(totalAmount).sub(fee)
212
+ const dust = getChangeDustValue(asset)
213
+ let ourAddress = replaceTx?.data?.changeAddress || changeAddress
214
+ if (asset.address.toLegacyAddress) {
215
+ const legacyAddress = asset.address.toLegacyAddress(ourAddress.address)
216
+ ourAddress = Address.create(legacyAddress, ourAddress.meta)
217
+ }
218
+
219
+ // Change Output
220
+ let changeOutput
221
+ if (change.gte(dust)) {
222
+ changeOutput = createOutput(assetName, ourAddress.address ?? ourAddress.toString(), change)
223
+ // Add the keypath of change address to support Trezor detect the change output.
224
+ // Output is change and does not need approval from user which shows the strange address that user never seen.
225
+ addressPathsMap[changeAddress] = ourAddress.meta.path
226
+ outputs.push(changeOutput)
227
+ } else {
228
+ // If we don't have enough for a change output, then all remaining dust is just added to fee
229
+ fee = fee.add(change)
230
+ }
231
+
232
+ outputs = replaceTx ? outputs : shuffle(outputs)
233
+
234
+ const unsignedTx = await createUnsignedTx({
235
+ inputs,
236
+ outputs,
237
+ useCashAddress,
238
+ addressPathsMap,
239
+ blockHeight,
240
+ asset,
241
+ selectedUtxos,
242
+ insightClient,
243
+ })
244
+
245
+ return {
246
+ amount,
247
+ change,
248
+ totalAmount,
249
+ address: processedAddress,
250
+ ourAddress,
251
+ receiveAddress,
252
+ sendAmount,
253
+ fee,
254
+ usableUtxos,
255
+ selectedUtxos,
256
+ replaceTx,
257
+ sendOutput,
258
+ changeOutput,
259
+ unsignedTx,
260
+ }
261
+ },
262
+ }
263
+
264
+ export const createTxFactory =
265
+ ({
266
+ getFeeEstimator,
267
+ allowUnconfirmedRbfEnabledUtxos,
268
+ utxosDescendingOrder,
269
+ assetClientInterface,
270
+ changeAddressType,
271
+ }) =>
272
+ async ({
273
+ asset,
274
+ walletAccount,
275
+ type,
276
+ toAddress,
277
+ amount,
278
+ blockHeight,
279
+ rbfEnabled,
280
+ multipleAddressesEnabled,
281
+ feePerKB,
282
+ customFee,
283
+ isSendAll,
284
+ bumpTxId,
285
+ isExchange,
286
+ isRbfAllowed,
287
+ taprootInputWitnessSize,
288
+ }) => {
289
+ const assetName = asset.name
290
+ const accountState = await assetClientInterface.getAccountState({
291
+ assetName,
292
+ walletAccount,
293
+ })
294
+ const feeData = await assetClientInterface.getFeeConfig({ assetName })
295
+
296
+ const txHandler = getTxHandler(type)
297
+
298
+ return txHandler.buildTransaction({
299
+ asset,
300
+ walletAccount,
301
+ toAddress,
302
+ amount,
303
+ blockHeight,
304
+ rbfEnabled,
305
+ multipleAddressesEnabled,
306
+ feePerKB,
307
+ customFee,
308
+ isSendAll,
309
+ bumpTxId,
310
+ isExchange,
311
+ isRbfAllowed,
312
+ taprootInputWitnessSize,
313
+ accountState,
314
+ feeData,
315
+ getFeeEstimator,
316
+ allowUnconfirmedRbfEnabledUtxos,
317
+ utxosDescendingOrder,
318
+ assetClientInterface,
319
+ changeAddressType,
320
+ })
321
+ }
@@ -0,0 +1,91 @@
1
+ import { getTxSequence } from '@exodus/bitcoin-lib'
2
+
3
+ import {
4
+ createInputs as dogecoinCreateInputs,
5
+ createOutput as dogecoinCreateOutput,
6
+ } from './dogecoin.js'
7
+
8
+ const ASSETS_USING_BUFFER_VALUES = new Set(['dogecoin', 'digibyte'])
9
+
10
+ const ASSETS_SUPPORTED_BIP_174 = new Set([
11
+ 'bitcoin',
12
+ 'bitcoinregtest',
13
+ 'bitcointestnet',
14
+ 'litecoin',
15
+ 'dash',
16
+ 'dogecoin',
17
+ 'ravencoin',
18
+ 'digibyte',
19
+ 'qtumignition',
20
+ 'vertcoin', // is not available on mobile!
21
+ ])
22
+
23
+ export async function getBlockHeight({ assetName, insightClient }) {
24
+ return ['zcash', 'bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)
25
+ ? insightClient.fetchBlockHeight()
26
+ : 0
27
+ }
28
+
29
+ export async function getNonWitnessTxs(asset, utxos, insightClient) {
30
+ const rawTxs = []
31
+
32
+ // BIP 174 (PSBT) requires full transaction for non-witness outputs
33
+ if (ASSETS_SUPPORTED_BIP_174.has(asset.name)) {
34
+ const nonWitnessTxIds = utxos.txIds.filter((txId) =>
35
+ utxos
36
+ .getAddressesForTxId(txId)
37
+ .toAddressStrings()
38
+ .some(
39
+ (a) =>
40
+ asset.address.isP2PKH(a) ||
41
+ asset.address.isP2SH(a) ||
42
+ asset.address.isP2SH2?.(a) ||
43
+ asset.address.isP2WPKH?.(a) ||
44
+ asset.address.isP2WSH?.(a)
45
+ )
46
+ )
47
+
48
+ if (nonWitnessTxIds.length > 0) {
49
+ for (const txId of nonWitnessTxIds) {
50
+ // full transaction is required for non-witness outputs
51
+ const rawData = await insightClient.fetchRawTx(txId)
52
+ rawTxs.push({ txId, rawData })
53
+ }
54
+ }
55
+ }
56
+
57
+ return rawTxs
58
+ }
59
+
60
+ export function createInputs(assetName, ...rest) {
61
+ if (ASSETS_USING_BUFFER_VALUES.has(assetName)) {
62
+ return dogecoinCreateInputs(...rest)
63
+ }
64
+
65
+ return defaultCreateInputs(...rest)
66
+ }
67
+
68
+ export function defaultCreateInputs(utxos, rbfEnabled) {
69
+ return utxos.map((utxo) => ({
70
+ txId: utxo.txId,
71
+ vout: utxo.vout,
72
+ address: utxo.address.toString(),
73
+ value: parseInt(utxo.value.toBaseString(), 10),
74
+ script: utxo.script,
75
+ sequence: getTxSequence(rbfEnabled),
76
+ inscriptionId: utxo.inscriptionId,
77
+ derivationPath: utxo.derivationPath,
78
+ }))
79
+ }
80
+
81
+ export function createOutput(assetName, ...rest) {
82
+ if (ASSETS_USING_BUFFER_VALUES.has(assetName)) {
83
+ return dogecoinCreateOutput(...rest)
84
+ }
85
+
86
+ return defaultCreateOutput(...rest)
87
+ }
88
+
89
+ export function defaultCreateOutput(address, sendAmount) {
90
+ return [address, parseInt(sendAmount.toBaseString(), 10)]
91
+ }
@@ -1,72 +1,11 @@
1
- import { getTxSequence } from '@exodus/bitcoin-lib'
2
1
  import * as defaultBitcoinjsLib from '@exodus/bitcoinjs'
3
- import { Address, UtxoCollection } from '@exodus/models'
2
+ import { Address } from '@exodus/models'
4
3
  import { retry } from '@exodus/simple-retry'
5
- import lodash from 'lodash'
6
4
  import assert from 'minimalistic-assert'
7
5
 
8
- import { getChangeDustValue } from '../dust.js'
9
- import { parseCurrency, serializeCurrency } from '../fee/fee-utils.js'
10
- import { selectUtxos } from '../fee/utxo-selector.js'
11
- import { findUnconfirmedSentRbfTxs } from '../tx-utils.js'
12
- import { getUnconfirmedTxAncestorMap } from '../unconfirmed-ancestor-data.js'
13
- import {
14
- getInscriptionIds,
15
- getOrdinalsUtxos,
16
- getTransferOrdinalsUtxos,
17
- getUsableUtxos,
18
- getUtxos,
19
- } from '../utxos-utils.js'
20
- import {
21
- createInputs as dogecoinCreateInputs,
22
- createOutput as dogecoinCreateOutput,
23
- } from './dogecoin.js'
24
-
25
- const ASSETS_SUPPORTED_BIP_174 = new Set([
26
- 'bitcoin',
27
- 'bitcoinregtest',
28
- 'bitcointestnet',
29
- 'litecoin',
30
- 'dash',
31
- 'dogecoin',
32
- 'ravencoin',
33
- 'digibyte',
34
- 'qtumignition',
35
- 'vertcoin', // is not available on mobile!
36
- ])
37
-
38
- const ASSETS_USING_BUFFER_VALUES = new Set(['dogecoin', 'digibyte'])
39
-
40
- export async function getNonWitnessTxs(asset, utxos, insightClient) {
41
- const rawTxs = []
42
-
43
- // BIP 174 (PSBT) requires full transaction for non-witness outputs
44
- if (ASSETS_SUPPORTED_BIP_174.has(asset.name)) {
45
- const nonWitnessTxIds = utxos.txIds.filter((txId) =>
46
- utxos
47
- .getAddressesForTxId(txId)
48
- .toAddressStrings()
49
- .some(
50
- (a) =>
51
- asset.address.isP2PKH(a) ||
52
- asset.address.isP2SH(a) ||
53
- asset.address.isP2SH2?.(a) ||
54
- asset.address.isP2WPKH?.(a) ||
55
- asset.address.isP2WSH?.(a)
56
- )
57
- )
58
-
59
- if (nonWitnessTxIds.length > 0) {
60
- for (const txId of nonWitnessTxIds) {
61
- // full transaction is required for non-witness outputs
62
- const rawData = await insightClient.fetchRawTx(txId)
63
- rawTxs.push({ txId, rawData })
64
- }
65
- }
66
- }
67
-
68
- return rawTxs
69
- }
6
+ import { serializeCurrency } from '../fee/fee-utils.js'
7
+ import { createTxFactory } from '../tx-create/create-tx.js'
8
+ import { getBlockHeight } from '../tx-create/tx-create-utils.js'
70
9
 
71
10
  const getSize = (tx) => {
72
11
  if (typeof tx.size === 'number') return tx.size
@@ -160,290 +99,39 @@ export async function signTransaction({
160
99
  return { rawTx, txId, tx }
161
100
  }
162
101
 
163
- async function createUnsignedTx({
164
- inputs,
165
- outputs,
166
- useCashAddress,
167
- addressPathsMap,
168
- blockHeight,
102
+ const getPrepareSendTransaction = async ({
103
+ address,
104
+ allowUnconfirmedRbfEnabledUtxos,
105
+ amount,
169
106
  asset,
170
- selectedUtxos,
171
- insightClient,
172
- }) {
173
- const unsignedTx = {
174
- txData: {
175
- inputs,
176
- outputs,
177
- },
178
- txMeta: {
179
- useCashAddress, // for trezor to show the receiver cash address
180
- addressPathsMap,
181
- blockHeight,
182
- },
183
- }
184
-
185
- const nonWitnessTxs = await getNonWitnessTxs(asset, selectedUtxos, insightClient)
186
- Object.assign(unsignedTx.txMeta, { rawTxs: nonWitnessTxs })
187
- return unsignedTx
188
- }
189
-
190
- async function getBlockHeight({ assetName, insightClient }) {
191
- return ['zcash', 'bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)
192
- ? insightClient.fetchBlockHeight()
193
- : 0
194
- }
195
-
196
- export const getPrepareSendTransaction =
197
- ({
198
- blockHeight: providedBlockHeight,
199
- ordinalsEnabled,
107
+ assetClientInterface,
108
+ blockHeight,
109
+ changeAddressType,
110
+ getFeeEstimator,
111
+ options,
112
+ rbfEnabled,
113
+ utxosDescendingOrder,
114
+ walletAccount,
115
+ }) => {
116
+ const createTx = createTxFactory({
200
117
  getFeeEstimator,
201
118
  allowUnconfirmedRbfEnabledUtxos,
202
119
  utxosDescendingOrder,
203
- rbfEnabled: providedRbfEnabled,
204
120
  assetClientInterface,
205
121
  changeAddressType,
206
- }) =>
207
- async ({ asset, walletAccount, address, amount, options }) => {
208
- const {
209
- multipleAddressesEnabled,
210
- feePerKB,
211
- customFee,
212
- isSendAll,
213
- bumpTxId,
214
- nft,
215
- isExchange,
216
- isBip70,
217
- isRbfAllowed,
218
- taprootInputWitnessSize,
219
- } = options
220
-
221
- const assetName = asset.name
222
- const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
223
- const feeData = await assetClientInterface.getFeeConfig({ assetName })
224
- feeData.feePerKB = feePerKB ?? feeData.feePerKB
225
- const insightClient = asset.baseAsset.insightClient
226
-
227
- const blockHeight = providedBlockHeight || (await getBlockHeight({ assetName, insightClient }))
228
-
229
- const rbfEnabled =
230
- providedRbfEnabled || (feeData.rbfEnabled && !isExchange && !isBip70 && isRbfAllowed && !nft)
231
-
232
- const inscriptionIds = getInscriptionIds({ nft })
233
-
234
- assert(
235
- ordinalsEnabled || !inscriptionIds,
236
- 'inscriptions cannot be sent when ordinalsEnabled=false '
237
- )
238
-
239
- const shuffle = (list) => {
240
- // Using full lodash.shuffle notation so it can be mocked with spyOn in tests
241
- return inscriptionIds ? list : lodash.shuffle(list) // don't shuffle when sending ordinal!!!!
242
- }
243
-
244
- assert(
245
- assetClientInterface,
246
- `assetClientInterface must be supplied in sendTx for ${asset.name}`
247
- )
248
- assert(
249
- address || bumpTxId,
250
- 'should not be called without either a receiving address or to bump a tx'
251
- )
252
-
253
- if (inscriptionIds) {
254
- assert(!bumpTxId, 'only inscriptionIds or bumpTxId must be provided')
255
- assert(address, 'address must be provided when sending ordinals')
256
- }
257
-
258
- const useCashAddress = asset.address.isCashAddress?.(address)
259
-
260
- const changeAddress = multipleAddressesEnabled
261
- ? await assetClientInterface.getNextChangeAddress({ assetName, walletAccount })
262
- : await assetClientInterface.getReceiveAddressObject({ assetName, walletAccount })
263
-
264
- const txSet = await assetClientInterface.getTxLog({ assetName, walletAccount })
265
-
266
- const currentOrdinalsUtxos = getOrdinalsUtxos({ accountState, asset })
267
- const transferOrdinalsUtxos = inscriptionIds
268
- ? getTransferOrdinalsUtxos({ inscriptionIds, ordinalsUtxos: currentOrdinalsUtxos })
269
- : undefined
270
-
271
- const currency = asset.currency
272
-
273
- const unconfirmedTxAncestor = getUnconfirmedTxAncestorMap({ accountState })
274
- const usableUtxos = getUsableUtxos({
275
- asset,
276
- utxos: getUtxos({ accountState, asset }),
277
- feeData,
278
- txSet,
279
- unconfirmedTxAncestor,
280
- })
281
-
282
- let replaceableTxs = findUnconfirmedSentRbfTxs(txSet)
283
-
284
- if (asset.address.toLegacyAddress) {
285
- address = asset.address.toLegacyAddress(address)
286
- }
287
-
288
- if (assetName === 'digibyte' && asset.address.isP2SH2(address)) {
289
- address = asset.address.P2SH2ToP2SH(address)
290
- }
291
-
292
- let utxosToBump
293
- if (bumpTxId) {
294
- const bumpTx = replaceableTxs.find(({ txId }) => txId === bumpTxId)
295
- if (bumpTx) {
296
- replaceableTxs = [bumpTx]
297
- } else {
298
- utxosToBump = usableUtxos.getTxIdUtxos(bumpTxId)
299
- if (utxosToBump.size === 0) {
300
- throw new Error(`Cannot bump transaction ${bumpTxId}`)
301
- }
302
-
303
- replaceableTxs = []
304
- }
305
- }
306
-
307
- const sendAmount = bumpTxId || transferOrdinalsUtxos ? asset.currency.ZERO : amount
308
- const receiveAddress = bumpTxId
309
- ? replaceableTxs.length > 0
310
- ? null
311
- : changeAddressType
312
- : address
313
- const feeRate = feeData.feePerKB
314
- const resolvedIsSendAll = (!rbfEnabled && feePerKB) || transferOrdinalsUtxos ? false : isSendAll
315
-
316
- let { selectedUtxos, fee, replaceTx } = selectUtxos({
317
- asset,
318
- usableUtxos,
319
- replaceableTxs,
320
- amount: sendAmount,
321
- feeRate: customFee || feeRate,
322
- receiveAddress,
323
- isSendAll: resolvedIsSendAll,
324
- getFeeEstimator: (asset, { feePerKB, ...options }) =>
325
- getFeeEstimator(asset, feePerKB, options),
326
- mustSpendUtxos: utxosToBump,
327
- allowUnconfirmedRbfEnabledUtxos,
328
- unconfirmedTxAncestor,
329
- inscriptionIds,
330
- transferOrdinalsUtxos,
331
- utxosDescendingOrder,
332
- taprootInputWitnessSize,
333
- changeAddressType,
334
- })
335
-
336
- if (!selectedUtxos && !replaceTx) throw new Error('Not enough funds.')
337
-
338
- // When bumping a tx, we can either replace the tx with RBF or spend its selected change.
339
- // If there is no selected UTXO or the tx to replace is not the tx we want to bump,
340
- // then something is wrong because we can't actually bump the tx.
341
- // This shouldn't happen but might due to either the tx confirming before accelerate was
342
- // pressed, or if the change was already spent from another wallet.
343
- if (bumpTxId && (!selectedUtxos || (replaceTx && replaceTx.txId !== bumpTxId))) {
344
- throw new Error(`Unable to bump ${bumpTxId}`)
345
- }
346
-
347
- if (replaceTx) {
348
- replaceTx = replaceTx.clone()
349
- replaceTx = replaceTx.update({ data: { ...replaceTx.data } })
350
- replaceTx.data.sent = replaceTx.data.sent.map((to) => {
351
- return { ...to, amount: serializeCurrency(to.amount, asset.currency) }
352
- })
353
- selectedUtxos = selectedUtxos.union(
354
- // how to avoid replace tx inputs when inputs are ordinals? !!!!
355
- UtxoCollection.fromJSON(replaceTx.data.inputs, { currency: asset.currency })
356
- )
357
- }
358
-
359
- const addressPathsMap = selectedUtxos.getAddressPathsMap()
360
-
361
- // Inputs and Outputs
362
- const inputs = shuffle(createInputs(assetName, selectedUtxos.toArray(), rbfEnabled))
363
- let outputs = replaceTx
364
- ? replaceTx.data.sent.map(({ address, amount }) =>
365
- createOutput(assetName, address, parseCurrency(amount, currency))
366
- )
367
- : []
368
-
369
- // Send output
370
- let sendOutput
371
- if (address) {
372
- if (transferOrdinalsUtxos) {
373
- outputs.push(
374
- ...transferOrdinalsUtxos
375
- .toArray()
376
- .map((ordinalUtxo) => createOutput(assetName, address, ordinalUtxo.value))
377
- )
378
- } else {
379
- sendOutput = createOutput(assetName, address, sendAmount)
380
- outputs.push(sendOutput)
381
- }
382
- }
383
-
384
- const totalAmount = replaceTx
385
- ? replaceTx.data.sent.reduce(
386
- (total, { amount }) => total.add(parseCurrency(amount, asset.currency)),
387
- sendAmount
388
- )
389
- : sendAmount
390
-
391
- const change = selectedUtxos.value
392
- .sub(totalAmount)
393
- .sub(transferOrdinalsUtxos?.value || currency.ZERO)
394
- .sub(fee)
395
- const dust = getChangeDustValue(asset)
396
- let ourAddress = replaceTx?.data?.changeAddress || changeAddress
397
- if (asset.address.toLegacyAddress) {
398
- const legacyAddress = asset.address.toLegacyAddress(ourAddress.address)
399
- ourAddress = Address.create(legacyAddress, ourAddress.meta)
400
- }
401
-
402
- // Change Output
403
- let changeOutput
404
- if (change.gte(dust)) {
405
- changeOutput = createOutput(assetName, ourAddress.address ?? ourAddress.toString(), change)
406
- // Add the keypath of change address to support Trezor detect the change output.
407
- // Output is change and does not need approval from user which shows the strange address that user never seen.
408
- addressPathsMap[changeAddress] = ourAddress.meta.path
409
- outputs.push(changeOutput)
410
- } else {
411
- // If we don't have enough for a change output, then all remaining dust is just added to fee
412
- fee = fee.add(change)
413
- }
414
-
415
- outputs = replaceTx ? outputs : shuffle(outputs)
122
+ })
416
123
 
417
- const unsignedTx = await createUnsignedTx({
418
- inputs,
419
- outputs,
420
- useCashAddress,
421
- addressPathsMap,
422
- blockHeight,
423
- asset,
424
- selectedUtxos,
425
- insightClient,
426
- })
427
- return {
428
- amount,
429
- change,
430
- totalAmount,
431
- currentOrdinalsUtxos,
432
- inscriptionIds,
433
- address,
434
- ourAddress,
435
- receiveAddress,
436
- sendAmount,
437
- fee,
438
- usableUtxos,
439
- selectedUtxos,
440
- transferOrdinalsUtxos,
441
- replaceTx,
442
- sendOutput,
443
- changeOutput,
444
- unsignedTx,
445
- }
446
- }
124
+ return createTx({
125
+ asset,
126
+ walletAccount,
127
+ type: 'transfer',
128
+ toAddress: address,
129
+ amount,
130
+ blockHeight,
131
+ rbfEnabled,
132
+ ...options,
133
+ })
134
+ }
447
135
 
448
136
  // not ported from Exodus; but this demos signing / broadcasting
449
137
  // NOTE: this will be ripped out in the coming weeks
@@ -452,47 +140,47 @@ export const createAndBroadcastTXFactory =
452
140
  getFeeEstimator,
453
141
  getSizeAndChangeScript = getSizeAndChangeScriptFactory(), // for decred customizations
454
142
  allowUnconfirmedRbfEnabledUtxos,
455
- ordinalsEnabled = false,
456
143
  utxosDescendingOrder,
457
144
  assetClientInterface,
458
145
  changeAddressType,
459
146
  }) =>
460
147
  async ({ asset, walletAccount, address, amount, options }) => {
461
148
  // Prepare transaction
462
- const { bumpTxId, nft, isExchange, isBip70, isRbfAllowed = true } = options
149
+ const { bumpTxId, isExchange, isRbfAllowed = true } = options
463
150
 
464
151
  const assetName = asset.name
465
152
  const feeData = await assetClientInterface.getFeeConfig({ assetName })
466
153
  const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
467
154
  const insightClient = asset.baseAsset.insightClient
468
155
 
469
- const rbfEnabled = feeData.rbfEnabled && !isExchange && !isBip70 && isRbfAllowed && !nft
156
+ const rbfEnabled = feeData.rbfEnabled && !isExchange && isRbfAllowed
470
157
 
471
158
  // blockHeight
472
159
  const blockHeight = await getBlockHeight({ assetName, insightClient })
473
160
 
474
161
  const transactionDescriptor = await getPrepareSendTransaction({
475
- blockHeight,
476
- ordinalsEnabled,
477
- getFeeEstimator,
162
+ address,
478
163
  allowUnconfirmedRbfEnabledUtxos,
479
- utxosDescendingOrder,
480
- rbfEnabled,
164
+ amount,
165
+ asset,
481
166
  assetClientInterface,
167
+ blockHeight,
482
168
  changeAddressType,
483
- })({ asset, walletAccount, address, amount, options })
169
+ getFeeEstimator,
170
+ options,
171
+ rbfEnabled,
172
+ utxosDescendingOrder,
173
+ walletAccount,
174
+ })
484
175
  const {
485
176
  change,
486
177
  totalAmount,
487
- currentOrdinalsUtxos,
488
- inscriptionIds,
489
178
  ourAddress,
490
179
  receiveAddress,
491
180
  sendAmount,
492
181
  fee,
493
182
  usableUtxos,
494
183
  selectedUtxos,
495
- transferOrdinalsUtxos,
496
184
  replaceTx,
497
185
  sendOutput,
498
186
  changeOutput,
@@ -570,7 +258,6 @@ export const createAndBroadcastTXFactory =
570
258
 
571
259
  const { script, size } = getSizeAndChangeScript({ assetName, tx, rawTx, changeUtxoIndex, txId })
572
260
 
573
- // for ordinals, used to allow users spending change utxos even when unconfirmed and ordinals are unknown
574
261
  const knownBalanceUtxoIds = accountState.knownBalanceUtxoIds || []
575
262
  let remainingUtxos = usableUtxos.difference(selectedUtxos)
576
263
  if (changeUtxoIndex !== -1) {
@@ -593,16 +280,11 @@ export const createAndBroadcastTXFactory =
593
280
  remainingUtxos = remainingUtxos.difference(remainingUtxos.getTxIdUtxos(replaceTx.txId))
594
281
  }
595
282
 
596
- const remainingOrdinalsUtxos = transferOrdinalsUtxos
597
- ? currentOrdinalsUtxos.difference(transferOrdinalsUtxos)
598
- : currentOrdinalsUtxos
599
-
600
283
  await assetClientInterface.updateAccountState({
601
284
  assetName,
602
285
  walletAccount,
603
286
  newData: {
604
287
  utxos: remainingUtxos,
605
- ordinalsUtxos: remainingOrdinalsUtxos,
606
288
  knownBalanceUtxoIds,
607
289
  },
608
290
  })
@@ -640,10 +322,6 @@ export const createAndBroadcastTXFactory =
640
322
  return asset.currency.ZERO
641
323
  }
642
324
 
643
- if (nft) {
644
- return transferOrdinalsUtxos.value.abs().negate()
645
- }
646
-
647
325
  return totalAmount.abs().negate()
648
326
  }
649
327
 
@@ -670,17 +348,6 @@ export const createAndBroadcastTXFactory =
670
348
  blocksSeen: 0,
671
349
  inputs: selectedUtxos.toJSON(),
672
350
  replacedTxId: replaceTx ? replaceTx.txId : undefined,
673
- nftId: nft ? `${assetName}:${nft.tokenId}` : undefined, // it allows BE to load the nft info while the nft is in transit
674
- inscriptionsIndexed: ordinalsEnabled ? true : undefined,
675
- sentInscriptions: inscriptionIds
676
- ? inscriptionIds.map((inscriptionId) => {
677
- return {
678
- inscriptionId,
679
- offset: 0,
680
- value: 0,
681
- }
682
- })
683
- : undefined,
684
351
  },
685
352
  },
686
353
  ],
@@ -705,38 +372,5 @@ export const createAndBroadcastTXFactory =
705
372
  }
706
373
  }
707
374
 
708
- export function createInputs(assetName, ...rest) {
709
- if (ASSETS_USING_BUFFER_VALUES.has(assetName)) {
710
- return dogecoinCreateInputs(...rest)
711
- }
712
-
713
- return defaultCreateInputs(...rest)
714
- }
715
-
716
- function defaultCreateInputs(utxos, rbfEnabled) {
717
- return utxos.map((utxo) => ({
718
- txId: utxo.txId,
719
- vout: utxo.vout,
720
- address: utxo.address.toString(),
721
- value: parseInt(utxo.value.toBaseString(), 10),
722
- script: utxo.script,
723
- sequence: getTxSequence(rbfEnabled),
724
- inscriptionId: utxo.inscriptionId,
725
- derivationPath: utxo.derivationPath,
726
- }))
727
- }
728
-
729
- export function createOutput(assetName, ...rest) {
730
- if (ASSETS_USING_BUFFER_VALUES.has(assetName)) {
731
- return dogecoinCreateOutput(...rest)
732
- }
733
-
734
- return defaultCreateOutput(...rest)
735
- }
736
-
737
- function defaultCreateOutput(address, sendAmount) {
738
- return [address, parseInt(sendAmount.toBaseString(), 10)]
739
- }
740
-
741
375
  // back compatibiliy
742
376
  export { getSendDustValue as getDustValue } from '../dust.js'
File without changes