@exodus/bitcoin-api 4.1.4 → 4.1.6
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 +16 -0
- package/package.json +2 -2
- package/src/tx-create/create-tx.js +333 -133
- package/src/tx-send/index.js +15 -17
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
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.6](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.1.5...@exodus/bitcoin-api@4.1.6) (2025-10-16)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @exodus/bitcoin-api
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [4.1.5](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.1.4...@exodus/bitcoin-api@4.1.5) (2025-10-16)
|
|
15
|
+
|
|
16
|
+
**Note:** Version bump only for package @exodus/bitcoin-api
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
## [4.1.4](https://github.com/ExodusMovement/assets/compare/@exodus/bitcoin-api@4.1.3...@exodus/bitcoin-api@4.1.4) (2025-10-15)
|
|
7
23
|
|
|
8
24
|
**Note:** Version bump only for package @exodus/bitcoin-api
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bitcoin-api",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.6",
|
|
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",
|
|
@@ -60,5 +60,5 @@
|
|
|
60
60
|
"type": "git",
|
|
61
61
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
62
62
|
},
|
|
63
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "f193d5effadc719b3fb4ba7d32fc46c0979f258a"
|
|
64
64
|
}
|
|
@@ -10,6 +10,255 @@ import { getUnconfirmedTxAncestorMap } from '../unconfirmed-ancestor-data.js'
|
|
|
10
10
|
import { getUsableUtxos, getUtxos } from '../utxos-utils.js'
|
|
11
11
|
import { createInputs, createOutput, getBlockHeight, getNonWitnessTxs } from './tx-create-utils.js'
|
|
12
12
|
|
|
13
|
+
// Helper to shuffle arrays for randomized input/output ordering
|
|
14
|
+
const shuffle = (list) => {
|
|
15
|
+
return lodash.shuffle(list)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function validateTransactionParams({ assetClientInterface, asset, toAddress, bumpTxId }) {
|
|
19
|
+
assert(assetClientInterface, `assetClientInterface must be supplied in sendTx for ${asset.name}`)
|
|
20
|
+
assert(
|
|
21
|
+
toAddress || bumpTxId,
|
|
22
|
+
'should not be called without either a receiving toAddress or to bump a tx'
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function prepareTransactionContext({
|
|
27
|
+
asset,
|
|
28
|
+
assetName,
|
|
29
|
+
walletAccount,
|
|
30
|
+
multipleAddressesEnabled,
|
|
31
|
+
assetClientInterface,
|
|
32
|
+
accountState,
|
|
33
|
+
insightClient,
|
|
34
|
+
feeData,
|
|
35
|
+
feePerKB,
|
|
36
|
+
isExchange,
|
|
37
|
+
isRbfAllowed,
|
|
38
|
+
}) {
|
|
39
|
+
const updatedFeeData = { ...feeData, feePerKB: feePerKB ?? feeData.feePerKB }
|
|
40
|
+
|
|
41
|
+
const blockHeight = await getBlockHeight({ assetName, insightClient })
|
|
42
|
+
|
|
43
|
+
const rbfEnabled = updatedFeeData.rbfEnabled && !isExchange && isRbfAllowed
|
|
44
|
+
|
|
45
|
+
const changeAddress = multipleAddressesEnabled
|
|
46
|
+
? await assetClientInterface.getNextChangeAddress({ assetName, walletAccount })
|
|
47
|
+
: await assetClientInterface.getReceiveAddressObject({ assetName, walletAccount })
|
|
48
|
+
|
|
49
|
+
const txSet = await assetClientInterface.getTxLog({ assetName, walletAccount })
|
|
50
|
+
|
|
51
|
+
const unconfirmedTxAncestor = getUnconfirmedTxAncestorMap({ accountState })
|
|
52
|
+
|
|
53
|
+
const usableUtxos = getUsableUtxos({
|
|
54
|
+
asset,
|
|
55
|
+
utxos: getUtxos({ accountState, asset }),
|
|
56
|
+
feeData: updatedFeeData,
|
|
57
|
+
txSet,
|
|
58
|
+
unconfirmedTxAncestor,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
updatedFeeData,
|
|
63
|
+
blockHeight,
|
|
64
|
+
rbfEnabled,
|
|
65
|
+
changeAddress,
|
|
66
|
+
txSet,
|
|
67
|
+
unconfirmedTxAncestor,
|
|
68
|
+
usableUtxos,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Process and normalize addresses
|
|
73
|
+
function processAddress({ asset, toAddress }) {
|
|
74
|
+
let processedAddress = toAddress
|
|
75
|
+
|
|
76
|
+
if (asset.address.toLegacyAddress) {
|
|
77
|
+
processedAddress = asset.address.toLegacyAddress(toAddress)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (asset.name === 'digibyte' && asset.address.isP2SH2(processedAddress)) {
|
|
81
|
+
processedAddress = asset.address.P2SH2ToP2SH(processedAddress)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const useCashAddress = asset.address.isCashAddress?.(toAddress)
|
|
85
|
+
|
|
86
|
+
return { processedAddress, useCashAddress }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Determine strategy for transaction bumping (RBF vs CPFP)
|
|
90
|
+
function determineBumpStrategy({ bumpTxId, replaceableTxs, usableUtxos }) {
|
|
91
|
+
if (!bumpTxId) {
|
|
92
|
+
return { replaceableTxs, utxosToBump: undefined }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check if we can use RBF (Replace-By-Fee)
|
|
96
|
+
const bumpTx = replaceableTxs.find(({ txId }) => txId === bumpTxId)
|
|
97
|
+
|
|
98
|
+
if (bumpTx) {
|
|
99
|
+
// Use RBF: replace the transaction directly
|
|
100
|
+
return { replaceableTxs: [bumpTx], utxosToBump: undefined }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Otherwise try CPFP (Child-Pays-For-Parent) by spending the transaction's outputs
|
|
104
|
+
const utxosToBump = usableUtxos.getTxIdUtxos(bumpTxId)
|
|
105
|
+
if (utxosToBump.size === 0) {
|
|
106
|
+
throw new Error(`Cannot bump transaction ${bumpTxId}`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { replaceableTxs: [], utxosToBump }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Prepare parameters for UTXO selection
|
|
113
|
+
function prepareUtxoSelectionParams({
|
|
114
|
+
bumpTxId,
|
|
115
|
+
asset,
|
|
116
|
+
amount,
|
|
117
|
+
replaceableTxs,
|
|
118
|
+
processedAddress,
|
|
119
|
+
changeAddressType,
|
|
120
|
+
updatedFeeData,
|
|
121
|
+
rbfEnabled,
|
|
122
|
+
feePerKB,
|
|
123
|
+
customFee,
|
|
124
|
+
isSendAll,
|
|
125
|
+
}) {
|
|
126
|
+
const sendAmount = bumpTxId ? asset.currency.ZERO : amount
|
|
127
|
+
|
|
128
|
+
const receiveAddress = bumpTxId
|
|
129
|
+
? replaceableTxs.length > 0
|
|
130
|
+
? null
|
|
131
|
+
: changeAddressType
|
|
132
|
+
: processedAddress
|
|
133
|
+
|
|
134
|
+
const feeRate = customFee || updatedFeeData.feePerKB
|
|
135
|
+
const resolvedIsSendAll = !rbfEnabled && feePerKB ? false : isSendAll
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
sendAmount,
|
|
139
|
+
receiveAddress,
|
|
140
|
+
feeRate,
|
|
141
|
+
resolvedIsSendAll,
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Process RBF replacement transaction
|
|
146
|
+
function processReplacementTransaction({ replaceTx, asset }) {
|
|
147
|
+
if (!replaceTx) return { replaceTx: undefined, replaceTxInputUtxos: undefined }
|
|
148
|
+
|
|
149
|
+
const clonedTx = replaceTx.clone()
|
|
150
|
+
const updatedTx = clonedTx.update({ data: { ...clonedTx.data } })
|
|
151
|
+
|
|
152
|
+
updatedTx.data.sent = updatedTx.data.sent.map((to) => ({
|
|
153
|
+
...to,
|
|
154
|
+
amount: serializeCurrency(to.amount, asset.currency),
|
|
155
|
+
}))
|
|
156
|
+
|
|
157
|
+
const replaceTxInputUtxos = UtxoCollection.fromJSON(updatedTx.data.inputs, {
|
|
158
|
+
currency: asset.currency,
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
return { replaceTx: updatedTx, replaceTxInputUtxos }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Create transaction outputs
|
|
165
|
+
function createTransactionOutputs({
|
|
166
|
+
replaceTx,
|
|
167
|
+
processedAddress,
|
|
168
|
+
sendAmount,
|
|
169
|
+
asset,
|
|
170
|
+
selectedUtxos,
|
|
171
|
+
fee,
|
|
172
|
+
changeAddress,
|
|
173
|
+
}) {
|
|
174
|
+
const assetName = asset.name
|
|
175
|
+
let outputs = []
|
|
176
|
+
let sendOutput
|
|
177
|
+
|
|
178
|
+
// Add existing outputs from replacement transaction
|
|
179
|
+
if (replaceTx) {
|
|
180
|
+
outputs = replaceTx.data.sent.map(({ address, amount }) =>
|
|
181
|
+
createOutput(assetName, address, parseCurrency(amount, asset.currency))
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Add send output if we have a destination address
|
|
186
|
+
if (processedAddress) {
|
|
187
|
+
sendOutput = createOutput(assetName, processedAddress, sendAmount)
|
|
188
|
+
outputs.push(sendOutput)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Calculate total amount
|
|
192
|
+
const totalAmount = replaceTx
|
|
193
|
+
? replaceTx.data.sent.reduce(
|
|
194
|
+
(total, { amount }) => total.add(parseCurrency(amount, asset.currency)),
|
|
195
|
+
sendAmount
|
|
196
|
+
)
|
|
197
|
+
: sendAmount
|
|
198
|
+
|
|
199
|
+
// Handle change output
|
|
200
|
+
const { changeOutput, adjustedFee, changeAddressKeypath, ourAddress } = createChangeOutput({
|
|
201
|
+
selectedUtxos,
|
|
202
|
+
totalAmount,
|
|
203
|
+
fee,
|
|
204
|
+
replaceTx,
|
|
205
|
+
changeAddress,
|
|
206
|
+
asset,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
if (changeOutput) {
|
|
210
|
+
outputs.push(changeOutput)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
outputs: replaceTx ? outputs : shuffle(outputs),
|
|
215
|
+
sendOutput,
|
|
216
|
+
changeOutput,
|
|
217
|
+
totalAmount,
|
|
218
|
+
adjustedFee,
|
|
219
|
+
changeAddressKeypath,
|
|
220
|
+
ourAddress,
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Create change output if needed
|
|
225
|
+
function createChangeOutput({ selectedUtxos, totalAmount, fee, replaceTx, changeAddress, asset }) {
|
|
226
|
+
const change = selectedUtxos.value.sub(totalAmount).sub(fee)
|
|
227
|
+
const dust = getChangeDustValue(asset)
|
|
228
|
+
|
|
229
|
+
// Process change address
|
|
230
|
+
let ourAddress = replaceTx?.data?.changeAddress || changeAddress
|
|
231
|
+
if (asset.address.toLegacyAddress) {
|
|
232
|
+
const legacyAddress = asset.address.toLegacyAddress(ourAddress.address)
|
|
233
|
+
ourAddress = Address.create(legacyAddress, ourAddress.meta)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Create change output if above dust threshold
|
|
237
|
+
if (change.gte(dust)) {
|
|
238
|
+
const changeOutput = createOutput(
|
|
239
|
+
asset.name,
|
|
240
|
+
ourAddress.address ?? ourAddress.toString(),
|
|
241
|
+
change
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
// Return the keypath for hardware wallet change detection
|
|
245
|
+
return {
|
|
246
|
+
changeOutput,
|
|
247
|
+
adjustedFee: fee,
|
|
248
|
+
changeAddressKeypath: ourAddress.meta.path,
|
|
249
|
+
ourAddress,
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Add dust to fee if not enough for change output
|
|
254
|
+
return {
|
|
255
|
+
changeOutput: undefined,
|
|
256
|
+
adjustedFee: fee.add(change),
|
|
257
|
+
changeAddressKeypath: undefined,
|
|
258
|
+
ourAddress,
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
13
262
|
async function createUnsignedTx({
|
|
14
263
|
inputs,
|
|
15
264
|
outputs,
|
|
@@ -51,8 +300,6 @@ const transferHandler = {
|
|
|
51
300
|
walletAccount,
|
|
52
301
|
toAddress,
|
|
53
302
|
amount,
|
|
54
|
-
blockHeight: providedBlockHeight,
|
|
55
|
-
rbfEnabled: providedRbfEnabled,
|
|
56
303
|
multipleAddressesEnabled,
|
|
57
304
|
feePerKB,
|
|
58
305
|
customFee,
|
|
@@ -70,99 +317,72 @@ const transferHandler = {
|
|
|
70
317
|
changeAddressType,
|
|
71
318
|
}) => {
|
|
72
319
|
const assetName = asset.name
|
|
73
|
-
const updatedFeeData = { ...feeData, feePerKB: feePerKB ?? feeData.feePerKB }
|
|
74
320
|
const insightClient = asset.baseAsset.insightClient
|
|
75
321
|
|
|
76
|
-
|
|
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
|
-
}
|
|
322
|
+
validateTransactionParams({ assetClientInterface, asset, toAddress, bumpTxId })
|
|
85
323
|
|
|
86
|
-
|
|
324
|
+
const context = await prepareTransactionContext({
|
|
325
|
+
asset,
|
|
326
|
+
assetName,
|
|
327
|
+
walletAccount,
|
|
328
|
+
multipleAddressesEnabled,
|
|
87
329
|
assetClientInterface,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
330
|
+
accountState,
|
|
331
|
+
insightClient,
|
|
332
|
+
feeData,
|
|
333
|
+
feePerKB,
|
|
334
|
+
isExchange,
|
|
335
|
+
isRbfAllowed,
|
|
336
|
+
})
|
|
94
337
|
|
|
95
|
-
const useCashAddress = asset
|
|
338
|
+
const { processedAddress, useCashAddress } = processAddress({ asset, toAddress })
|
|
96
339
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
: await assetClientInterface.getReceiveAddressObject({ assetName, walletAccount })
|
|
340
|
+
// Get replaceable transactions
|
|
341
|
+
let replaceableTxs = findUnconfirmedSentRbfTxs(context.txSet)
|
|
100
342
|
|
|
101
|
-
|
|
343
|
+
// Determine bumping strategy (RBF or CPFP)
|
|
344
|
+
const { replaceableTxs: updatedReplaceableTxs, utxosToBump } = determineBumpStrategy({
|
|
345
|
+
bumpTxId,
|
|
346
|
+
replaceableTxs,
|
|
347
|
+
usableUtxos: context.usableUtxos,
|
|
348
|
+
})
|
|
349
|
+
replaceableTxs = updatedReplaceableTxs
|
|
102
350
|
|
|
103
|
-
const
|
|
104
|
-
|
|
351
|
+
const utxoParams = prepareUtxoSelectionParams({
|
|
352
|
+
bumpTxId,
|
|
105
353
|
asset,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
354
|
+
amount,
|
|
355
|
+
replaceableTxs,
|
|
356
|
+
processedAddress,
|
|
357
|
+
changeAddressType,
|
|
358
|
+
updatedFeeData: context.updatedFeeData,
|
|
359
|
+
rbfEnabled: context.rbfEnabled,
|
|
360
|
+
feePerKB,
|
|
361
|
+
customFee,
|
|
362
|
+
isSendAll,
|
|
110
363
|
})
|
|
111
364
|
|
|
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
365
|
let { selectedUtxos, fee, replaceTx } = selectUtxos({
|
|
148
366
|
asset,
|
|
149
|
-
usableUtxos,
|
|
367
|
+
usableUtxos: context.usableUtxos,
|
|
150
368
|
replaceableTxs,
|
|
151
|
-
amount: sendAmount,
|
|
152
|
-
feeRate:
|
|
153
|
-
receiveAddress,
|
|
154
|
-
isSendAll: resolvedIsSendAll,
|
|
369
|
+
amount: utxoParams.sendAmount,
|
|
370
|
+
feeRate: utxoParams.feeRate,
|
|
371
|
+
receiveAddress: utxoParams.receiveAddress,
|
|
372
|
+
isSendAll: utxoParams.resolvedIsSendAll,
|
|
155
373
|
getFeeEstimator: (asset, { feePerKB, ...options }) =>
|
|
156
374
|
getFeeEstimator(asset, feePerKB, options),
|
|
157
375
|
mustSpendUtxos: utxosToBump,
|
|
158
376
|
allowUnconfirmedRbfEnabledUtxos,
|
|
159
|
-
unconfirmedTxAncestor,
|
|
377
|
+
unconfirmedTxAncestor: context.unconfirmedTxAncestor,
|
|
160
378
|
utxosDescendingOrder,
|
|
161
379
|
taprootInputWitnessSize,
|
|
162
380
|
changeAddressType,
|
|
163
381
|
})
|
|
164
382
|
|
|
165
|
-
if (!selectedUtxos && !replaceTx)
|
|
383
|
+
if (!selectedUtxos && !replaceTx) {
|
|
384
|
+
throw new Error('Not enough funds.')
|
|
385
|
+
}
|
|
166
386
|
|
|
167
387
|
// When bumping a tx, we can either replace the tx with RBF or spend its selected change.
|
|
168
388
|
// If there is no selected UTXO or the tx to replace is not the tx we want to bump,
|
|
@@ -173,70 +393,52 @@ const transferHandler = {
|
|
|
173
393
|
throw new Error(`Unable to bump ${bumpTxId}`)
|
|
174
394
|
}
|
|
175
395
|
|
|
176
|
-
|
|
177
|
-
replaceTx
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
selectedUtxos = selectedUtxos.union(
|
|
183
|
-
UtxoCollection.fromJSON(replaceTx.data.inputs, { currency: asset.currency })
|
|
184
|
-
)
|
|
396
|
+
const { replaceTx: processedReplaceTx, replaceTxInputUtxos } = processReplacementTransaction({
|
|
397
|
+
replaceTx,
|
|
398
|
+
asset,
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
if (replaceTxInputUtxos) {
|
|
402
|
+
selectedUtxos = selectedUtxos.union(replaceTxInputUtxos)
|
|
185
403
|
}
|
|
186
404
|
|
|
187
405
|
const addressPathsMap = selectedUtxos.getAddressPathsMap()
|
|
188
406
|
|
|
189
|
-
//
|
|
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
|
-
}
|
|
407
|
+
// Create inputs
|
|
408
|
+
const inputs = shuffle(createInputs(assetName, selectedUtxos.toArray(), context.rbfEnabled))
|
|
203
409
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
410
|
+
// Create outputs
|
|
411
|
+
const {
|
|
412
|
+
outputs,
|
|
413
|
+
sendOutput,
|
|
414
|
+
changeOutput,
|
|
415
|
+
totalAmount,
|
|
416
|
+
adjustedFee,
|
|
417
|
+
changeAddressKeypath,
|
|
418
|
+
ourAddress,
|
|
419
|
+
} = createTransactionOutputs({
|
|
420
|
+
replaceTx: processedReplaceTx,
|
|
421
|
+
processedAddress,
|
|
422
|
+
sendAmount: utxoParams.sendAmount,
|
|
423
|
+
asset,
|
|
424
|
+
selectedUtxos,
|
|
425
|
+
fee,
|
|
426
|
+
changeAddress: context.changeAddress,
|
|
427
|
+
})
|
|
218
428
|
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
if (
|
|
222
|
-
|
|
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)
|
|
429
|
+
// Add the keypath of change address to support Trezor detect the change output.
|
|
430
|
+
// Output is change and does not need approval from user which shows the strange address that user never seen.
|
|
431
|
+
if (changeAddressKeypath) {
|
|
432
|
+
addressPathsMap[context.changeAddress] = changeAddressKeypath
|
|
230
433
|
}
|
|
231
434
|
|
|
232
|
-
|
|
233
|
-
|
|
435
|
+
// Create unsigned transaction
|
|
234
436
|
const unsignedTx = await createUnsignedTx({
|
|
235
437
|
inputs,
|
|
236
438
|
outputs,
|
|
237
439
|
useCashAddress,
|
|
238
440
|
addressPathsMap,
|
|
239
|
-
blockHeight,
|
|
441
|
+
blockHeight: context.blockHeight,
|
|
240
442
|
asset,
|
|
241
443
|
selectedUtxos,
|
|
242
444
|
insightClient,
|
|
@@ -244,20 +446,22 @@ const transferHandler = {
|
|
|
244
446
|
|
|
245
447
|
return {
|
|
246
448
|
unsignedTx,
|
|
247
|
-
fee,
|
|
449
|
+
fee: adjustedFee,
|
|
248
450
|
metadata: {
|
|
249
451
|
amount,
|
|
250
|
-
change,
|
|
452
|
+
change: selectedUtxos.value.sub(totalAmount).sub(adjustedFee),
|
|
251
453
|
totalAmount,
|
|
252
454
|
address: processedAddress,
|
|
253
455
|
ourAddress,
|
|
254
|
-
receiveAddress,
|
|
255
|
-
sendAmount,
|
|
256
|
-
usableUtxos,
|
|
456
|
+
receiveAddress: utxoParams.receiveAddress,
|
|
457
|
+
sendAmount: utxoParams.sendAmount,
|
|
458
|
+
usableUtxos: context.usableUtxos,
|
|
257
459
|
selectedUtxos,
|
|
258
|
-
replaceTx,
|
|
460
|
+
replaceTx: processedReplaceTx,
|
|
259
461
|
sendOutput,
|
|
260
462
|
changeOutput,
|
|
463
|
+
blockHeight: context.blockHeight,
|
|
464
|
+
rbfEnabled: context.rbfEnabled,
|
|
261
465
|
},
|
|
262
466
|
}
|
|
263
467
|
},
|
|
@@ -277,8 +481,6 @@ export const createTxFactory =
|
|
|
277
481
|
type,
|
|
278
482
|
toAddress,
|
|
279
483
|
amount,
|
|
280
|
-
blockHeight,
|
|
281
|
-
rbfEnabled,
|
|
282
484
|
multipleAddressesEnabled,
|
|
283
485
|
feePerKB,
|
|
284
486
|
customFee,
|
|
@@ -302,8 +504,6 @@ export const createTxFactory =
|
|
|
302
504
|
walletAccount,
|
|
303
505
|
toAddress,
|
|
304
506
|
amount,
|
|
305
|
-
blockHeight,
|
|
306
|
-
rbfEnabled,
|
|
307
507
|
multipleAddressesEnabled,
|
|
308
508
|
feePerKB,
|
|
309
509
|
customFee,
|
package/src/tx-send/index.js
CHANGED
|
@@ -2,7 +2,6 @@ import * as defaultBitcoinjsLib from '@exodus/bitcoinjs'
|
|
|
2
2
|
import assert from 'minimalistic-assert'
|
|
3
3
|
|
|
4
4
|
import { createTxFactory } from '../tx-create/create-tx.js'
|
|
5
|
-
import { getBlockHeight } from '../tx-create/tx-create-utils.js'
|
|
6
5
|
import { broadcastTransaction } from './broadcast-tx.js'
|
|
7
6
|
import { updateAccountState, updateTransactionLog } from './update-state.js'
|
|
8
7
|
|
|
@@ -104,11 +103,9 @@ const getPrepareSendTransaction = async ({
|
|
|
104
103
|
amount,
|
|
105
104
|
asset,
|
|
106
105
|
assetClientInterface,
|
|
107
|
-
blockHeight,
|
|
108
106
|
changeAddressType,
|
|
109
107
|
getFeeEstimator,
|
|
110
108
|
options,
|
|
111
|
-
rbfEnabled,
|
|
112
109
|
utxosDescendingOrder,
|
|
113
110
|
walletAccount,
|
|
114
111
|
}) => {
|
|
@@ -120,15 +117,17 @@ const getPrepareSendTransaction = async ({
|
|
|
120
117
|
changeAddressType,
|
|
121
118
|
})
|
|
122
119
|
|
|
120
|
+
// Set default values for options
|
|
121
|
+
const { isRbfAllowed = true, ...restOptions } = options || Object.create(null)
|
|
122
|
+
|
|
123
123
|
return createTx({
|
|
124
124
|
asset,
|
|
125
125
|
walletAccount,
|
|
126
126
|
type: 'transfer',
|
|
127
127
|
toAddress: address,
|
|
128
128
|
amount,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
...options,
|
|
129
|
+
isRbfAllowed,
|
|
130
|
+
...restOptions,
|
|
132
131
|
})
|
|
133
132
|
}
|
|
134
133
|
|
|
@@ -145,17 +144,10 @@ export const createAndBroadcastTXFactory =
|
|
|
145
144
|
}) =>
|
|
146
145
|
async ({ asset, walletAccount, address, amount, options }) => {
|
|
147
146
|
// Prepare transaction
|
|
148
|
-
const { bumpTxId
|
|
147
|
+
const { bumpTxId } = options
|
|
149
148
|
|
|
150
149
|
const assetName = asset.name
|
|
151
|
-
const feeData = await assetClientInterface.getFeeConfig({ assetName })
|
|
152
150
|
const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
|
|
153
|
-
const insightClient = asset.baseAsset.insightClient
|
|
154
|
-
|
|
155
|
-
const rbfEnabled = feeData.rbfEnabled && !isExchange && isRbfAllowed
|
|
156
|
-
|
|
157
|
-
// blockHeight
|
|
158
|
-
const blockHeight = await getBlockHeight({ assetName, insightClient })
|
|
159
151
|
|
|
160
152
|
const transactionDescriptor = await getPrepareSendTransaction({
|
|
161
153
|
address,
|
|
@@ -163,17 +155,23 @@ export const createAndBroadcastTXFactory =
|
|
|
163
155
|
amount,
|
|
164
156
|
asset,
|
|
165
157
|
assetClientInterface,
|
|
166
|
-
blockHeight,
|
|
167
158
|
changeAddressType,
|
|
168
159
|
getFeeEstimator,
|
|
169
160
|
options,
|
|
170
|
-
rbfEnabled,
|
|
171
161
|
utxosDescendingOrder,
|
|
172
162
|
walletAccount,
|
|
173
163
|
})
|
|
174
164
|
|
|
175
165
|
const { unsignedTx, fee, metadata } = transactionDescriptor
|
|
176
|
-
const {
|
|
166
|
+
const {
|
|
167
|
+
sendAmount,
|
|
168
|
+
usableUtxos,
|
|
169
|
+
replaceTx,
|
|
170
|
+
sendOutput,
|
|
171
|
+
changeOutput,
|
|
172
|
+
blockHeight,
|
|
173
|
+
rbfEnabled,
|
|
174
|
+
} = metadata
|
|
177
175
|
|
|
178
176
|
const outputs = unsignedTx.txData.outputs
|
|
179
177
|
|