@exodus/bitcoin-api 2.8.1 → 2.9.0

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": "2.8.1",
3
+ "version": "2.9.0",
4
4
  "description": "Exodus bitcoin-api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -42,5 +42,5 @@
42
42
  "@scure/btc-signer": "^1.1.0",
43
43
  "jest-when": "^3.5.1"
44
44
  },
45
- "gitHead": "652eb821c1fe5b2bd561710d05fcb569d9f4a137"
45
+ "gitHead": "ed03529f7a8bb4b39bfd173387f9cdd5d7207925"
46
46
  }
@@ -6,6 +6,9 @@ export function createAccountState({ asset, ordinalsEnabled = false, brc20Enable
6
6
  })
7
7
  const defaults = {
8
8
  utxos: empty,
9
+ mem: {
10
+ unconfirmedTxAncestor: {},
11
+ },
9
12
  }
10
13
 
11
14
  if (ordinalsEnabled) {
@@ -4,6 +4,7 @@ import { findUnconfirmedSentRbfTxs } from '../tx-utils'
4
4
  import { getUsableUtxos, getUtxos } from '../utxos-utils'
5
5
 
6
6
  import { BumpType } from '@exodus/bitcoin-lib'
7
+ import { getUnconfirmedTxAncestorMap } from '../unconfirmed-ancestor-data'
7
8
 
8
9
  export const ASSET_NAMES = ['bitcoin', 'bitcoinregtest', 'bitcointestnet']
9
10
 
@@ -51,13 +52,14 @@ const _canBumpTx = ({
51
52
  }
52
53
 
53
54
  const utxos = getUtxos({ accountState, asset })
54
-
55
+ const unconfirmedTxAncestor = getUnconfirmedTxAncestorMap({ accountState })
55
56
  const usableUtxos = getUsableUtxos({
56
57
  asset,
57
58
  utxos,
58
59
  feeData,
59
60
  txSet,
60
61
  allowUnconfirmedRbfEnabledUtxos,
62
+ unconfirmedTxAncestor,
61
63
  })
62
64
  if (usableUtxos.value.isZero) return { errorMessage: 'insufficient funds' }
63
65
 
@@ -86,6 +88,7 @@ const _canBumpTx = ({
86
88
  receiveAddress: null,
87
89
  getFeeEstimator,
88
90
  allowUnconfirmedRbfEnabledUtxos,
91
+ unconfirmedTxAncestor,
89
92
  })
90
93
  if (replaceTx) return { bumpType: BumpType.RBF, bumpFee: fee.sub(replaceTx.feeAmount) }
91
94
  }
@@ -98,6 +101,7 @@ const _canBumpTx = ({
98
101
  getFeeEstimator,
99
102
  mustSpendUtxos: changeUtxos,
100
103
  allowUnconfirmedRbfEnabledUtxos,
104
+ unconfirmedTxAncestor,
101
105
  })
102
106
 
103
107
  return fee ? { bumpType: BumpType.CPFP } : { errorMessage: 'insufficient funds' }
@@ -5,7 +5,7 @@ import assert from 'minimalistic-assert'
5
5
 
6
6
  export const isHex = (s) => typeof s === 'string' && /[0-9a-f]*/.test(s.toLowerCase())
7
7
 
8
- export function getExtraFee({ asset, inputs, feePerKB }) {
8
+ export function getExtraFee({ asset, inputs, feePerKB, unconfirmedTxAncestor }) {
9
9
  let extraFee = 0
10
10
  // Add extra fee to unconfirmed bitcoin utxos to allow transaction to CPFP ancestors
11
11
  if (
@@ -18,7 +18,12 @@ export function getExtraFee({ asset, inputs, feePerKB }) {
18
18
  )
19
19
  const txIds = new Set(utxos.map(({ txId }) => txId))
20
20
  for (const txId of txIds) {
21
- extraFee += resolveExtraFeeOfTx({ assetName: asset.name, feeRate, txId })
21
+ extraFee += resolveExtraFeeOfTx({
22
+ assetName: asset.name,
23
+ feeRate,
24
+ txId,
25
+ unconfirmedTxAncestor,
26
+ })
22
27
  }
23
28
  extraFee = Math.ceil(extraFee)
24
29
  }
@@ -26,9 +31,13 @@ export function getExtraFee({ asset, inputs, feePerKB }) {
26
31
  }
27
32
 
28
33
  export default function createDefaultFeeEstimator(getSize) {
29
- return (asset, feePerKB, options) => {
30
- return ({ inputs = options.inputs, outputs = options.outputs } = {}) => {
31
- const extraFee = getExtraFee({ asset, inputs, feePerKB })
34
+ return (asset, feePerKB, options = {}) => {
35
+ return ({
36
+ inputs = options.inputs,
37
+ outputs = options.outputs,
38
+ unconfirmedTxAncestor = options.unconfirmedTxAncestor,
39
+ } = {}) => {
40
+ const extraFee = getExtraFee({ asset, inputs, feePerKB, unconfirmedTxAncestor })
32
41
  // Yes, it's suppose to be '1000' and not '1024'
33
42
  // https://bitcoin.stackexchange.com/questions/24000/a-fee-is-added-per-kilobyte-of-data-that-means-1000-bytes-or-1024
34
43
  const size = getSize(asset, inputs, outputs, options)
@@ -3,6 +3,7 @@ import { getUtxosData } from './utxo-selector'
3
3
  import { findUnconfirmedSentRbfTxs } from '../tx-utils'
4
4
  import { getUsableUtxos, getUtxos } from '../utxos-utils'
5
5
  import { canBumpTx } from './can-bump-tx'
6
+ import { getUnconfirmedTxAncestorMap } from '../unconfirmed-ancestor-data'
6
7
 
7
8
  export class GetFeeResolver {
8
9
  #getFeeEstimator
@@ -10,7 +11,8 @@ export class GetFeeResolver {
10
11
 
11
12
  constructor({ getFeeEstimator, allowUnconfirmedRbfEnabledUtxos }) {
12
13
  assert(getFeeEstimator, 'getFeeEstimator must be provided')
13
- this.#getFeeEstimator = (asset, { feePerKB }) => getFeeEstimator(asset, feePerKB)
14
+ this.#getFeeEstimator = (asset, { feePerKB, ...options }) =>
15
+ getFeeEstimator(asset, feePerKB, options)
14
16
  this.#allowUnconfirmedRbfEnabledUtxos = allowUnconfirmedRbfEnabledUtxos
15
17
  }
16
18
 
@@ -92,12 +94,14 @@ export class GetFeeResolver {
92
94
  assert(txSet, 'txSet must be provided')
93
95
 
94
96
  const utxos = getUtxos({ accountState, asset })
97
+ const unconfirmedTxAncestor = getUnconfirmedTxAncestorMap({ accountState })
95
98
 
96
99
  const usableUtxos = getUsableUtxos({
97
100
  asset,
98
101
  utxos,
99
102
  feeData,
100
103
  txSet,
104
+ unconfirmedTxAncestor,
101
105
  })
102
106
  const replaceableTxs = findUnconfirmedSentRbfTxs(txSet)
103
107
 
@@ -113,6 +117,7 @@ export class GetFeeResolver {
113
117
  isSendAll: isSendAll,
114
118
  getFeeEstimator: this.#getFeeEstimator,
115
119
  allowUnconfirmedRbfEnabledUtxos: this.#allowUnconfirmedRbfEnabledUtxos,
120
+ unconfirmedTxAncestor,
116
121
  })
117
122
  }
118
123
 
@@ -29,6 +29,7 @@ export const selectUtxos = ({
29
29
  disableReplacement = false,
30
30
  mustSpendUtxos,
31
31
  allowUnconfirmedRbfEnabledUtxos,
32
+ unconfirmedTxAncestor,
32
33
  inscriptionIds, // for each inscription transfer, we need to calculate one more input and one more output
33
34
  }) => {
34
35
  const resolvedReceiveAddresses = getBestReceiveAddresses({
@@ -52,7 +53,7 @@ export const selectUtxos = ({
52
53
  ? 'P2WPKH'
53
54
  : 'P2PKH'
54
55
 
55
- const feeEstimator = getFeeEstimator(asset, { feePerKB: feeRate })
56
+ const feeEstimator = getFeeEstimator(asset, { feePerKB: feeRate, unconfirmedTxAncestor })
56
57
  const { currency } = asset
57
58
  if (!amount) amount = currency.ZERO
58
59
 
@@ -78,7 +79,7 @@ export const selectUtxos = ({
78
79
  feePerKB = feeRate
79
80
  }
80
81
 
81
- const replaceFeeEstimator = getFeeEstimator(asset, { feePerKB })
82
+ const replaceFeeEstimator = getFeeEstimator(asset, { feePerKB, unconfirmedTxAncestor })
82
83
  // how to avoid replace tx inputs when inputs are ordinals? !!!!
83
84
  const inputs = UtxoCollection.fromJSON(tx.data.inputs, { currency })
84
85
  const outputs = isSendAll
@@ -118,7 +119,10 @@ export const selectUtxos = ({
118
119
 
119
120
  // Add a new UTXO and recalculate the fee
120
121
  additionalUtxos = additionalUtxos.addUtxo(confirmedUtxosArray.shift())
121
- fee = replaceFeeEstimator({ inputs: inputs.union(additionalUtxos), outputs })
122
+ fee = replaceFeeEstimator({
123
+ inputs: inputs.union(additionalUtxos),
124
+ outputs,
125
+ })
122
126
  }
123
127
  }
124
128
  if (isSendAll || replaceTxAmount.add(additionalUtxos.value).gte(amount.add(fee))) {
@@ -224,6 +228,7 @@ export const getUtxosData = ({
224
228
  mustSpendUtxos,
225
229
  allowUnconfirmedRbfEnabledUtxos,
226
230
  inscriptionIds,
231
+ unconfirmedTxAncestor,
227
232
  }) => {
228
233
  const { selectedUtxos, replaceTx, fee } = selectUtxos({
229
234
  asset,
@@ -237,6 +242,7 @@ export const getUtxosData = ({
237
242
  disableReplacement,
238
243
  mustSpendUtxos,
239
244
  allowUnconfirmedRbfEnabledUtxos,
245
+ unconfirmedTxAncestor,
240
246
  inscriptionIds,
241
247
  })
242
248
 
@@ -252,7 +258,14 @@ export const getUtxosData = ({
252
258
  const spendableBalance = spendableUtxos.value
253
259
 
254
260
  const extraFee = selectedUtxos
255
- ? asset.currency.baseUnit(getExtraFee({ asset, inputs: selectedUtxos, feePerKB: feeRate }))
261
+ ? asset.currency.baseUnit(
262
+ getExtraFee({
263
+ asset,
264
+ inputs: selectedUtxos,
265
+ feePerKB: feeRate,
266
+ unconfirmedTxAncestor,
267
+ })
268
+ )
256
269
  : asset.currency.ZERO
257
270
 
258
271
  const availableBalance = spendableBalance.sub(resolvedFee).clampLowerZero()
@@ -3,7 +3,7 @@ import { isEmpty, isEqual, pickBy } from 'lodash'
3
3
 
4
4
  import { BaseMonitor } from '@exodus/asset-lib'
5
5
  import InsightWSClient from '../insight-api-client/ws'
6
- import { updateUnconfirmedAncestorData } from '../unconfirmed-ancestor-data'
6
+ import { resolveUnconfirmedAncestorData } from '../unconfirmed-ancestor-data'
7
7
  import { BitcoinMonitorScanner } from './bitcoin-monitor-scanner'
8
8
  import { normalizeInsightConfig, toWSUrl } from '../insight-api-client/util'
9
9
  import ms from 'ms'
@@ -153,32 +153,38 @@ export class Monitor extends BaseMonitor {
153
153
  walletAccount,
154
154
  })
155
155
 
156
+ const newData = {}
156
157
  if (utxos || ordinalsUtxos) {
157
- await aci.updateAccountState({
158
- assetName,
159
- walletAccount,
160
- newData: pickBy(
158
+ Object.assign(
159
+ newData,
160
+ pickBy(
161
161
  {
162
162
  utxos,
163
163
  ordinalsUtxos,
164
164
  },
165
165
  Boolean
166
- ),
167
- })
166
+ )
167
+ )
168
168
  }
169
169
 
170
170
  if (txsToUpdate.length) {
171
171
  const accountState = await aci.getAccountState({ assetName, walletAccount })
172
172
  await this.updateTxLog({ assetName, walletAccount, logItems: txsToUpdate })
173
173
  if (['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)) {
174
- updateUnconfirmedAncestorData({
175
- asset,
176
- walletAccount,
174
+ const unconfirmedTxAncestor = await resolveUnconfirmedAncestorData({
175
+ asset: this.asset,
177
176
  accountState,
177
+ walletAccount,
178
178
  insightClient: this.#insightClient,
179
179
  })
180
+ newData.mem = { unconfirmedTxAncestor }
180
181
  }
181
182
  }
183
+ await aci.updateAccountState({
184
+ assetName,
185
+ walletAccount,
186
+ newData,
187
+ })
182
188
  }
183
189
  }
184
190
 
@@ -228,20 +234,19 @@ export class Monitor extends BaseMonitor {
228
234
  walletAccount,
229
235
  refresh,
230
236
  })
231
- const accountState = await aci.getAccountState({ assetName, walletAccount })
232
237
 
238
+ const newData = {}
233
239
  if (utxos || ordinalsUtxos)
234
- await aci.updateAccountState({
235
- assetName,
236
- walletAccount,
237
- newData: pickBy(
240
+ Object.assign(
241
+ newData,
242
+ pickBy(
238
243
  {
239
244
  utxos,
240
245
  ordinalsUtxos,
241
246
  },
242
247
  Boolean
243
- ),
244
- })
248
+ )
249
+ )
245
250
 
246
251
  if (!isEmpty(changedUnusedAddressIndexes)) {
247
252
  // Only for mobile atm, browser and hydra calculates from the latest txLogs
@@ -265,13 +270,21 @@ export class Monitor extends BaseMonitor {
265
270
 
266
271
  // Move to after tick
267
272
  if (['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)) {
268
- updateUnconfirmedAncestorData({
273
+ const accountState = await aci.getAccountState({ assetName, walletAccount })
274
+ const unconfirmedTxAncestor = await resolveUnconfirmedAncestorData({
269
275
  asset: this.asset,
270
- walletAccount,
271
276
  accountState,
277
+ walletAccount,
272
278
  insightClient: this.#insightClient,
273
279
  })
280
+ newData.mem = { unconfirmedTxAncestor }
274
281
  }
282
+
283
+ await aci.updateAccountState({
284
+ assetName,
285
+ walletAccount,
286
+ newData,
287
+ })
275
288
  }
276
289
 
277
290
  #logWsStatus = (message, ...args) => {
@@ -22,6 +22,7 @@ import {
22
22
  } from '../utxos-utils'
23
23
 
24
24
  import * as defaultBitcoinjsLib from '@exodus/bitcoinjs-lib'
25
+ import { getUnconfirmedTxAncestorMap } from '../unconfirmed-ancestor-data'
25
26
 
26
27
  const ASSETS_SUPPORTED_BIP_174 = [
27
28
  'bitcoin',
@@ -209,11 +210,13 @@ export const createAndBroadcastTXFactory = ({
209
210
  const insightClient = asset.baseAsset.insightClient
210
211
  const currency = asset.currency
211
212
  const feeData = await assetClientInterface.getFeeConfig({ assetName })
213
+ const unconfirmedTxAncestor = getUnconfirmedTxAncestorMap({ accountState })
212
214
  const usableUtxos = getUsableUtxos({
213
215
  asset,
214
216
  utxos: getUtxos({ accountState, asset }),
215
217
  feeData,
216
218
  txSet,
219
+ unconfirmedTxAncestor,
217
220
  })
218
221
 
219
222
  let replaceableTxs = findUnconfirmedSentRbfTxs(txSet)
@@ -257,9 +260,10 @@ export const createAndBroadcastTXFactory = ({
257
260
  feeRate: customFee || feeRate,
258
261
  receiveAddress: receiveAddress,
259
262
  isSendAll: resolvedIsSendAll,
260
- getFeeEstimator: (asset, { feePerKB }) => getFeeEstimator(asset, feePerKB),
263
+ getFeeEstimator: (asset, { feePerKB, ...options }) => getFeeEstimator(asset, feePerKB, options),
261
264
  mustSpendUtxos: utxosToBump,
262
265
  allowUnconfirmedRbfEnabledUtxos,
266
+ unconfirmedTxAncestor,
263
267
  inscriptionIds,
264
268
  })
265
269
 
package/src/tx-utils.js CHANGED
@@ -11,14 +11,25 @@ export const findUnconfirmedSentRbfTxs = (txSet) =>
11
11
  tx.data.inputs
12
12
  )
13
13
 
14
- export const findLargeUnconfirmedTxs = ({ assetName, txSet, feeRate, maxFee }) =>
14
+ export const findLargeUnconfirmedTxs = ({
15
+ assetName,
16
+ txSet,
17
+ feeRate,
18
+ maxFee,
19
+ unconfirmedTxAncestor,
20
+ }) =>
15
21
  !txSet
16
22
  ? new Set()
17
23
  : new Set(
18
24
  Array.from(txSet)
19
25
  .filter((tx) => {
20
26
  if (!tx.pending) return false
21
- const extraFee = resolveExtraFeeOfTx({ assetName, feeRate, txId: tx.txId })
27
+ const extraFee = resolveExtraFeeOfTx({
28
+ assetName,
29
+ feeRate,
30
+ txId: tx.txId,
31
+ unconfirmedTxAncestor,
32
+ })
22
33
  return extraFee && extraFee > maxFee
23
34
  })
24
35
  .map((tx) => tx.txId)
@@ -1,17 +1,16 @@
1
1
  import { getUtxos } from './utxos-utils'
2
2
 
3
- const dataMap = new Map()
4
-
5
- export async function updateUnconfirmedAncestorData({
3
+ export async function resolveUnconfirmedAncestorData({
6
4
  asset,
7
5
  walletAccount,
8
6
  accountState,
9
7
  insightClient,
10
8
  }) {
11
- for (const [txId, stored] of dataMap) {
9
+ const dataMap = getUnconfirmedTxAncestorMap({ accountState })
10
+ Object.entries(dataMap).forEach(([txId, stored]) => {
12
11
  if (stored.walletAccount === walletAccount && stored.assetName === asset.name)
13
- dataMap.delete(txId)
14
- }
12
+ delete dataMap[txId]
13
+ })
15
14
 
16
15
  const utxos = getUtxos({ accountState, asset })
17
16
  .toArray()
@@ -23,24 +22,29 @@ export async function updateUnconfirmedAncestorData({
23
22
  try {
24
23
  const { size, fees } = await insightClient.fetchUnconfirmedAncestorData(txId)
25
24
  if (size !== 0) {
26
- dataMap.set(txId, { assetName: asset.name, walletAccount, size, fees })
25
+ dataMap[txId] = { assetName: asset.name, walletAccount, size, fees }
27
26
  }
28
27
  } catch (e) {
29
28
  console.warn(e)
30
29
  }
31
30
  }
31
+ return dataMap
32
+ }
33
+
34
+ export function getUnconfirmedTxAncestorMap({ accountState }) {
35
+ return accountState?.mem?.unconfirmedTxAncestor || {}
32
36
  }
33
37
 
34
- function getUnconfirmedAncestorData({ txId, assetName }) {
35
- const stored = dataMap.get(txId)
38
+ function getUnconfirmedAncestorData({ txId, assetName, unconfirmedTxAncestor = {} }) {
39
+ const stored = unconfirmedTxAncestor[txId]
36
40
  if (stored?.assetName === assetName) {
37
41
  return { size: stored.size, fees: stored.fees }
38
42
  }
39
43
  return undefined
40
44
  }
41
45
 
42
- export function resolveExtraFeeOfTx({ assetName, feeRate, txId }) {
43
- const data = getUnconfirmedAncestorData({ assetName, txId })
46
+ export function resolveExtraFeeOfTx({ assetName, feeRate, txId, unconfirmedTxAncestor }) {
47
+ const data = getUnconfirmedAncestorData({ assetName, txId, unconfirmedTxAncestor })
44
48
  if (!data) return 0
45
49
  const { fees, size } = data
46
50
  // Get the difference in fee rate between ancestors and current estimate
@@ -178,7 +178,7 @@ function filterDustUtxos({ utxos, feeData }) {
178
178
  return utxos
179
179
  }
180
180
 
181
- export function getUsableUtxos({ asset, utxos, feeData, txSet }) {
181
+ export function getUsableUtxos({ asset, utxos, feeData, txSet, unconfirmedTxAncestor }) {
182
182
  assert(asset, 'asset is required')
183
183
  assert(utxos, 'utxos is required')
184
184
  assert(feeData, 'feeData is required')
@@ -195,6 +195,7 @@ export function getUsableUtxos({ asset, utxos, feeData, txSet }) {
195
195
  txSet,
196
196
  feeRate,
197
197
  maxFee,
198
+ unconfirmedTxAncestor,
198
199
  })
199
200
  const confirmedAndSmallUtxos =
200
201
  largeUnconfirmedTxs.size === 0