@exodus/bitcoin-api 4.1.1 → 4.1.2

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,18 @@
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.2](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.1.1...@exodus/bitcoin-api@4.1.2) (2025-10-09)
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
+
6
18
  ## [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
19
 
8
20
 
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.2",
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": "6237b0d98791ed9e6290260947068d2600bf8270"
63
64
  }
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,367 @@
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 {
11
+ getInscriptionIds,
12
+ getOrdinalsUtxos,
13
+ getTransferOrdinalsUtxos,
14
+ getUsableUtxos,
15
+ getUtxos,
16
+ } from '../utxos-utils.js'
17
+ import { createInputs, createOutput, getBlockHeight, getNonWitnessTxs } from './tx-create-utils.js'
18
+
19
+ async function createUnsignedTx({
20
+ inputs,
21
+ outputs,
22
+ useCashAddress,
23
+ addressPathsMap,
24
+ blockHeight,
25
+ asset,
26
+ selectedUtxos,
27
+ insightClient,
28
+ }) {
29
+ const nonWitnessTxs = await getNonWitnessTxs(asset, selectedUtxos, insightClient)
30
+
31
+ return {
32
+ txData: {
33
+ inputs,
34
+ outputs,
35
+ },
36
+ txMeta: {
37
+ useCashAddress, // for trezor to show the receiver cash address
38
+ addressPathsMap,
39
+ blockHeight,
40
+ rawTxs: nonWitnessTxs,
41
+ },
42
+ }
43
+ }
44
+
45
+ const getTxHandler = (type) => {
46
+ switch (type) {
47
+ case 'transfer':
48
+ return transferHandler
49
+ default:
50
+ throw new Error(`Unknown transaction type: ${type}`)
51
+ }
52
+ }
53
+
54
+ const transferHandler = {
55
+ buildTransaction: async ({
56
+ asset,
57
+ walletAccount,
58
+ toAddress,
59
+ amount,
60
+ blockHeight: providedBlockHeight,
61
+ rbfEnabled: providedRbfEnabled,
62
+ multipleAddressesEnabled,
63
+ feePerKB,
64
+ customFee,
65
+ isSendAll,
66
+ bumpTxId,
67
+ nft,
68
+ isExchange,
69
+ isRbfAllowed,
70
+ taprootInputWitnessSize,
71
+ accountState,
72
+ feeData,
73
+ ordinalsEnabled,
74
+ getFeeEstimator,
75
+ allowUnconfirmedRbfEnabledUtxos,
76
+ utxosDescendingOrder,
77
+ assetClientInterface,
78
+ changeAddressType,
79
+ }) => {
80
+ const assetName = asset.name
81
+ const updatedFeeData = { ...feeData, feePerKB: feePerKB ?? feeData.feePerKB }
82
+ const insightClient = asset.baseAsset.insightClient
83
+
84
+ const blockHeight = providedBlockHeight || (await getBlockHeight({ assetName, insightClient }))
85
+
86
+ const rbfEnabled =
87
+ providedRbfEnabled || (updatedFeeData.rbfEnabled && !isExchange && isRbfAllowed && !nft)
88
+
89
+ const inscriptionIds = getInscriptionIds({ nft })
90
+
91
+ assert(
92
+ ordinalsEnabled || !inscriptionIds,
93
+ 'inscriptions cannot be sent when ordinalsEnabled=false '
94
+ )
95
+
96
+ const shuffle = (list) => {
97
+ // Using full lodash.shuffle notation so it can be mocked with spyOn in tests
98
+ return inscriptionIds ? list : lodash.shuffle(list) // don't shuffle when sending ordinal!!!!
99
+ }
100
+
101
+ assert(
102
+ assetClientInterface,
103
+ `assetClientInterface must be supplied in sendTx for ${asset.name}`
104
+ )
105
+ assert(
106
+ toAddress || bumpTxId,
107
+ 'should not be called without either a receiving toAddress or to bump a tx'
108
+ )
109
+
110
+ if (inscriptionIds) {
111
+ assert(!bumpTxId, 'only inscriptionIds or bumpTxId must be provided')
112
+ assert(toAddress, 'toAddress must be provided when sending ordinals')
113
+ }
114
+
115
+ const useCashAddress = asset.address.isCashAddress?.(toAddress)
116
+
117
+ const changeAddress = multipleAddressesEnabled
118
+ ? await assetClientInterface.getNextChangeAddress({ assetName, walletAccount })
119
+ : await assetClientInterface.getReceiveAddressObject({ assetName, walletAccount })
120
+
121
+ const txSet = await assetClientInterface.getTxLog({ assetName, walletAccount })
122
+
123
+ const currentOrdinalsUtxos = getOrdinalsUtxos({ accountState, asset })
124
+ const transferOrdinalsUtxos = inscriptionIds
125
+ ? getTransferOrdinalsUtxos({ inscriptionIds, ordinalsUtxos: currentOrdinalsUtxos })
126
+ : undefined
127
+
128
+ const unconfirmedTxAncestor = getUnconfirmedTxAncestorMap({ accountState })
129
+ const usableUtxos = getUsableUtxos({
130
+ asset,
131
+ utxos: getUtxos({ accountState, asset }),
132
+ feeData: updatedFeeData,
133
+ txSet,
134
+ unconfirmedTxAncestor,
135
+ })
136
+
137
+ let replaceableTxs = findUnconfirmedSentRbfTxs(txSet)
138
+
139
+ let processedAddress = toAddress
140
+ if (asset.address.toLegacyAddress) {
141
+ processedAddress = asset.address.toLegacyAddress(toAddress)
142
+ }
143
+
144
+ if (assetName === 'digibyte' && asset.address.isP2SH2(processedAddress)) {
145
+ processedAddress = asset.address.P2SH2ToP2SH(processedAddress)
146
+ }
147
+
148
+ let utxosToBump
149
+ if (bumpTxId) {
150
+ const bumpTx = replaceableTxs.find(({ txId }) => txId === bumpTxId)
151
+ if (bumpTx) {
152
+ replaceableTxs = [bumpTx]
153
+ } else {
154
+ utxosToBump = usableUtxos.getTxIdUtxos(bumpTxId)
155
+ if (utxosToBump.size === 0) {
156
+ throw new Error(`Cannot bump transaction ${bumpTxId}`)
157
+ }
158
+
159
+ replaceableTxs = []
160
+ }
161
+ }
162
+
163
+ const sendAmount = bumpTxId || transferOrdinalsUtxos ? asset.currency.ZERO : amount
164
+ const receiveAddress = bumpTxId
165
+ ? replaceableTxs.length > 0
166
+ ? null
167
+ : changeAddressType
168
+ : processedAddress
169
+ const feeRate = updatedFeeData.feePerKB
170
+ const resolvedIsSendAll = (!rbfEnabled && feePerKB) || transferOrdinalsUtxos ? false : isSendAll
171
+
172
+ let { selectedUtxos, fee, replaceTx } = selectUtxos({
173
+ asset,
174
+ usableUtxos,
175
+ replaceableTxs,
176
+ amount: sendAmount,
177
+ feeRate: customFee || feeRate,
178
+ receiveAddress,
179
+ isSendAll: resolvedIsSendAll,
180
+ getFeeEstimator: (asset, { feePerKB, ...options }) =>
181
+ getFeeEstimator(asset, feePerKB, options),
182
+ mustSpendUtxos: utxosToBump,
183
+ allowUnconfirmedRbfEnabledUtxos,
184
+ unconfirmedTxAncestor,
185
+ inscriptionIds,
186
+ transferOrdinalsUtxos,
187
+ utxosDescendingOrder,
188
+ taprootInputWitnessSize,
189
+ changeAddressType,
190
+ })
191
+
192
+ if (!selectedUtxos && !replaceTx) throw new Error('Not enough funds.')
193
+
194
+ // When bumping a tx, we can either replace the tx with RBF or spend its selected change.
195
+ // If there is no selected UTXO or the tx to replace is not the tx we want to bump,
196
+ // then something is wrong because we can't actually bump the tx.
197
+ // This shouldn't happen but might due to either the tx confirming before accelerate was
198
+ // pressed, or if the change was already spent from another wallet.
199
+ if (bumpTxId && (!selectedUtxos || (replaceTx && replaceTx.txId !== bumpTxId))) {
200
+ throw new Error(`Unable to bump ${bumpTxId}`)
201
+ }
202
+
203
+ if (replaceTx) {
204
+ replaceTx = replaceTx.clone()
205
+ replaceTx = replaceTx.update({ data: { ...replaceTx.data } })
206
+ replaceTx.data.sent = replaceTx.data.sent.map((to) => {
207
+ return { ...to, amount: serializeCurrency(to.amount, asset.currency) }
208
+ })
209
+ selectedUtxos = selectedUtxos.union(
210
+ // how to avoid replace tx inputs when inputs are ordinals? !!!!
211
+ UtxoCollection.fromJSON(replaceTx.data.inputs, { currency: asset.currency })
212
+ )
213
+ }
214
+
215
+ const addressPathsMap = selectedUtxos.getAddressPathsMap()
216
+
217
+ // Inputs and Outputs
218
+ const inputs = shuffle(createInputs(assetName, selectedUtxos.toArray(), rbfEnabled))
219
+ let outputs = replaceTx
220
+ ? replaceTx.data.sent.map(({ address, amount }) =>
221
+ createOutput(assetName, address, parseCurrency(amount, asset.currency))
222
+ )
223
+ : []
224
+
225
+ // Send output
226
+ let sendOutput
227
+ if (processedAddress) {
228
+ if (transferOrdinalsUtxos) {
229
+ outputs.push(
230
+ ...transferOrdinalsUtxos
231
+ .toArray()
232
+ .map((ordinalUtxo) => createOutput(assetName, processedAddress, ordinalUtxo.value))
233
+ )
234
+ } else {
235
+ sendOutput = createOutput(assetName, processedAddress, sendAmount)
236
+ outputs.push(sendOutput)
237
+ }
238
+ }
239
+
240
+ const totalAmount = replaceTx
241
+ ? replaceTx.data.sent.reduce(
242
+ (total, { amount }) => total.add(parseCurrency(amount, asset.currency)),
243
+ sendAmount
244
+ )
245
+ : sendAmount
246
+
247
+ const change = selectedUtxos.value
248
+ .sub(totalAmount)
249
+ .sub(transferOrdinalsUtxos?.value || asset.currency.ZERO)
250
+ .sub(fee)
251
+ const dust = getChangeDustValue(asset)
252
+ let ourAddress = replaceTx?.data?.changeAddress || changeAddress
253
+ if (asset.address.toLegacyAddress) {
254
+ const legacyAddress = asset.address.toLegacyAddress(ourAddress.address)
255
+ ourAddress = Address.create(legacyAddress, ourAddress.meta)
256
+ }
257
+
258
+ // Change Output
259
+ let changeOutput
260
+ if (change.gte(dust)) {
261
+ changeOutput = createOutput(assetName, ourAddress.address ?? ourAddress.toString(), change)
262
+ // Add the keypath of change address to support Trezor detect the change output.
263
+ // Output is change and does not need approval from user which shows the strange address that user never seen.
264
+ addressPathsMap[changeAddress] = ourAddress.meta.path
265
+ outputs.push(changeOutput)
266
+ } else {
267
+ // If we don't have enough for a change output, then all remaining dust is just added to fee
268
+ fee = fee.add(change)
269
+ }
270
+
271
+ outputs = replaceTx ? outputs : shuffle(outputs)
272
+
273
+ const unsignedTx = await createUnsignedTx({
274
+ inputs,
275
+ outputs,
276
+ useCashAddress,
277
+ addressPathsMap,
278
+ blockHeight,
279
+ asset,
280
+ selectedUtxos,
281
+ insightClient,
282
+ })
283
+
284
+ return {
285
+ amount,
286
+ change,
287
+ totalAmount,
288
+ currentOrdinalsUtxos,
289
+ inscriptionIds,
290
+ address: processedAddress,
291
+ ourAddress,
292
+ receiveAddress,
293
+ sendAmount,
294
+ fee,
295
+ usableUtxos,
296
+ selectedUtxos,
297
+ transferOrdinalsUtxos,
298
+ replaceTx,
299
+ sendOutput,
300
+ changeOutput,
301
+ unsignedTx,
302
+ }
303
+ },
304
+ }
305
+
306
+ export const createTxFactory =
307
+ ({
308
+ ordinalsEnabled,
309
+ getFeeEstimator,
310
+ allowUnconfirmedRbfEnabledUtxos,
311
+ utxosDescendingOrder,
312
+ assetClientInterface,
313
+ changeAddressType,
314
+ }) =>
315
+ async ({
316
+ asset,
317
+ walletAccount,
318
+ type,
319
+ toAddress,
320
+ amount,
321
+ blockHeight,
322
+ rbfEnabled,
323
+ multipleAddressesEnabled,
324
+ feePerKB,
325
+ customFee,
326
+ isSendAll,
327
+ bumpTxId,
328
+ nft,
329
+ isExchange,
330
+ isRbfAllowed,
331
+ taprootInputWitnessSize,
332
+ }) => {
333
+ const assetName = asset.name
334
+ const accountState = await assetClientInterface.getAccountState({
335
+ assetName,
336
+ walletAccount,
337
+ })
338
+ const feeData = await assetClientInterface.getFeeConfig({ assetName })
339
+
340
+ const txHandler = getTxHandler(type)
341
+
342
+ return txHandler.buildTransaction({
343
+ asset,
344
+ walletAccount,
345
+ toAddress,
346
+ amount,
347
+ blockHeight,
348
+ rbfEnabled,
349
+ multipleAddressesEnabled,
350
+ feePerKB,
351
+ customFee,
352
+ isSendAll,
353
+ bumpTxId,
354
+ nft,
355
+ isExchange,
356
+ isRbfAllowed,
357
+ taprootInputWitnessSize,
358
+ accountState,
359
+ feeData,
360
+ ordinalsEnabled,
361
+ getFeeEstimator,
362
+ allowUnconfirmedRbfEnabledUtxos,
363
+ utxosDescendingOrder,
364
+ assetClientInterface,
365
+ changeAddressType,
366
+ })
367
+ }
@@ -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,41 @@ 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,
107
+ assetClientInterface,
108
+ blockHeight,
109
+ changeAddressType,
110
+ getFeeEstimator,
111
+ options,
112
+ ordinalsEnabled,
113
+ rbfEnabled,
114
+ utxosDescendingOrder,
115
+ walletAccount,
116
+ }) => {
117
+ const createTx = createTxFactory({
199
118
  ordinalsEnabled,
200
119
  getFeeEstimator,
201
120
  allowUnconfirmedRbfEnabledUtxos,
202
121
  utxosDescendingOrder,
203
- rbfEnabled: providedRbfEnabled,
204
122
  assetClientInterface,
205
123
  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)
124
+ })
416
125
 
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
- }
126
+ return createTx({
127
+ asset,
128
+ walletAccount,
129
+ type: 'transfer',
130
+ toAddress: address,
131
+ amount,
132
+ blockHeight,
133
+ rbfEnabled,
134
+ ...options,
135
+ })
136
+ }
447
137
 
