@exodus/bitcoin-api 1.0.2 → 1.0.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/bitcoin-api",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Exodus bitcoin-api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -38,5 +38,5 @@
38
38
  "@exodus/bip-schnorr": "0.6.6-fork-1",
39
39
  "@noble/secp256k1": "~1.5.3"
40
40
  },
41
- "gitHead": "6bbd1f431656185196b2dbe74a9347a29e6bb3b1"
41
+ "gitHead": "6da3cc2b9aaba76c1e3523d9598e465c4b4f45eb"
42
42
  }
@@ -1,9 +1,3 @@
1
- export const eccFactory = (useDesktopEcc, useSchnorrEcc) => {
2
- if (useDesktopEcc) {
3
- return require('./desktop').desktopEcc
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
- import assert from 'minimalistic-assert'
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
  }
@@ -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 { getSpendableUtxos, getUtxos } from '../utxos-utils'
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 spendableUtxos = getSpendableUtxos({
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 (!spendableUtxos) return { errorMessage: 'insufficient funds' }
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 = spendableUtxos.getTxIdUtxos(txId)
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
- spendableUtxos,
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
- spendableUtxos,
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 spendableUtxos = getSpendableUtxos({
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
- spendableUtxos,
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
  }
@@ -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
- spendableUtxos,
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(spendableUtxos, 'spendableUtxos is required')
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 = Array.from(spendableUtxos).filter(
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 === spendableUtxos.size - 1))
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 = spendableUtxos.getTxIdUtxos(tx.txId)
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 = spendableUtxos.getTxIdUtxos(tx.txId)
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
- spendableUtxos,
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
- spendableUtxos,
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 = spendableUtxos.value
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 }))
@@ -165,7 +165,7 @@ export default class InsightAPIClient {
165
165
  return response.json()
166
166
  }
167
167
 
168
- broadcastTx = async (rawTx) => {
168
+ async broadcastTx(rawTx) {
169
169
  console.log('gonna broadcastTx')
170
170
  const url = urlJoin(this._baseURL, '/tx/send')
171
171
  const fetchOptions = {
@@ -14,7 +14,7 @@ import {
14
14
  createOutput as dogecoinCreateOutput,
15
15
  } from './dogecoin'
16
16
  import { findUnconfirmedSentRbfTxs } from '../tx-utils'
17
- import { getSpendableUtxos, getUtxos } from '../utxos-utils'
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 = ({ getFeeEstimator, taprootEnabled }) => async (
86
- { asset, walletAccount, address, amount, options },
87
- { assetClientInterface }
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 spendableUtxos = getSpendableUtxos({
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 = spendableUtxos.getTxIdUtxos(bumpTxId)
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
- spendableUtxos,
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: spendableUtxos.toJSON(),
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 = spendableUtxos.difference(selectedUtxos)
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 = {
@@ -0,0 +1,4 @@
1
+ // extension point so tests can assert signatures deterministically
2
+ export function getSchnorrEntropy() {
3
+ return undefined
4
+ }
@@ -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
@@ -12,7 +12,11 @@ export function getUtxos({ accountState, asset }) {
12
12
  )
13
13
  }
14
14
 
15
- export const getBalancesFactory = ({ taprootEnabled, feeData }) => {
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 getSpendableUtxos({ asset, utxos, feeData, txSet, taprootEnabled }) {
37
- if (!['bitcoin', 'bitcointestnet', 'bitcoinregtest'].includes(asset.name)) return utxos
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
+ }