@exodus/bitcoin-api 1.0.2 → 1.0.4
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 +3 -2
- package/src/bitcoinjs-lib/ecc/index.js +3 -9
- package/src/bitcoinjs-lib/ecc/index.native.js +1 -4
- package/src/fee/can-bump-tx.js +10 -6
- package/src/fee/get-fee-resolver.js +9 -4
- package/src/fee/utxo-selector.js +23 -11
- package/src/insight-api-client/index.js +29 -30
- package/src/tx-send/index.js +12 -10
- package/src/tx-sign/default-create-tx.js +20 -3
- package/src/tx-sign/default-entropy.js +4 -0
- package/src/tx-sign/taproot.js +2 -1
- package/src/utxos-utils.js +54 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bitcoin-api",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Exodus bitcoin-api",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"@exodus/models": "^8.10.4",
|
|
26
26
|
"@exodus/secp256k1": "4.0.2-exodus.0",
|
|
27
27
|
"@exodus/simple-retry": "0.0.6",
|
|
28
|
+
"bech32": "^1.1.3",
|
|
28
29
|
"bip44-constants": "55.0.0",
|
|
29
30
|
"coininfo": "5.1.0",
|
|
30
31
|
"delay": "4.0.1",
|
|
@@ -38,5 +39,5 @@
|
|
|
38
39
|
"@exodus/bip-schnorr": "0.6.6-fork-1",
|
|
39
40
|
"@noble/secp256k1": "~1.5.3"
|
|
40
41
|
},
|
|
41
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "00377ab9ad8a29114f5064efcb0197d4257056d1"
|
|
42
43
|
}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
5
|
-
if (useSchnorrEcc) {
|
|
6
|
-
return require('./mobile-schnorr').mobileSchnorrEcc
|
|
7
|
-
}
|
|
8
|
-
return require('./mobile').mobileEcc
|
|
9
|
-
}
|
|
1
|
+
import { desktopEcc } from './desktop'
|
|
2
|
+
|
|
3
|
+
export const eccFactory = () => desktopEcc
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export const eccFactory = (useDesktopEcc, useSchnorrEcc) => {
|
|
4
|
-
assert(!useDesktopEcc, 'useDesktopEcc must be false on mobile!!')
|
|
1
|
+
export const eccFactory = (useSchnorrEcc) => {
|
|
5
2
|
if (useSchnorrEcc) {
|
|
6
3
|
return require('./mobile-schnorr').mobileSchnorrEcc
|
|
7
4
|
}
|
package/src/fee/can-bump-tx.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { selectUtxos } from './utxo-selector'
|
|
2
2
|
import assert from 'minimalistic-assert'
|
|
3
3
|
import { findUnconfirmedSentRbfTxs } from '../tx-utils'
|
|
4
|
-
import {
|
|
4
|
+
import { getUsableUtxos, getUtxos } from '../utxos-utils'
|
|
5
5
|
|
|
6
6
|
import { BumpType } from '@exodus/bitcoin-lib/lib/selectors/get-can-bump-tx-factory'
|
|
7
7
|
|
|
@@ -16,6 +16,7 @@ const _canBumpTx = ({
|
|
|
16
16
|
feeData,
|
|
17
17
|
getFeeEstimator,
|
|
18
18
|
taprootEnabled,
|
|
19
|
+
allowUnconfirmedRbfEnabledUtxos,
|
|
19
20
|
}) => {
|
|
20
21
|
assert(asset, 'asset must be provided')
|
|
21
22
|
assert(tx, 'tx must be provided')
|
|
@@ -52,19 +53,20 @@ const _canBumpTx = ({
|
|
|
52
53
|
|
|
53
54
|
const utxos = getUtxos({ accountState, asset })
|
|
54
55
|
|
|
55
|
-
const
|
|
56
|
+
const usableUtxos = getUsableUtxos({
|
|
56
57
|
asset,
|
|
57
58
|
utxos,
|
|
58
59
|
feeData,
|
|
59
60
|
txSet,
|
|
60
61
|
taprootEnabled,
|
|
62
|
+
allowUnconfirmedRbfEnabledUtxos,
|
|
61
63
|
})
|
|
62
|
-
if (
|
|
64
|
+
if (usableUtxos.value.isZero) return { errorMessage: 'insufficient funds' }
|
|
63
65
|
|
|
64
66
|
const { txId } = tx
|
|
65
67
|
const replaceableTxs = findUnconfirmedSentRbfTxs(txSet)
|
|
66
68
|
const bumpTx = replaceableTxs.find((tx) => tx.txId === txId)
|
|
67
|
-
const changeUtxos =
|
|
69
|
+
const changeUtxos = usableUtxos.getTxIdUtxos(txId)
|
|
68
70
|
|
|
69
71
|
// Can't bump a non-rbf tx with no change
|
|
70
72
|
if (!bumpTx && changeUtxos.size === 0) return { errorMessage: 'no change' }
|
|
@@ -80,22 +82,24 @@ const _canBumpTx = ({
|
|
|
80
82
|
if (bumpTx) {
|
|
81
83
|
const { replaceTx } = selectUtxos({
|
|
82
84
|
asset,
|
|
83
|
-
|
|
85
|
+
usableUtxos,
|
|
84
86
|
replaceableTxs: [bumpTx],
|
|
85
87
|
feeRate,
|
|
86
88
|
receiveAddress: null,
|
|
87
89
|
getFeeEstimator,
|
|
90
|
+
allowUnconfirmedRbfEnabledUtxos,
|
|
88
91
|
})
|
|
89
92
|
if (replaceTx) return { bumpType: BumpType.RBF }
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
const { fee } = selectUtxos({
|
|
93
96
|
asset,
|
|
94
|
-
|
|
97
|
+
usableUtxos,
|
|
95
98
|
feeRate,
|
|
96
99
|
receiveAddress: 'P2WPKH',
|
|
97
100
|
getFeeEstimator,
|
|
98
101
|
mustSpendUtxos: changeUtxos,
|
|
102
|
+
allowUnconfirmedRbfEnabledUtxos,
|
|
99
103
|
})
|
|
100
104
|
|
|
101
105
|
return fee ? { bumpType: BumpType.CPFP } : { errorMessage: 'insufficient funds' }
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import assert from 'minimalistic-assert'
|
|
2
2
|
import { getUtxosData } from './utxo-selector'
|
|
3
3
|
import { findUnconfirmedSentRbfTxs } from '../tx-utils'
|
|
4
|
-
import { getSpendableUtxos, getUtxos } from '../utxos-utils'
|
|
4
|
+
import { getSpendableUtxos, getUsableUtxos, getUtxos } from '../utxos-utils'
|
|
5
5
|
import { canBumpTx } from './can-bump-tx'
|
|
6
6
|
|
|
7
7
|
export class GetFeeResolver {
|
|
8
8
|
#getFeeEstimator
|
|
9
9
|
#taprootEnabled
|
|
10
|
+
#allowUnconfirmedRbfEnabledUtxos
|
|
10
11
|
|
|
11
|
-
constructor({ getFeeEstimator, taprootEnabled }) {
|
|
12
|
+
constructor({ getFeeEstimator, taprootEnabled, allowUnconfirmedRbfEnabledUtxos }) {
|
|
12
13
|
assert(getFeeEstimator, 'getFeeEstimator must be provided')
|
|
13
14
|
this.#getFeeEstimator = (asset, { feePerKB }) => getFeeEstimator(asset, feePerKB)
|
|
14
15
|
this.#taprootEnabled = taprootEnabled
|
|
16
|
+
this.#allowUnconfirmedRbfEnabledUtxos = allowUnconfirmedRbfEnabledUtxos
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
getFee = ({ asset, accountState, txSet, feeData, amount, customFee, isSendAll }) => {
|
|
@@ -47,6 +49,7 @@ export class GetFeeResolver {
|
|
|
47
49
|
feeData,
|
|
48
50
|
txSet,
|
|
49
51
|
taprootEnabled: this.#taprootEnabled,
|
|
52
|
+
allowUnconfirmedRbfEnabledUtxos: this.#allowUnconfirmedRbfEnabledUtxos,
|
|
50
53
|
})
|
|
51
54
|
return spendableUtxos.value
|
|
52
55
|
}
|
|
@@ -59,7 +62,7 @@ export class GetFeeResolver {
|
|
|
59
62
|
|
|
60
63
|
const utxos = getUtxos({ accountState, asset })
|
|
61
64
|
|
|
62
|
-
const
|
|
65
|
+
const usableUtxos = getUsableUtxos({
|
|
63
66
|
asset,
|
|
64
67
|
utxos,
|
|
65
68
|
feeData,
|
|
@@ -75,13 +78,14 @@ export class GetFeeResolver {
|
|
|
75
78
|
const feePerKB = customFee || feeData.feePerKB
|
|
76
79
|
return getUtxosData({
|
|
77
80
|
asset,
|
|
78
|
-
|
|
81
|
+
usableUtxos,
|
|
79
82
|
replaceableTxs,
|
|
80
83
|
amount,
|
|
81
84
|
feeRate: feePerKB,
|
|
82
85
|
receiveAddress,
|
|
83
86
|
isSendAll: isSendAll,
|
|
84
87
|
getFeeEstimator: this.#getFeeEstimator,
|
|
88
|
+
allowUnconfirmedRbfEnabledUtxos: this.#allowUnconfirmedRbfEnabledUtxos,
|
|
85
89
|
})
|
|
86
90
|
}
|
|
87
91
|
|
|
@@ -94,6 +98,7 @@ export class GetFeeResolver {
|
|
|
94
98
|
feeData,
|
|
95
99
|
getFeeEstimator: this.#getFeeEstimator,
|
|
96
100
|
taprootEnabled: this.#taprootEnabled,
|
|
101
|
+
allowUnconfirmedRbfEnabledUtxos: this.#allowUnconfirmedRbfEnabledUtxos,
|
|
97
102
|
})
|
|
98
103
|
}
|
|
99
104
|
}
|
package/src/fee/utxo-selector.js
CHANGED
|
@@ -2,12 +2,13 @@ import { UtxoCollection } from '@exodus/models'
|
|
|
2
2
|
import NumberUnit from '@exodus/currency'
|
|
3
3
|
import assert from 'minimalistic-assert'
|
|
4
4
|
import { getExtraFee } from './fee-utils'
|
|
5
|
+
import { getConfirmedUtxos, getConfirmedOrRfbDisabledUtxos } from '../utxos-utils'
|
|
5
6
|
|
|
6
7
|
const MIN_RELAY_FEE = 1000
|
|
7
8
|
|
|
8
9
|
export const selectUtxos = ({
|
|
9
10
|
asset,
|
|
10
|
-
|
|
11
|
+
usableUtxos,
|
|
11
12
|
replaceableTxs,
|
|
12
13
|
amount,
|
|
13
14
|
feeRate,
|
|
@@ -16,9 +17,10 @@ export const selectUtxos = ({
|
|
|
16
17
|
getFeeEstimator,
|
|
17
18
|
disableReplacement = false,
|
|
18
19
|
mustSpendUtxos,
|
|
20
|
+
allowUnconfirmedRbfEnabledUtxos,
|
|
19
21
|
}) => {
|
|
20
22
|
assert(asset, 'asset is required')
|
|
21
|
-
assert(
|
|
23
|
+
assert(usableUtxos, 'usableUtxos is required')
|
|
22
24
|
assert(getFeeEstimator, 'getFeeEstimator is required')
|
|
23
25
|
|
|
24
26
|
const changeAddressType = ['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(asset.name)
|
|
@@ -30,19 +32,17 @@ export const selectUtxos = ({
|
|
|
30
32
|
if (!amount) amount = currency.ZERO
|
|
31
33
|
|
|
32
34
|
// We can only replace for a sendAll if only 1 replaceable tx and no unconfirmed utxos
|
|
33
|
-
const confirmedUtxosArray =
|
|
34
|
-
({ confirmations }) => confirmations > 0
|
|
35
|
-
)
|
|
35
|
+
const confirmedUtxosArray = getConfirmedUtxos({ asset, utxos: usableUtxos }).toArray()
|
|
36
36
|
const canReplace =
|
|
37
37
|
!mustSpendUtxos &&
|
|
38
38
|
!disableReplacement &&
|
|
39
39
|
replaceableTxs &&
|
|
40
40
|
(!isSendAll ||
|
|
41
|
-
(replaceableTxs.length === 1 && confirmedUtxosArray.length ===
|
|
41
|
+
(replaceableTxs.length === 1 && confirmedUtxosArray.length === usableUtxos.size - 1))
|
|
42
42
|
|
|
43
43
|
if (canReplace) {
|
|
44
44
|
for (let tx of replaceableTxs) {
|
|
45
|
-
const changeUtxos =
|
|
45
|
+
const changeUtxos = usableUtxos.getTxIdUtxos(tx.txId)
|
|
46
46
|
// Don't replace a tx that has already been spent
|
|
47
47
|
if (tx.data.changeAddress && changeUtxos.size === 0) continue
|
|
48
48
|
let feePerKB
|
|
@@ -117,11 +117,17 @@ export const selectUtxos = ({
|
|
|
117
117
|
if (replaceableTxs) {
|
|
118
118
|
for (let tx of replaceableTxs) {
|
|
119
119
|
if (!tx.data.changeAddress) continue
|
|
120
|
-
const changeUtxos =
|
|
120
|
+
const changeUtxos = usableUtxos.getTxIdUtxos(tx.txId)
|
|
121
121
|
ourRbfUtxos = ourRbfUtxos.union(changeUtxos)
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
const spendableUtxos = getConfirmedOrRfbDisabledUtxos({
|
|
126
|
+
asset,
|
|
127
|
+
utxos: usableUtxos,
|
|
128
|
+
allowUnconfirmedRbfEnabledUtxos,
|
|
129
|
+
})
|
|
130
|
+
|
|
125
131
|
const utxosArray = spendableUtxos.union(ourRbfUtxos).toPriorityOrderedArray()
|
|
126
132
|
|
|
127
133
|
if (isSendAll) {
|
|
@@ -177,7 +183,7 @@ export const selectUtxos = ({
|
|
|
177
183
|
|
|
178
184
|
export const getUtxosData = ({
|
|
179
185
|
asset,
|
|
180
|
-
|
|
186
|
+
usableUtxos,
|
|
181
187
|
replaceableTxs,
|
|
182
188
|
amount,
|
|
183
189
|
feeRate,
|
|
@@ -186,10 +192,11 @@ export const getUtxosData = ({
|
|
|
186
192
|
getFeeEstimator,
|
|
187
193
|
disableReplacement,
|
|
188
194
|
mustSpendUtxos,
|
|
195
|
+
allowUnconfirmedRbfEnabledUtxos,
|
|
189
196
|
}) => {
|
|
190
197
|
const { selectedUtxos, replaceTx, fee } = selectUtxos({
|
|
191
198
|
asset,
|
|
192
|
-
|
|
199
|
+
usableUtxos,
|
|
193
200
|
replaceableTxs,
|
|
194
201
|
amount,
|
|
195
202
|
feeRate,
|
|
@@ -198,11 +205,16 @@ export const getUtxosData = ({
|
|
|
198
205
|
getFeeEstimator,
|
|
199
206
|
disableReplacement,
|
|
200
207
|
mustSpendUtxos,
|
|
208
|
+
allowUnconfirmedRbfEnabledUtxos,
|
|
201
209
|
})
|
|
202
210
|
|
|
203
211
|
const resolvedFee = replaceTx ? fee.sub(replaceTx.feeAmount) : fee
|
|
204
212
|
|
|
205
|
-
const spendableBalance =
|
|
213
|
+
const spendableBalance = getConfirmedOrRfbDisabledUtxos({
|
|
214
|
+
asset,
|
|
215
|
+
utxos: usableUtxos,
|
|
216
|
+
allowUnconfirmedRbfEnabledUtxos,
|
|
217
|
+
}).value
|
|
206
218
|
|
|
207
219
|
const extraFee = selectedUtxos
|
|
208
220
|
? asset.currency.baseUnit(getExtraFee({ asset, inputs: selectedUtxos, feePerKB: feeRate }))
|
|
@@ -4,6 +4,25 @@ import urlJoin from 'url-join'
|
|
|
4
4
|
import qs from 'querystring'
|
|
5
5
|
import delay from 'delay'
|
|
6
6
|
|
|
7
|
+
const getTextFromResponse = async (response) => {
|
|
8
|
+
try {
|
|
9
|
+
const responseBody = await response.text()
|
|
10
|
+
return responseBody.substring(0, 100)
|
|
11
|
+
} catch (e) {
|
|
12
|
+
return ''
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const fetchJson = async (url, fetchOptions) => {
|
|
17
|
+
const response = await fetch(url, fetchOptions)
|
|
18
|
+
if (!response.ok)
|
|
19
|
+
throw new Error(
|
|
20
|
+
`${url} returned ${response.status}: ${response.statusText ||
|
|
21
|
+
'Unknown Status Text'}. Body: ${await getTextFromResponse(response)}`
|
|
22
|
+
)
|
|
23
|
+
return response.json()
|
|
24
|
+
}
|
|
25
|
+
|
|
7
26
|
// TODO: use p-retry
|
|
8
27
|
async function fetchJsonRetry(url, fetchOptions) {
|
|
9
28
|
const waitTimes = [5, 10, 20, 30].map((t) => t * 1000)
|
|
@@ -22,12 +41,6 @@ async function fetchJsonRetry(url, fetchOptions) {
|
|
|
22
41
|
}
|
|
23
42
|
}
|
|
24
43
|
}
|
|
25
|
-
|
|
26
|
-
async function fetchJson(url, fetchOptions) {
|
|
27
|
-
const response = await fetch(url, fetchOptions)
|
|
28
|
-
if (!response.ok) throw new Error(`${url} returned ${response.status}: ${response.statusText}`)
|
|
29
|
-
return response.json()
|
|
30
|
-
}
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
export default class InsightAPIClient {
|
|
@@ -41,9 +54,7 @@ export default class InsightAPIClient {
|
|
|
41
54
|
|
|
42
55
|
async isNetworkConnected() {
|
|
43
56
|
const url = urlJoin(this._baseURL, '/peer')
|
|
44
|
-
const
|
|
45
|
-
const peerStatus = await resp.json()
|
|
46
|
-
|
|
57
|
+
const peerStatus = await fetchJson(url, { timeout: 10000 })
|
|
47
58
|
return !!peerStatus.connected
|
|
48
59
|
}
|
|
49
60
|
|
|
@@ -56,9 +67,7 @@ export default class InsightAPIClient {
|
|
|
56
67
|
|
|
57
68
|
async fetchBlockHeight() {
|
|
58
69
|
const url = urlJoin(this._baseURL, '/status')
|
|
59
|
-
const
|
|
60
|
-
const status = await resp.json()
|
|
61
|
-
|
|
70
|
+
const status = await fetchJson(url, { timeout: 10000 })
|
|
62
71
|
return status.info.blocks
|
|
63
72
|
}
|
|
64
73
|
|
|
@@ -68,9 +77,7 @@ export default class InsightAPIClient {
|
|
|
68
77
|
this._baseURL,
|
|
69
78
|
opts.includeTxs ? `/addr/${encodedAddress}` : `/addr/${encodedAddress}?noTxList=1`
|
|
70
79
|
)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return response.json()
|
|
80
|
+
return fetchJson(url)
|
|
74
81
|
}
|
|
75
82
|
|
|
76
83
|
async fetchUTXOs(addresses, { assetNames = [] } = {}) {
|
|
@@ -81,8 +88,7 @@ export default class InsightAPIClient {
|
|
|
81
88
|
}
|
|
82
89
|
const encodedAddresses = encodeURIComponent(addresses)
|
|
83
90
|
const url = urlJoin(this._baseURL, `/addrs/${encodedAddresses}/utxo?${query}`)
|
|
84
|
-
const
|
|
85
|
-
const utxos = await response.json()
|
|
91
|
+
const utxos = await fetchJson(url)
|
|
86
92
|
|
|
87
93
|
return utxos.map((utxo) => ({
|
|
88
94
|
address: utxo.address,
|
|
@@ -111,8 +117,7 @@ export default class InsightAPIClient {
|
|
|
111
117
|
async fetchRawTx(txId: string) {
|
|
112
118
|
const encodedTxId = encodeURIComponent(txId)
|
|
113
119
|
const url = urlJoin(this._baseURL, `/rawtx/${encodedTxId}`)
|
|
114
|
-
const
|
|
115
|
-
const { rawtx } = await response.json()
|
|
120
|
+
const { rawtx } = await fetchJson(url)
|
|
116
121
|
return rawtx
|
|
117
122
|
}
|
|
118
123
|
|
|
@@ -155,17 +160,15 @@ export default class InsightAPIClient {
|
|
|
155
160
|
async fetchUnconfirmedAncestorData(txId: string) {
|
|
156
161
|
const encodedTxId = encodeURIComponent(txId)
|
|
157
162
|
const url = urlJoin(this._baseURL, `/unconfirmed_ancestor/${encodedTxId}`)
|
|
158
|
-
|
|
159
|
-
return response.json()
|
|
163
|
+
return fetchJson(url)
|
|
160
164
|
}
|
|
161
165
|
|
|
162
166
|
async fetchFeeRate() {
|
|
163
167
|
const url = urlJoin(this._baseURL, '/v2/fees')
|
|
164
|
-
|
|
165
|
-
return response.json()
|
|
168
|
+
return fetchJson(url)
|
|
166
169
|
}
|
|
167
170
|
|
|
168
|
-
|
|
171
|
+
async broadcastTx(rawTx) {
|
|
169
172
|
console.log('gonna broadcastTx')
|
|
170
173
|
const url = urlJoin(this._baseURL, '/tx/send')
|
|
171
174
|
const fetchOptions = {
|
|
@@ -200,16 +203,12 @@ export default class InsightAPIClient {
|
|
|
200
203
|
async getClaimable(address) {
|
|
201
204
|
const encodedAddress = encodeURIComponent(address)
|
|
202
205
|
const url = urlJoin(this._baseURL, `/addr/${encodedAddress}/claimable`)
|
|
203
|
-
|
|
204
|
-
if (!response.ok) throw new Error(`${url} returned ${response.status}: ${response.statusText}`)
|
|
205
|
-
return response.json()
|
|
206
|
+
return fetchJson(url)
|
|
206
207
|
}
|
|
207
208
|
|
|
208
209
|
async getUnclaimed(address) {
|
|
209
210
|
const encodedAddress = encodeURIComponent(address)
|
|
210
211
|
const url = urlJoin(this._baseURL, `/addr/${encodedAddress}/unclaimed`)
|
|
211
|
-
|
|
212
|
-
if (!response.ok) throw new Error(`${url} returned ${response.status}: ${response.statusText}`)
|
|
213
|
-
return response.json()
|
|
212
|
+
return fetchJson(url)
|
|
214
213
|
}
|
|
215
214
|
}
|
package/src/tx-send/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
createOutput as dogecoinCreateOutput,
|
|
15
15
|
} from './dogecoin'
|
|
16
16
|
import { findUnconfirmedSentRbfTxs } from '../tx-utils'
|
|
17
|
-
import {
|
|
17
|
+
import { getUsableUtxos, getUtxos } from '../utxos-utils'
|
|
18
18
|
|
|
19
19
|
const ASSETS_SUPPORTED_BIP_174 = [
|
|
20
20
|
'bitcoin',
|
|
@@ -82,10 +82,11 @@ export async function getNonWitnessTxs(asset, utxos, insightClient) {
|
|
|
82
82
|
// not ported from Exodus; but this demos signing / broadcasting
|
|
83
83
|
// NOTE: this will be ripped out in the coming weeks
|
|
84
84
|
|
|
85
|
-
export const createAndBroadcastTXFactory = ({
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
export const createAndBroadcastTXFactory = ({
|
|
86
|
+
getFeeEstimator,
|
|
87
|
+
taprootEnabled,
|
|
88
|
+
allowUnconfirmedRbfEnabledUtxos,
|
|
89
|
+
}) => async ({ asset, walletAccount, address, amount, options }, { assetClientInterface }) => {
|
|
89
90
|
const {
|
|
90
91
|
multipleAddressesEnabled,
|
|
91
92
|
feePerKB,
|
|
@@ -120,7 +121,7 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
120
121
|
const insightClient = asset.baseAsset.insightClient
|
|
121
122
|
const currency = asset.currency
|
|
122
123
|
const feeData = await assetClientInterface.getFeeConfig({ assetName })
|
|
123
|
-
const
|
|
124
|
+
const usableUtxos = getUsableUtxos({
|
|
124
125
|
asset,
|
|
125
126
|
utxos: getUtxos({ accountState, asset }),
|
|
126
127
|
feeData,
|
|
@@ -145,7 +146,7 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
145
146
|
if (bumpTxId) {
|
|
146
147
|
const bumpTx = replaceableTxs.find(({ txId }) => txId === bumpTxId)
|
|
147
148
|
if (!bumpTx) {
|
|
148
|
-
utxosToBump =
|
|
149
|
+
utxosToBump = usableUtxos.getTxIdUtxos(bumpTxId)
|
|
149
150
|
if (utxosToBump.size === 0) {
|
|
150
151
|
throw new Error(`Cannot bump transaction ${bumpTxId}`)
|
|
151
152
|
}
|
|
@@ -161,7 +162,7 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
161
162
|
|
|
162
163
|
let { selectedUtxos, fee, replaceTx } = selectUtxos({
|
|
163
164
|
asset,
|
|
164
|
-
|
|
165
|
+
usableUtxos,
|
|
165
166
|
replaceableTxs,
|
|
166
167
|
amount: sendAmount,
|
|
167
168
|
feeRate: customFee || feeRate,
|
|
@@ -169,6 +170,7 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
169
170
|
isSendAll: !rbfEnabled && feePerKB ? false : isSendAll,
|
|
170
171
|
getFeeEstimator: (asset, { feePerKB }) => getFeeEstimator(asset, feePerKB),
|
|
171
172
|
mustSpendUtxos: utxosToBump,
|
|
173
|
+
allowUnconfirmedRbfEnabledUtxos,
|
|
172
174
|
})
|
|
173
175
|
|
|
174
176
|
if (!selectedUtxos && !replaceTx) throw new Error('Not enough funds.')
|
|
@@ -287,7 +289,7 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
287
289
|
err.txInfo = JSON.stringify({
|
|
288
290
|
amount: sendAmount.toDefaultString({ unit: true }),
|
|
289
291
|
fee: ((fee && fee.toDefaultString({ unit: true })) || 0).toString(), // todo why does 0 not have a unit? Is default unit ok here?
|
|
290
|
-
allUtxos:
|
|
292
|
+
allUtxos: usableUtxos.toJSON(),
|
|
291
293
|
})
|
|
292
294
|
throw err
|
|
293
295
|
} else {
|
|
@@ -306,7 +308,7 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
306
308
|
}
|
|
307
309
|
}
|
|
308
310
|
|
|
309
|
-
let remainingUtxos =
|
|
311
|
+
let remainingUtxos = usableUtxos.difference(selectedUtxos)
|
|
310
312
|
if (changeUtxoIndex !== -1) {
|
|
311
313
|
const address = Address.create(ourAddress.address, ourAddress.meta)
|
|
312
314
|
const changeUtxo = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from 'minimalistic-assert'
|
|
2
2
|
import lodash from 'lodash'
|
|
3
3
|
import ECPairFactory from 'ecpair'
|
|
4
|
-
import { Psbt } from '@exodus/bitcoinjs-lib'
|
|
4
|
+
import { Psbt, Transaction } from '@exodus/bitcoinjs-lib'
|
|
5
5
|
|
|
6
6
|
import { toAsyncSigner, tweakSigner } from './taproot'
|
|
7
7
|
|
|
@@ -12,6 +12,15 @@ const _MAXIMUM_FEE_RATES = {
|
|
|
12
12
|
ravencoin: 1000000,
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
const canParseTx = (rawTxBuffer) => {
|
|
16
|
+
try {
|
|
17
|
+
Transaction.fromBuffer(rawTxBuffer)
|
|
18
|
+
return true
|
|
19
|
+
} catch (e) {
|
|
20
|
+
return false
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, network, ecc }) => {
|
|
16
25
|
assert(assetName, 'assetName is required')
|
|
17
26
|
assert(resolvePurpose, 'resolvePurpose is required')
|
|
@@ -62,10 +71,18 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, netwo
|
|
|
62
71
|
// witness outputs only require the value and the script, not the full transaction
|
|
63
72
|
txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
|
|
64
73
|
} else {
|
|
65
|
-
// non-witness outptus require the full transaction
|
|
66
74
|
const rawTx = (rawTxs || []).find((t) => t.txId === txId)
|
|
75
|
+
// non-witness outptus require the full transaction
|
|
67
76
|
assert(!!rawTx, 'Non-witness outputs require the full previous transaction.')
|
|
68
|
-
|
|
77
|
+
const rawTxBuffer = Buffer.from(rawTx.rawData, 'hex')
|
|
78
|
+
if (canParseTx(rawTxBuffer)) {
|
|
79
|
+
txIn.nonWitnessUtxo = rawTxBuffer
|
|
80
|
+
} else {
|
|
81
|
+
// temp fix for https://exodusio.slack.com/archives/CP202D90Q/p1671014704829939 until bitcoinjs could parse a mweb tx without failing
|
|
82
|
+
console.warn(`Setting psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true for asset ${assetName}`)
|
|
83
|
+
psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true
|
|
84
|
+
txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
|
|
85
|
+
}
|
|
69
86
|
}
|
|
70
87
|
psbt.addInput(txIn)
|
|
71
88
|
}
|
package/src/tx-sign/taproot.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { crypto } from '@exodus/bitcoinjs-lib'
|
|
2
2
|
import assert from 'minimalistic-assert'
|
|
3
|
+
import { getSchnorrEntropy } from './default-entropy'
|
|
3
4
|
|
|
4
5
|
export function tweakSigner({ signer, ECPair, ecc, tweakHash, network }) {
|
|
5
6
|
assert(signer, 'signer is required')
|
|
@@ -42,7 +43,7 @@ export function toAsyncSigner({ keyPair, ecc }) {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
keyPair.signSchnorr = async (h) => {
|
|
45
|
-
const sig = await ecc.signSchnorrAsync(h, keyPair.privateKey)
|
|
46
|
+
const sig = await ecc.signSchnorrAsync(h, keyPair.privateKey, getSchnorrEntropy())
|
|
46
47
|
return Buffer.from(sig)
|
|
47
48
|
}
|
|
48
49
|
return keyPair
|
package/src/utxos-utils.js
CHANGED
|
@@ -12,7 +12,11 @@ export function getUtxos({ accountState, asset }) {
|
|
|
12
12
|
)
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export const getBalancesFactory = ({
|
|
15
|
+
export const getBalancesFactory = ({
|
|
16
|
+
taprootEnabled,
|
|
17
|
+
feeData,
|
|
18
|
+
allowUnconfirmedRbfEnabledUtxos,
|
|
19
|
+
}) => {
|
|
16
20
|
assert(feeData, 'feeData is required')
|
|
17
21
|
return ({ asset, accountState, txLog }) => {
|
|
18
22
|
assert(asset, 'asset is required')
|
|
@@ -26,6 +30,7 @@ export const getBalancesFactory = ({ taprootEnabled, feeData }) => {
|
|
|
26
30
|
txSet: txLog,
|
|
27
31
|
feeData,
|
|
28
32
|
taprootEnabled,
|
|
33
|
+
allowUnconfirmedRbfEnabledUtxos,
|
|
29
34
|
}).value
|
|
30
35
|
return { balance, spendableBalance }
|
|
31
36
|
}
|
|
@@ -33,16 +38,45 @@ export const getBalancesFactory = ({ taprootEnabled, feeData }) => {
|
|
|
33
38
|
|
|
34
39
|
const isTaprootUtxo = ({ utxo }) => String(utxo.address).length === 62
|
|
35
40
|
|
|
36
|
-
export function
|
|
37
|
-
|
|
41
|
+
export function getConfirmedUtxos({ asset, utxos }) {
|
|
42
|
+
assert(asset, 'asset is required')
|
|
43
|
+
assert(utxos, 'utxos is required')
|
|
44
|
+
const currency = asset.currency
|
|
45
|
+
return UtxoCollection.fromArray(
|
|
46
|
+
utxos.toArray().filter(({ confirmations }) => confirmations > 0),
|
|
47
|
+
{ currency }
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getConfirmedOrRfbDisabledUtxos({ asset, utxos, allowUnconfirmedRbfEnabledUtxos }) {
|
|
52
|
+
assert(asset, 'asset is required')
|
|
53
|
+
assert(utxos, 'utxos is required')
|
|
54
|
+
assert(
|
|
55
|
+
allowUnconfirmedRbfEnabledUtxos !== undefined,
|
|
56
|
+
'allowUnconfirmedRbfEnabledUtxos is required'
|
|
57
|
+
)
|
|
58
|
+
if (allowUnconfirmedRbfEnabledUtxos) {
|
|
59
|
+
return utxos
|
|
60
|
+
}
|
|
61
|
+
const currency = asset.currency
|
|
62
|
+
return UtxoCollection.fromArray(
|
|
63
|
+
utxos.toArray().filter((utxo) => utxo.confirmations > 0 || !utxo.rbfEnabled),
|
|
64
|
+
{ currency }
|
|
65
|
+
)
|
|
66
|
+
}
|
|
38
67
|
|
|
68
|
+
export function getUsableUtxos({ asset, utxos, feeData, txSet, taprootEnabled }) {
|
|
69
|
+
assert(asset, 'asset is required')
|
|
70
|
+
assert(utxos, 'utxos is required')
|
|
71
|
+
assert(feeData, 'feeData is required')
|
|
72
|
+
assert(txSet, 'txSet is required')
|
|
73
|
+
if (!['bitcoin', 'bitcointestnet', 'bitcoinregtest'].includes(asset.name)) return utxos
|
|
39
74
|
if (!taprootEnabled) {
|
|
40
75
|
utxos = UtxoCollection.fromArray(
|
|
41
76
|
utxos.toArray().filter((utxo) => !isTaprootUtxo({ utxo })),
|
|
42
77
|
{ currency: asset.currency }
|
|
43
78
|
)
|
|
44
79
|
}
|
|
45
|
-
|
|
46
80
|
const { fastestFee } = feeData
|
|
47
81
|
const feeRate = fastestFee.toBaseNumber()
|
|
48
82
|
const maxFee = feeData.maxExtraCpfpFee
|
|
@@ -61,3 +95,19 @@ export function getSpendableUtxos({ asset, utxos, feeData, txSet, taprootEnabled
|
|
|
61
95
|
{ currency: asset.currency }
|
|
62
96
|
)
|
|
63
97
|
}
|
|
98
|
+
|
|
99
|
+
export function getSpendableUtxos({
|
|
100
|
+
asset,
|
|
101
|
+
utxos,
|
|
102
|
+
feeData,
|
|
103
|
+
txSet,
|
|
104
|
+
taprootEnabled,
|
|
105
|
+
allowUnconfirmedRbfEnabledUtxos,
|
|
106
|
+
}) {
|
|
107
|
+
const usableUtxos = getUsableUtxos({ asset, utxos, feeData, txSet, taprootEnabled })
|
|
108
|
+
return getConfirmedOrRfbDisabledUtxos({
|
|
109
|
+
asset,
|
|
110
|
+
utxos: usableUtxos,
|
|
111
|
+
allowUnconfirmedRbfEnabledUtxos,
|
|
112
|
+
})
|
|
113
|
+
}
|