@exodus/bitcoin-api 4.1.0 → 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 +22 -0
- package/package.json +4 -3
- package/src/dust.js +3 -3
- package/src/move-funds.js +1 -1
- package/src/send-validation.js +1 -16
- package/src/tx-create/create-tx.js +367 -0
- package/src/tx-create/tx-create-utils.js +91 -0
- package/src/tx-send/index.js +44 -382
- /package/src/{tx-send → tx-create}/dogecoin.js +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
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
|
+
|
|
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)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Bug Fixes
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
* fix: reduce change address dust value from 1500 to 1000 (#6603)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
6
28
|
## [4.1.0](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.0.4...@exodus/bitcoin-api@4.1.0) (2025-09-17)
|
|
7
29
|
|
|
8
30
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bitcoin-api",
|
|
3
|
-
"version": "4.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": "
|
|
63
|
+
"gitHead": "6237b0d98791ed9e6290260947068d2600bf8270"
|
|
63
64
|
}
|
package/src/dust.js
CHANGED
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,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
|
+
}
|
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,41 @@ 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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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,
|
|
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 &&
|
|
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
|
-
|
|
476
|
-
ordinalsEnabled,
|
|
477
|
-
getFeeEstimator,
|
|
165
|
+
address,
|
|
478
166
|
allowUnconfirmedRbfEnabledUtxos,
|
|
479
|
-
|
|
480
|
-
|
|
167
|
+
amount,
|
|
168
|
+
asset,
|
|
481
169
|
assetClientInterface,
|
|
170
|
+
blockHeight,
|
|
482
171
|
changeAddressType,
|
|
483
|
-
|
|
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
|