@exodus/bitcoin-api 2.9.1 → 2.9.3-hotfix

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.
Files changed (35) hide show
  1. package/package.json +23 -8
  2. package/src/bitcoinjs-lib/ecc/common.js +1 -1
  3. package/src/bitcoinjs-lib/ecc/mobile.js +3 -3
  4. package/src/bitcoinjs-lib/script-classify/index.js +1 -1
  5. package/src/btc-like-address.js +3 -4
  6. package/src/btc-like-keys.js +4 -0
  7. package/src/constants/bip44.js +2 -2
  8. package/src/fee/can-bump-tx.js +4 -2
  9. package/src/fee/fee-estimator.js +5 -1
  10. package/src/fee/fee-utils.js +6 -5
  11. package/src/fee/get-fee-resolver.js +3 -1
  12. package/src/fee/script-classifier.js +5 -10
  13. package/src/fee/utxo-selector.js +10 -3
  14. package/src/hash-utils.js +4 -9
  15. package/src/insight-api-client/index.js +16 -13
  16. package/src/insight-api-client/util.js +16 -15
  17. package/src/insight-api-client/ws.js +1 -1
  18. package/src/move-funds.js +17 -17
  19. package/src/ordinals-utils.js +2 -2
  20. package/src/parse-unsigned-tx.js +80 -81
  21. package/src/tx-log/bitcoin-monitor-scanner.js +50 -38
  22. package/src/tx-log/bitcoin-monitor.js +14 -13
  23. package/src/tx-log/ordinals-indexer-utils.js +6 -0
  24. package/src/tx-send/dogecoin.js +2 -2
  25. package/src/tx-send/index.js +426 -418
  26. package/src/tx-sign/common.js +9 -9
  27. package/src/tx-sign/create-get-key-and-purpose.js +2 -2
  28. package/src/tx-sign/create-sign-with-wallet.js +3 -3
  29. package/src/tx-sign/default-entropy.js +1 -3
  30. package/src/tx-sign/default-prepare-for-signing.js +16 -18
  31. package/src/tx-sign/default-sign-hardware.js +2 -1
  32. package/src/tx-sign/taproot.js +4 -0
  33. package/src/tx-utils.js +5 -5
  34. package/src/unconfirmed-ancestor-data.js +1 -0
  35. package/src/utxos-utils.js +20 -9