448
138
  // not ported from Exodus; but this demos signing / broadcasting
449
139
  // NOTE: this will be ripped out in the coming weeks
@@ -459,28 +149,33 @@ export const createAndBroadcastTXFactory =
459
149
  }) =>
460
150
  async ({ asset, walletAccount, address, amount, options }) => {
461
151
  // Prepare transaction
462
- const { bumpTxId, nft, isExchange, isBip70, isRbfAllowed = true } = options
152
+ const { bumpTxId, nft, isExchange, isRbfAllowed = true } = options
463
153
 
464
154
  const assetName = asset.name
465
155
  const feeData = await assetClientInterface.getFeeConfig({ assetName })
466
156
  const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
467
157
  const insightClient = asset.baseAsset.insightClient
468
158
 
469
- const rbfEnabled = feeData.rbfEnabled && !isExchange && !isBip70 && isRbfAllowed && !nft
159
+ const rbfEnabled = feeData.rbfEnabled && !isExchange && isRbfAllowed && !nft
470
160
 
471
161
  // blockHeight
472
162
  const blockHeight = await getBlockHeight({ assetName, insightClient })
473
163
 
474
164
  const transactionDescriptor = await getPrepareSendTransaction({
475
- blockHeight,
476
- ordinalsEnabled,
477
- getFeeEstimator,
165
+ address,
478
166
  allowUnconfirmedRbfEnabledUtxos,
479
- utxosDescendingOrder,
480
- rbfEnabled,
167
+ amount,
168
+ asset,
481
169
  assetClientInterface,
170
+ blockHeight,
482
171
  changeAddressType,
483
- })({ asset, walletAccount, address, amount, options })
172
+ getFeeEstimator,
173
+ options,
174
+ ordinalsEnabled,
175
+ rbfEnabled,
176
+ utxosDescendingOrder,
177
+ walletAccount,
178
+ })
484
179
  const {
485
180
  change,
486
181
  totalAmount,
@@ -705,38 +400,5 @@ export const createAndBroadcastTXFactory =
705
400
  }
706
401
  }
707
402
 
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
403
  // back compatibiliy
742
404
  export { getSendDustValue as getDustValue } from '../dust.js'
File without changes