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