@@ -24,7 +24,7 @@ import {
24
24
  import * as defaultBitcoinjsLib from '@exodus/bitcoinjs-lib'
25
25
  import { getUnconfirmedTxAncestorMap } from '../unconfirmed-ancestor-data'
26
26
 
27
- const ASSETS_SUPPORTED_BIP_174 = [
27
+ const ASSETS_SUPPORTED_BIP_174 = new Set([
28
28
  'bitcoin',
29
29
  'bitcoinregtest',
30
30
  'bitcointestnet',
@@ -34,7 +34,7 @@ const ASSETS_SUPPORTED_BIP_174 = [
34
34
  'digibyte',
35
35
  'qtumignition',
36
36
  'vertcoin', // is not available on mobile!
37
- ]
37
+ ])
38
38
 
39
39
  const DUST_VALUES = {
40
40
  bitcoin: 6000,
@@ -43,23 +43,24 @@ const DUST_VALUES = {
43
43
  bitcoinsv: 5000,
44
44
  bcash: 6000,
45
45
  bgold: 6000,
46
- litecoin: 60000,
46
+ litecoin: 60_000,
47
47
  dash: 5500,
48
- dogecoin: 99999999,
49
- decred: 70000,
50
- digibyte: 60000,
48
+ dogecoin: 99_999_999,
49
+ decred: 70_000,
50
+ digibyte: 60_000,
51
51
  zcash: 1500,
52
- qtumignition: 400000,
52
+ qtumignition: 400_000,
53
53
  ravencoin: 545,
54
- lightningnetwork: 20000,
55
- vertcoin: 20000,
54
+ lightningnetwork: 20_000,
55
+ vertcoin: 20_000,
56
56
  }
57
57
 
58
58
  export const getDustValue = (asset) => {
59
59
  const value = DUST_VALUES[asset.name]
60
60
  if (!value) {
61
- return undefined
61
+ return
62
62
  }
63
+
63
64
  return asset.currency.baseUnit(value)
64
65
  }
65
66
 
@@ -67,16 +68,15 @@ export async function getNonWitnessTxs(asset, utxos, insightClient) {
67
68
  const rawTxs = []
68
69
 
69
70
  // BIP 174 (PSBT) requires full transaction for non-witness outputs
70
- if (ASSETS_SUPPORTED_BIP_174.includes(asset.name)) {
71
- const nonWitnessTxIds = utxos.txIds.filter(
72
- (txId) =>
73
- utxos
74
- .getAddressesForTxId(txId)
75
- .toAddressStrings()
76
- .filter((a) => asset.address.isP2PKH(a) || asset.address.isP2SH(a)).length !== 0
71
+ if (ASSETS_SUPPORTED_BIP_174.has(asset.name)) {
72
+ const nonWitnessTxIds = utxos.txIds.filter((txId) =>
73
+ utxos
74
+ .getAddressesForTxId(txId)
75
+ .toAddressStrings()
76
+ .some((a) => asset.address.isP2PKH(a) || asset.address.isP2SH(a))
77
77
  )
78
78
 
79
- if (nonWitnessTxIds.length) {
79
+ if (nonWitnessTxIds.length > 0) {
80
80
  for (const txId of nonWitnessTxIds) {
81
81
  // full transaction is required for non-witness outputs
82
82
  const rawData = await insightClient.fetchRawTx(txId)
@@ -93,471 +93,479 @@ const getSize = (tx) => {
93
93
  if (typeof tx.virtualSize === 'function') {
94
94
  return tx.virtualSize()
95
95
  }
96
+
96
97
  if (typeof tx.virtualSize === 'number') {
97
98
  return tx.virtualSize
98
99
  }
99
- return undefined
100
100
  }
101
101
 
102
- export const getSizeAndChangeScriptFactory = ({ bitcoinjsLib = defaultBitcoinjsLib } = {}) => ({
103
- assetName,
104
- tx,
105
- rawTx,
106
- changeUtxoIndex,
107
- txId,
108
- }) => {
109
- assert(assetName, 'assetName is required')
110
- assert(rawTx, 'rawTx is required')
111
- assert(typeof changeUtxoIndex === 'number', 'changeUtxoIndex must be a number')
112
-
113
- if (tx) {
114
- return {
115
- script: tx.outs?.[changeUtxoIndex]?.script.toString('hex'),
116
- size: getSize(tx),
102
+ export const getSizeAndChangeScriptFactory =
103
+ ({ bitcoinjsLib = defaultBitcoinjsLib } = {}) =>
104
+ ({ assetName, tx, rawTx, changeUtxoIndex, txId }) => {
105
+ assert(assetName, 'assetName is required')
106
+ assert(rawTx, 'rawTx is required')
107
+ assert(typeof changeUtxoIndex === 'number', 'changeUtxoIndex must be a number')
108
+
109
+ if (tx) {
110
+ return {
111
+ script: tx.outs?.[changeUtxoIndex]?.script.toString('hex'),
112
+ size: getSize(tx),
113
+ }
117
114
  }
118
- }
119
- // Trezor doesn't return tx!! we need to reparse it!
120
- const parsedTx = bitcoinjsLib.Transaction.fromBuffer(Buffer.from(rawTx, 'hex'))
121
- try {
122
- return {
123
- script: parsedTx.outs?.[changeUtxoIndex]?.script.toString('hex'),
124
- size: getSize(parsedTx),
115
+
116
+ // Trezor doesn't return tx!! we need to reparse it!
117
+ const parsedTx = bitcoinjsLib.Transaction.fromBuffer(Buffer.from(rawTx, 'hex'))
118
+ try {
119
+ return {
120
+ script: parsedTx.outs?.[changeUtxoIndex]?.script.toString('hex'),
121
+ size: getSize(parsedTx),
122
+ }
123
+ } catch (e) {
124
+ console.warn(
125
+ `tx-send warning: ${assetName} cannot extract script and size from tx ${txId}. ${e}`
126
+ )
127
+ return {}
125
128
  }
126
- } catch (e) {
127
- console.warn(
128
- `tx-send warning: ${assetName} cannot extract script and size from tx ${txId}. ${e}`
129
- )
130
- return {}
131
129
  }
132
- }
133
130
 
134
131
  // not ported from Exodus; but this demos signing / broadcasting
135
132
  // NOTE: this will be ripped out in the coming weeks
136
133
 
137
- export const createAndBroadcastTXFactory = ({
138
- getFeeEstimator,
139
- getSizeAndChangeScript = getSizeAndChangeScriptFactory(), // for decred customizations
140
- allowUnconfirmedRbfEnabledUtxos,
141
- ordinalsEnabled = false,
142
- }) => async (
143
- { asset: maybeToken, walletAccount, address, amount: tokenAmount, options },
144
- { assetClientInterface }
145
- ) => {
146
- const {
147
- multipleAddressesEnabled,
148
- feePerKB,
149
- customFee,
150
- isSendAll,
151
- isExchange,
152
- isBip70,
153
- bumpTxId,
154
- isRbfAllowed = true,
155
- nft,
156
- feeOpts,
157
- } = options
158
-
159
- const brc20 = options.brc20 || feeOpts?.brc20 // feeOpts is the only way I've found atm to pass brc20 param without changing the tx-send hydra module
160
-
161
- const asset = maybeToken.baseAsset
162
-
163
- const isToken = maybeToken.name !== asset.name
164
-
165
- if (isToken) {
166
- assert(brc20, 'brc20 is required when sending bitcoin token')
167
- }
168
-
169
- const amount = isToken ? asset.currency.ZERO : tokenAmount
170
-
171
- const assetName = asset.name
172
-
173
- const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
134
+ export const createAndBroadcastTXFactory =
135
+ ({
136
+ getFeeEstimator,
137
+ getSizeAndChangeScript = getSizeAndChangeScriptFactory(), // for decred customizations
138
+ allowUnconfirmedRbfEnabledUtxos,
139
+ ordinalsEnabled = false,
140
+ }) =>
141
+ async (
142
+ { asset: maybeToken, walletAccount, address, amount: tokenAmount, options },
143
+ { assetClientInterface }
144
+ ) => {
145
+ const {
146
+ multipleAddressesEnabled,
147
+ feePerKB,
148
+ customFee,
149
+ isSendAll,
150
+ isExchange,
151
+ isBip70,
152
+ bumpTxId,
153
+ isRbfAllowed = true,
154
+ nft,
155
+ feeOpts,
156
+ } = options
157
+
158
+ const brc20 = options.brc20 || feeOpts?.brc20 // feeOpts is the only way I've found atm to pass brc20 param without changing the tx-send hydra module
159
+
160
+ const asset = maybeToken.baseAsset
161
+
162
+ const isToken = maybeToken.name !== asset.name
163
+
164
+ if (isToken) {
165
+ assert(brc20, 'brc20 is required when sending bitcoin token')
166
+ }
174
167
 
175
- const inscriptionIds = nft?.tokenId ? [nft?.tokenId] : brc20 ? brc20.inscriptionIds : undefined
168
+ const amount = isToken ? asset.currency.ZERO : tokenAmount
176
169
 
177
- assert(
178
- ordinalsEnabled || !inscriptionIds,
179
- 'inscriptions cannot be sent when ordinalsEnabled=false '
180
- )
170
+ const assetName = asset.name
181
171
 
182
- const shuffle = (list) => {
183
- return inscriptionIds ? list : doShuffle(list) // don't shuffle when sending ordinal!!!!
184
- }
172
+ const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
185
173
 
186
- assert(assetClientInterface, `assetClientInterface must be supplied in sendTx for ${asset.name}`)
187
- assert(
188
- address || bumpTxId,
189
- 'should not be called without either a receiving address or to bump a tx'
190
- )
174
+ const inscriptionIds = nft?.tokenId ? [nft?.tokenId] : brc20 ? brc20.inscriptionIds : undefined
191
175
 
192
- if (inscriptionIds) {
193
- assert(!bumpTxId, 'only inscriptionIds or bumpTxId must be provided')
194
- assert(address, 'address must be provided when sending ordinals')
195
- }
196
-
197
- const useCashAddress = asset.address.isCashAddress?.(address)
176
+ assert(
177
+ ordinalsEnabled || !inscriptionIds,
178
+ 'inscriptions cannot be sent when ordinalsEnabled=false '
179
+ )
198
180
 
199
- const changeAddress = multipleAddressesEnabled
200
- ? await assetClientInterface.getNextChangeAddress({ assetName, walletAccount })
201
- : await assetClientInterface.getReceiveAddressObject({ assetName, walletAccount })
181
+ const shuffle = (list) => {
182
+ return inscriptionIds ? list : doShuffle(list) // don't shuffle when sending ordinal!!!!
183
+ }
202
184
 
203
- const txSet = await assetClientInterface.getTxLog({ assetName, walletAccount })
185
+ assert(
186
+ assetClientInterface,
187
+ `assetClientInterface must be supplied in sendTx for ${asset.name}`
188
+ )
189
+ assert(
190
+ address || bumpTxId,
191
+ 'should not be called without either a receiving address or to bump a tx'
192
+ )
204
193
 
205
- const currentOrdinalsUtxos = getOrdinalsUtxos({ accountState, asset })
206
- const transferOrdinalsUtxos = inscriptionIds
207
- ? getTransferOrdinalsUtxos({ inscriptionIds, ordinalsUtxos: currentOrdinalsUtxos })
208
- : undefined
194
+ if (inscriptionIds) {
195
+ assert(!bumpTxId, 'only inscriptionIds or bumpTxId must be provided')
196
+ assert(address, 'address must be provided when sending ordinals')
197
+ }
209
198
 
210
- const insightClient = asset.baseAsset.insightClient
211
- const currency = asset.currency
212
- const feeData = await assetClientInterface.getFeeConfig({ assetName })
213
- const unconfirmedTxAncestor = getUnconfirmedTxAncestorMap({ accountState })
214
- const usableUtxos = getUsableUtxos({
215
- asset,
216
- utxos: getUtxos({ accountState, asset }),
217
- feeData,
218
- txSet,
219
- unconfirmedTxAncestor,
220
- })
199
+ const useCashAddress = asset.address.isCashAddress?.(address)
200
+
201
+ const changeAddress = multipleAddressesEnabled
202
+ ? await assetClientInterface.getNextChangeAddress({ assetName, walletAccount })
203
+ : await assetClientInterface.getReceiveAddressObject({ assetName, walletAccount })
204
+
205
+ const txSet = await assetClientInterface.getTxLog({ assetName, walletAccount })
206
+
207
+ const currentOrdinalsUtxos = getOrdinalsUtxos({ accountState, asset })
208
+ const transferOrdinalsUtxos = inscriptionIds
209
+ ? getTransferOrdinalsUtxos({ inscriptionIds, ordinalsUtxos: currentOrdinalsUtxos })
210
+ : undefined
211
+
212
+ const insightClient = asset.baseAsset.insightClient
213
+ const currency = asset.currency
214
+ const feeData = await assetClientInterface.getFeeConfig({ assetName })
215
+ const unconfirmedTxAncestor = getUnconfirmedTxAncestorMap({ accountState })
216
+ const usableUtxos = getUsableUtxos({
217
+ asset,
218
+ utxos: getUtxos({ accountState, asset }),
219
+ feeData,
220
+ txSet,
221
+ unconfirmedTxAncestor,
222
+ })
221
223
 
222
- let replaceableTxs = findUnconfirmedSentRbfTxs(txSet)
224
+ let replaceableTxs = findUnconfirmedSentRbfTxs(txSet)
223
225
 
224
- if (asset.address.toLegacyAddress) {
225
- address = asset.address.toLegacyAddress(address)
226
- }
226
+ if (asset.address.toLegacyAddress) {
227
+ address = asset.address.toLegacyAddress(address)
228
+ }
227
229
 
228
- if (assetName === 'digibyte') {
229
- if (asset.address.isP2SH2(address)) {
230
+ if (assetName === 'digibyte' && asset.address.isP2SH2(address)) {
230
231
  address = asset.address.P2SH2ToP2SH(address)
231
232
  }
232
- }
233
233
 
234
- const rbfEnabled = feeData.rbfEnabled && !isExchange && !isBip70 && isRbfAllowed && !nft && !brc20
234
+ const rbfEnabled =
235
+ feeData.rbfEnabled && !isExchange && !isBip70 && isRbfAllowed && !nft && !brc20
236
+
237
+ let utxosToBump
238
+ if (bumpTxId) {
239
+ const bumpTx = replaceableTxs.find(({ txId }) => txId === bumpTxId)
240
+ if (bumpTx) {
241
+ replaceableTxs = [bumpTx]
242
+ } else {
243
+ utxosToBump = usableUtxos.getTxIdUtxos(bumpTxId)
244
+ if (utxosToBump.size === 0) {
245
+ throw new Error(`Cannot bump transaction ${bumpTxId}`)
246
+ }
235
247
 
236
- let utxosToBump
237
- if (bumpTxId) {
238
- const bumpTx = replaceableTxs.find(({ txId }) => txId === bumpTxId)
239
- if (!bumpTx) {
240
- utxosToBump = usableUtxos.getTxIdUtxos(bumpTxId)
241
- if (utxosToBump.size === 0) {
242
- throw new Error(`Cannot bump transaction ${bumpTxId}`)
248
+ replaceableTxs = []
243
249
  }
244
- replaceableTxs = []
245
- } else {
246
- replaceableTxs = [bumpTx]
247
250
  }
248
- }
249
251
 
250
- const sendAmount = bumpTxId || transferOrdinalsUtxos ? asset.currency.ZERO : amount.toDefault()
251
- const receiveAddress = bumpTxId ? (replaceableTxs.length > 0 ? null : 'P2WPKH') : address
252
- const feeRate = feeData.feePerKB
253
- const resolvedIsSendAll = (!rbfEnabled && feePerKB) || transferOrdinalsUtxos ? false : isSendAll
254
-
255
- let { selectedUtxos, fee, replaceTx } = selectUtxos({
256
- asset,
257
- usableUtxos,
258
- replaceableTxs,
259
- amount: sendAmount,
260
- feeRate: customFee || feeRate,
261
- receiveAddress: receiveAddress,
262
- isSendAll: resolvedIsSendAll,
263
- getFeeEstimator: (asset, { feePerKB, ...options }) => getFeeEstimator(asset, feePerKB, options),
264
- mustSpendUtxos: utxosToBump,
265
- allowUnconfirmedRbfEnabledUtxos,
266
- unconfirmedTxAncestor,
267
- inscriptionIds,
268
- })
269
-
270
- if (!selectedUtxos && !replaceTx) throw new Error('Not enough funds.')
271
-
272
- // When bumping a tx, we can either replace the tx with RBF or spend its selected change.
273
- // If there is no selected UTXO or the tx to replace is not the tx we want to bump,
274
- // then something is wrong because we can't actually bump the tx.
275
- // This shouldn't happen but might due to either the tx confirming before accelerate was
276
- // pressed, or if the change was already spent from another wallet.
277
- if (bumpTxId && (!selectedUtxos || (replaceTx && replaceTx.txId !== bumpTxId))) {
278
- throw new Error(`Unable to bump ${bumpTxId}`)
279
- }
280
- if (transferOrdinalsUtxos) {
281
- selectedUtxos = transferOrdinalsUtxos.union(selectedUtxos)
282
- }
283
- if (replaceTx) {
284
- replaceTx = replaceTx.clone()
285
- replaceTx = replaceTx.update({ data: { ...replaceTx.data } })
286
- replaceTx.data.sent = replaceTx.data.sent.map((to) => {
287
- return { ...to, amount: parseCurrency(to.amount, asset.currency) }
252
+ const sendAmount = bumpTxId || transferOrdinalsUtxos ? asset.currency.ZERO : amount.toDefault()
253
+ const receiveAddress = bumpTxId ? (replaceableTxs.length > 0 ? null : 'P2WPKH') : address
254
+ const feeRate = feeData.feePerKB
255
+ const resolvedIsSendAll = (!rbfEnabled && feePerKB) || transferOrdinalsUtxos ? false : isSendAll
256
+
257
+ let { selectedUtxos, fee, replaceTx } = selectUtxos({
258
+ asset,
259
+ usableUtxos,
260
+ replaceableTxs,
261
+ amount: sendAmount,
262
+ feeRate: customFee || feeRate,
263
+ receiveAddress,
264
+ isSendAll: resolvedIsSendAll,
265
+ getFeeEstimator: (asset, { feePerKB, ...options }) =>
266
+ getFeeEstimator(asset, feePerKB, options),
267
+ mustSpendUtxos: utxosToBump,
268
+ allowUnconfirmedRbfEnabledUtxos,
269
+ unconfirmedTxAncestor,
270
+ inscriptionIds,
288
271
  })
289
- selectedUtxos = selectedUtxos.union(
290
- // how to avoid replace tx inputs when inputs are ordinals? !!!!
291
- UtxoCollection.fromJSON(replaceTx.data.inputs, { currency: asset.currency })
292
- )
293
- }
294
- const addressPathsMap = selectedUtxos.getAddressPathsMap()
295
272
 
296
- // transform UTXO object to raw
297
- const inputs = shuffle(createInputs(assetName, selectedUtxos.toArray(), rbfEnabled))
273
+ if (!selectedUtxos && !replaceTx) throw new Error('Not enough funds.')
298
274
 
299
- let outputs
300
- if (replaceTx) {
301
- outputs = replaceTx.data.sent.map(({ address, amount }) =>
302
- createOutput(assetName, address, amount)
303
- )
304
- } else {
305
- outputs = []
306
- }
275
+ // When bumping a tx, we can either replace the tx with RBF or spend its selected change.
276
+ // If there is no selected UTXO or the tx to replace is not the tx we want to bump,
277
+ // then something is wrong because we can't actually bump the tx.
278
+ // This shouldn't happen but might due to either the tx confirming before accelerate was
279
+ // pressed, or if the change was already spent from another wallet.
280
+ if (bumpTxId && (!selectedUtxos || (replaceTx && replaceTx.txId !== bumpTxId))) {
281
+ throw new Error(`Unable to bump ${bumpTxId}`)
282
+ }
307
283
 
308
- let sendOutput
309
- if (address) {
310
284
  if (transferOrdinalsUtxos) {
311
- outputs.push(
312
- ...transferOrdinalsUtxos
313
- .toArray()
314
- .map((ordinalUtxo) => createOutput(assetName, address, ordinalUtxo.value))
285
+ selectedUtxos = transferOrdinalsUtxos.union(selectedUtxos)
286
+ }
287
+
288
+ if (replaceTx) {
289
+ replaceTx = replaceTx.clone()
290
+ replaceTx = replaceTx.update({ data: { ...replaceTx.data } })
291
+ replaceTx.data.sent = replaceTx.data.sent.map((to) => {
292
+ return { ...to, amount: parseCurrency(to.amount, asset.currency) }
293
+ })
294
+ selectedUtxos = selectedUtxos.union(
295
+ // how to avoid replace tx inputs when inputs are ordinals? !!!!
296
+ UtxoCollection.fromJSON(replaceTx.data.inputs, { currency: asset.currency })
315
297
  )
316
- } else {
317
- sendOutput = createOutput(assetName, address, sendAmount)
318
- outputs.push(sendOutput)
319
298
  }
320
- }
321
299
 
322
- const totalAmount = replaceTx
323
- ? replaceTx.data.sent.reduce((total, { amount }) => total.add(amount), sendAmount)
324
- : sendAmount
325
-
326
- const change = selectedUtxos.value
327
- .sub(totalAmount)
328
- .sub(transferOrdinalsUtxos?.value || currency.ZERO)
329
- .sub(fee)
330
- const dust = getDustValue(asset)
331
- let ourAddress = replaceTx?.data?.changeAddress || changeAddress
332
- if (asset.address.toLegacyAddress) {
333
- const legacyAddress = asset.address.toLegacyAddress(ourAddress.address)
334
- ourAddress = Address.create(legacyAddress, ourAddress.meta)
335
- }
336
- let changeOutput
337
- if (change.gte(dust)) {
338
- changeOutput = createOutput(
339
- assetName,
340
- ourAddress.address ? ourAddress.address : ourAddress.toString(),
341
- change
342
- )
343
- // Add the keypath of change address to support Trezor detect the change output.
344
- // Output is change and does not need approval from user which shows the strange address that user never seen.
345
- addressPathsMap[changeAddress] = ourAddress.meta.path
346
- outputs.push(changeOutput)
347
- } else {
348
- // If we don't have enough for a change output, then all remaining dust is just added to fee
349
- fee = fee.add(change)
350
- }
300
+ const addressPathsMap = selectedUtxos.getAddressPathsMap()
351
301
 
352
- outputs = replaceTx ? outputs : shuffle(outputs)
353
- const blockHeight = ['zcash', 'bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)
354
- ? await insightClient.fetchBlockHeight()
355
- : 0
356
-
357
- // desktop/mobile shared format
358
- const unsignedTx = {
359
- txData: {
360
- inputs,
361
- outputs,
362
- },
363
- txMeta: {
364
- useCashAddress, // for trezor to show the receiver cash address
365
- addressPathsMap,
366
- blockHeight,
367
- },
368
- }
302
+ // transform UTXO object to raw
303
+ const inputs = shuffle(createInputs(assetName, selectedUtxos.toArray(), rbfEnabled))
304
+
305
+ let outputs
306
+ if (replaceTx) {
307
+ outputs = replaceTx.data.sent.map(({ address, amount }) =>
308
+ createOutput(assetName, address, amount)
309
+ )
310
+ } else {
311
+ outputs = []
312
+ }
369
313
 
370
- const nonWitnessTxs = await getNonWitnessTxs(asset, selectedUtxos, insightClient)
371
- Object.assign(unsignedTx.txMeta, { rawTxs: nonWitnessTxs })
372
- const { rawTx, txId, tx } = await assetClientInterface.signTransaction({
373
- assetName,
374
- unsignedTx,
375
- walletAccount,
376
- })
377
-
378
- const broadcastTxWithRetry = retry(
379
- async (rawTx) => {
380
- try {
381
- return await asset.api.broadcastTx(rawTx)
382
- } catch (e) {
383
- if (
384
- /missing inputs/i.test(e.message) ||
385
- /absurdly-high-fee/.test(e.message) ||
386
- /too-long-mempool-chain/.test(e.message) ||
387
- /txn-mempool-conflict/.test(e.message) ||
388
- /tx-size/.test(e.message) ||
389
- /txn-already-in-mempool/.test(e.message)
314
+ let sendOutput
315
+ if (address) {
316
+ if (transferOrdinalsUtxos) {
317
+ outputs.push(
318
+ ...transferOrdinalsUtxos
319
+ .toArray()
320
+ .map((ordinalUtxo) => createOutput(assetName, address, ordinalUtxo.value))
390
321
  )
391
- e.finalError = true
392
- throw e
322
+ } else {
323
+ sendOutput = createOutput(assetName, address, sendAmount)
324
+ outputs.push(sendOutput)
393
325
  }
394
- },
395
- { delayTimesMs: ['10s'] }
396
- )
397
-
398
- try {
399
- await broadcastTxWithRetry(rawTx.toString('hex'))
400
- } catch (err) {
401
- if (err.message.includes('txn-already-in-mempool')) {
402
- // It's not an error, we must ignore it.
403
- console.log('Transaction is already in the mempool.')
404
- } else if (err.message.match(/Insight Broadcast HTTP Error.*Missing inputs/i)) {
405
- err.txInfo = JSON.stringify({
406
- amount: sendAmount.toDefaultString({ unit: true }),
407
- fee: ((fee && fee.toDefaultString({ unit: true })) || 0).toString(), // todo why does 0 not have a unit? Is default unit ok here?
408
- allUtxos: usableUtxos.toJSON(),
409
- })
410
- throw err
326
+ }
327
+
328
+ const totalAmount = replaceTx
329
+ ? replaceTx.data.sent.reduce((total, { amount }) => total.add(amount), sendAmount)
330
+ : sendAmount
331
+
332
+ const change = selectedUtxos.value
333
+ .sub(totalAmount)
334
+ .sub(transferOrdinalsUtxos?.value || currency.ZERO)
335
+ .sub(fee)
336
+ const dust = getDustValue(asset)
337
+ let ourAddress = replaceTx?.data?.changeAddress || changeAddress
338
+ if (asset.address.toLegacyAddress) {
339
+ const legacyAddress = asset.address.toLegacyAddress(ourAddress.address)
340
+ ourAddress = Address.create(legacyAddress, ourAddress.meta)
341
+ }
342
+
343
+ let changeOutput
344
+ if (change.gte(dust)) {
345
+ changeOutput = createOutput(assetName, ourAddress.address ?? ourAddress.toString(), change)
346
+ // Add the keypath of change address to support Trezor detect the change output.
347
+ // Output is change and does not need approval from user which shows the strange address that user never seen.
348
+ addressPathsMap[changeAddress] = ourAddress.meta.path
349
+ outputs.push(changeOutput)
411
350
  } else {
412
- throw err
351
+ // If we don't have enough for a change output, then all remaining dust is just added to fee
352
+ fee = fee.add(change)
413
353
  }
414
- }
415
354
 
416
- function findUtxoIndex(output) {
417
- let utxoIndex = -1
418
- if (output) {
419
- for (let i = 0; i < outputs.length; i++) {
420
- const [address, amount] = outputs[i]
421
- if (output[0] === address && output[1] === amount) {
422
- utxoIndex = i
423
- break
424
- }
425
- }
355
+ outputs = replaceTx ? outputs : shuffle(outputs)
356
+ const blockHeight = ['zcash', 'bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)
357
+ ? await insightClient.fetchBlockHeight()
358
+ : 0
359
+
360
+ // desktop/mobile shared format
361
+ const unsignedTx = {
362
+ txData: {
363
+ inputs,
364
+ outputs,
365
+ },
366
+ txMeta: {
367
+ useCashAddress, // for trezor to show the receiver cash address
368
+ addressPathsMap,
369
+ blockHeight,
370
+ },
426
371
  }
427
- return utxoIndex
428
- }
429
372
 
430
- const changeUtxoIndex = findUtxoIndex(changeOutput)
431
- const sendUtxoIndex = findUtxoIndex(sendOutput)
373
+ const nonWitnessTxs = await getNonWitnessTxs(asset, selectedUtxos, insightClient)
374
+ Object.assign(unsignedTx.txMeta, { rawTxs: nonWitnessTxs })
375
+ const { rawTx, txId, tx } = await assetClientInterface.signTransaction({
376
+ assetName,
377
+ unsignedTx,
378
+ walletAccount,
379
+ })
432
380
 
433
- const { script, size } = getSizeAndChangeScript({ assetName, tx, rawTx, changeUtxoIndex, txId })
381
+ const broadcastTxWithRetry = retry(
382
+ async (rawTx) => {
383
+ try {
384
+ return await asset.api.broadcastTx(rawTx)
385
+ } catch (e) {
386
+ if (
387
+ /missing inputs/i.test(e.message) ||
388
+ /absurdly-high-fee/.test(e.message) ||
389
+ /too-long-mempool-chain/.test(e.message) ||
390
+ /txn-mempool-conflict/.test(e.message) ||
391
+ /tx-size/.test(e.message) ||
392
+ /txn-already-in-mempool/.test(e.message)
393
+ )
394
+ e.finalError = true
395
+ throw e
396
+ }
397
+ },
398
+ { delayTimesMs: ['10s'] }
399
+ )
434
400
 
435
- // for ordinals, used to allow users spending change utxos even when unconfirmed and ordinals are unknown
436
- const knownBalanceUtxoIds = accountState.knownBalanceUtxoIds || []
437
- let remainingUtxos = usableUtxos.difference(selectedUtxos)
438
- if (changeUtxoIndex !== -1) {
439
- const address = Address.create(ourAddress.address, ourAddress.meta)
440
- const changeUtxo = {
441
- txId,
442
- address,
443
- vout: changeUtxoIndex,
444
- script,
445
- value: change,
446
- confirmations: 0,
447
- rbfEnabled,
401
+ try {
402
+ await broadcastTxWithRetry(rawTx.toString('hex'))
403
+ } catch (err) {
404
+ if (err.message.includes('txn-already-in-mempool')) {
405
+ // It's not an error, we must ignore it.
406
+ console.log('Transaction is already in the mempool.')
407
+ } else if (/insight broadcast http error.*missing inputs/i.test(err.message)) {
408
+ err.txInfo = JSON.stringify({
409
+ amount: sendAmount.toDefaultString({ unit: true }),
410
+ fee: ((fee && fee.toDefaultString({ unit: true })) || 0).toString(), // todo why does 0 not have a unit? Is default unit ok here?
411
+ allUtxos: usableUtxos.toJSON(),
412
+ })
413
+ throw err
414
+ } else {
415
+ throw err
416
+ }
448
417
  }
449
418
 
450
- knownBalanceUtxoIds.push(`${changeUtxo.txId}:${changeUtxo.vout}`.toLowerCase())
451
- remainingUtxos = remainingUtxos.addUtxo(changeUtxo)
452
- }
453
- if (replaceTx) {
454
- remainingUtxos = remainingUtxos.difference(remainingUtxos.getTxIdUtxos(replaceTx.txId))
455
- }
419
+ function findUtxoIndex(output) {
420
+ let utxoIndex = -1
421
+ if (output) {
422
+ for (const [i, [address, amount]] of outputs.entries()) {
423
+ if (output[0] === address && output[1] === amount) {
424
+ utxoIndex = i
425
+ break
426
+ }
427
+ }
428
+ }
456
429
 
457
- const remainingOrdinalsUtxos = transferOrdinalsUtxos
458
- ? currentOrdinalsUtxos.difference(transferOrdinalsUtxos)
459
- : currentOrdinalsUtxos
460
-
461
- await assetClientInterface.updateAccountState({
462
- assetName,
463
- walletAccount,
464
- newData: {
465
- utxos: remainingUtxos,
466
- ordinalsUtxos: remainingOrdinalsUtxos,
467
- knownBalanceUtxoIds,
468
- },
469
- })
470
-
471
- const walletAddresses = await assetClientInterface.getReceiveAddresses({
472
- walletAccount,
473
- assetName,
474
- multiAddressMode: true,
475
- })
476
- // There are two cases of bumping, replacing or chaining a self-send.
477
- // If we have a bumpTxId, but we aren't replacing, then it is a self-send.
478
- const selfSend = bumpTxId
479
- ? !replaceTx
480
- : walletAddresses.some((receiveAddress) => String(receiveAddress) === String(address))
481
-
482
- const displayReceiveAddress = asset.address.displayAddress?.(receiveAddress) || receiveAddress
483
-
484
- const receivers = bumpTxId
485
- ? replaceTx
486
- ? replaceTx.data.sent
487
- : []
488
- : replaceTx
489
- ? replaceTx.data.sent.concat([{ address: displayReceiveAddress, amount }])
490
- : [{ address: displayReceiveAddress, amount }]
491
-
492
- const calculateCoinAmount = () => {
493
- if (selfSend) {
494
- return maybeToken.currency.ZERO
495
- } else if (isToken) {
496
- return tokenAmount.abs().negate()
497
- } else if (nft) {
498
- return transferOrdinalsUtxos.value.abs().negate()
499
- } else {
500
- return totalAmount.abs().negate()
430
+ return utxoIndex
501
431
  }
502
- }
503
432
 
504
- const coinAmount = calculateCoinAmount()
433
+ const changeUtxoIndex = findUtxoIndex(changeOutput)
434
+ const sendUtxoIndex = findUtxoIndex(sendOutput)
435
+
436
+ const { script, size } = getSizeAndChangeScript({ assetName, tx, rawTx, changeUtxoIndex, txId })
505
437
 
506
- await assetClientInterface.updateTxLogAndNotify({
507
- assetName: maybeToken.name,
508
- walletAccount,
509
- txs: [
510
- {
438
+ // for ordinals, used to allow users spending change utxos even when unconfirmed and ordinals are unknown
439
+ const knownBalanceUtxoIds = accountState.knownBalanceUtxoIds || []
440
+ let remainingUtxos = usableUtxos.difference(selectedUtxos)
441
+ if (changeUtxoIndex !== -1) {
442
+ const address = Address.create(ourAddress.address, ourAddress.meta)
443
+ const changeUtxo = {
511
444
  txId,
445
+ address,
446
+ vout: changeUtxoIndex,
447
+ script,
448
+ value: change,
512
449
  confirmations: 0,
513
- coinAmount,
514
- coinName: maybeToken.name,
515
- feeAmount: fee,
516
- feeCoinName: assetName,
517
- selfSend,
518
- data: {
519
- sent: selfSend ? [] : receivers,
520
- rbfEnabled,
521
- feePerKB: size ? fee.div(size / 1000).toBaseNumber() : undefined,
522
- changeAddress: changeOutput ? ourAddress : undefined,
523
- blockHeight,
524
- blocksSeen: 0,
525
- inputs: selectedUtxos.toJSON(),
526
- replacedTxId: replaceTx ? replaceTx.txId : undefined,
527
- nftId: nft ? `${assetName}:${nft.tokenId}` : undefined, // it allows BE to load the nft info while the nft is in transit
528
- inscriptionsIndexed: ordinalsEnabled ? true : undefined,
529
- sentInscriptions: inscriptionIds
530
- ? inscriptionIds.map((inscriptionId) => {
531
- return {
532
- inscriptionId,
533
- offset: 0,
534
- value: 0,
535
- }
536
- })
537
- : undefined,
538
- },
450
+ rbfEnabled,
451
+ }
452
+
453
+ knownBalanceUtxoIds.push(`${changeUtxo.txId}:${changeUtxo.vout}`.toLowerCase())
454
+ remainingUtxos = remainingUtxos.addUtxo(changeUtxo)
455
+ }
456
+
457
+ if (replaceTx) {
458
+ remainingUtxos = remainingUtxos.difference(remainingUtxos.getTxIdUtxos(replaceTx.txId))
459
+ }
460
+
461
+ const remainingOrdinalsUtxos = transferOrdinalsUtxos
462
+ ? currentOrdinalsUtxos.difference(transferOrdinalsUtxos)
463
+ : currentOrdinalsUtxos
464
+
465
+ await assetClientInterface.updateAccountState({
466
+ assetName,
467
+ walletAccount,
468
+ newData: {
469
+ utxos: remainingUtxos,
470
+ ordinalsUtxos: remainingOrdinalsUtxos,
471
+ knownBalanceUtxoIds,
539
472
  },
540
- ],
541
- })
473
+ })
542
474
 
543
- // If we are replacing the tx, add the replacedBy info to the previous tx to update UI
544
- // Also, clone the personal note and attach it to the new tx so it is not lost
545
- if (replaceTx) {
546
- replaceTx.data.replacedBy = txId
547
- await assetClientInterface.updateTxLogAndNotify({
475
+ const walletAddressObjects = await assetClientInterface.getReceiveAddresses({
476
+ walletAccount,
548
477
  assetName,
478
+ multiAddressMode: true,
479
+ })
480
+ // There are two cases of bumping, replacing or chaining a self-send.
481
+ // If we have a bumpTxId, but we aren't replacing, then it is a self-send.
482
+ const selfSend = bumpTxId
483
+ ? !replaceTx
484
+ : walletAddressObjects.some((receiveAddress) => String(receiveAddress) === String(address))
485
+
486
+ const displayReceiveAddress = asset.address.displayAddress?.(receiveAddress) || receiveAddress
487
+
488
+ const receivers = bumpTxId
489
+ ? replaceTx
490
+ ? replaceTx.data.sent
491
+ : []
492
+ : replaceTx
493
+ ? [...replaceTx.data.sent, { address: displayReceiveAddress, amount }]
494
+ : [{ address: displayReceiveAddress, amount }]
495
+
496
+ const calculateCoinAmount = () => {
497
+ if (selfSend) {
498
+ return maybeToken.currency.ZERO
499
+ }
500
+
501
+ if (isToken) {
502
+ return tokenAmount.abs().negate()
503
+ }
504
+
505
+ if (nft) {
506
+ return transferOrdinalsUtxos.value.abs().negate()
507
+ }
508
+
509
+ return totalAmount.abs().negate()
510
+ }
511
+
512
+ const coinAmount = calculateCoinAmount()
513
+
514
+ await assetClientInterface.updateTxLogAndNotify({
515
+ assetName: maybeToken.name,
549
516
  walletAccount,
550
- txs: [replaceTx],
517
+ txs: [
518
+ {
519
+ txId,
520
+ confirmations: 0,
521
+ coinAmount,
522
+ coinName: maybeToken.name,
523
+ feeAmount: fee,
524
+ feeCoinName: assetName,
525
+ selfSend,
526
+ data: {
527
+ sent: selfSend ? [] : receivers,
528
+ rbfEnabled,
529
+ feePerKB: size ? fee.div(size / 1000).toBaseNumber() : undefined,
530
+ changeAddress: changeOutput ? ourAddress : undefined,
531
+ blockHeight,
532
+ blocksSeen: 0,
533
+ inputs: selectedUtxos.toJSON(),
534
+ replacedTxId: replaceTx ? replaceTx.txId : undefined,
535
+ nftId: nft ? `${assetName}:${nft.tokenId}` : undefined, // it allows BE to load the nft info while the nft is in transit
536
+ inscriptionsIndexed: ordinalsEnabled ? true : undefined,
537
+ sentInscriptions: inscriptionIds
538
+ ? inscriptionIds.map((inscriptionId) => {
539
+ return {
540
+ inscriptionId,
541
+ offset: 0,
542
+ value: 0,
543
+ }
544
+ })
545
+ : undefined,
546
+ },
547
+ },
548
+ ],
551
549
  })
552
- }
553
550
 
554
- return {
555
- txId,
556
- sendUtxoIndex,
557
- sendAmount: sendAmount.toBaseNumber(),
558
- replacedTxId: replaceTx?.txId,
551
+ // If we are replacing the tx, add the replacedBy info to the previous tx to update UI
552
+ // Also, clone the personal note and attach it to the new tx so it is not lost
553
+ if (replaceTx) {
554
+ replaceTx.data.replacedBy = txId
555
+ await assetClientInterface.updateTxLogAndNotify({
556
+ assetName,
557
+ walletAccount,
558
+ txs: [replaceTx],
559
+ })
560
+ }
561
+
562
+ return {
563
+ txId,
564
+ sendUtxoIndex,
565
+ sendAmount: sendAmount.toBaseNumber(),
566
+ replacedTxId: replaceTx?.txId,
567
+ }
559
568
  }
560
- }
561
569
 
562
570
  export function createInputs(assetName, ...rest) {
563
571
  switch (assetName) {