@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 +24 -0
- package/package.json +4 -3
- package/src/fee/get-fee-resolver.js +1 -25
- package/src/fee/utxo-selector.js +4 -24
- package/src/move-funds.js +1 -1
- package/src/send-validation.js +1 -16
- package/src/tx-create/create-tx.js +321 -0
- package/src/tx-create/tx-create-utils.js +91 -0
- package/src/tx-send/index.js +42 -408
- /package/src/{tx-send → tx-create}/dogecoin.js +0 -0
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.
|
|
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": "
|
|
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,
|
package/src/fee/utxo-selector.js
CHANGED
|
@@ -11,11 +11,7 @@ const { sortBy } = lodash
|
|
|
11
11
|
|
|
12
12
|
const MIN_RELAY_FEE = 1000
|
|
13
13
|
|
|
14
|
-
const getBestReceiveAddresses = ({ asset, receiveAddress
|
|
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 (
|
|
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 =
|
|
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-
|
|
6
|
+
import { createInputs, createOutput, getNonWitnessTxs } from './tx-create/tx-create-utils.js'
|
|
7
7
|
|
|
8
8
|
const isValidPrivateKey = (privateKey) => {
|
|
9
9
|
try {
|
package/src/send-validation.js
CHANGED
|
@@ -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
|
+
}
|
package/src/tx-send/index.js
CHANGED
|
@@ -1,72 +1,11 @@
|
|
|
1
|
-
import { getTxSequence } from '@exodus/bitcoin-lib'
|
|
2
1
|
import * as defaultBitcoinjsLib from '@exodus/bitcoinjs'
|
|
3
|
-
import { Address
|
|
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 {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
addressPathsMap,
|
|
168
|
-
blockHeight,
|
|
102
|
+
const getPrepareSendTransaction = async ({
|
|
103
|
+
address,
|
|
104
|
+
allowUnconfirmedRbfEnabledUtxos,
|
|
105
|
+
amount,
|
|
169
106
|
asset,
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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,
|
|
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 &&
|
|
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
|
-
|
|
476
|
-
ordinalsEnabled,
|
|
477
|
-
getFeeEstimator,
|
|
162
|
+
address,
|
|
478
163
|
allowUnconfirmedRbfEnabledUtxos,
|
|
479
|
-
|
|
480
|
-
|
|
164
|
+
amount,
|
|
165
|
+
asset,
|
|
481
166
|
assetClientInterface,
|
|
167
|
+
blockHeight,
|
|
482
168
|
changeAddressType,
|
|
483
|
-
|
|
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
|