@exodus/bitcoin-api 2.6.8 → 2.7.1

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.6.8",
3
+ "version": "2.7.1",
4
4
  "description": "Exodus bitcoin-api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -43,5 +43,5 @@
43
43
  "@scure/btc-signer": "^1.1.0",
44
44
  "jest-when": "^3.5.1"
45
45
  },
46
- "gitHead": "eae322a1c75f9cdaebc919af510619cb59ba4c0e"
46
+ "gitHead": "9c993eed3a3bcfc1c49d86377fdc5c7cb731d632"
47
47
  }
@@ -10,6 +10,7 @@ export function createAccountState({ asset, ordinalsEnabled = false, brc20Enable
10
10
 
11
11
  if (ordinalsEnabled) {
12
12
  defaults.ordinalsUtxos = empty
13
+ defaults.knownBalanceUtxoIds = []
13
14
  }
14
15
 
15
16
  if (brc20Enabled) {
@@ -2,6 +2,7 @@
2
2
  import urlJoin from 'url-join'
3
3
  import qs from 'querystring'
4
4
  import delay from 'delay'
5
+ import { isEmpty } from 'lodash'
5
6
 
6
7
  const getTextFromResponse = async (response) => {
7
8
  try {
@@ -105,13 +106,22 @@ export default class InsightAPIClient {
105
106
  const url = urlJoin(this._baseURL, `/tx/${encodedTxId}`)
106
107
  const response = await fetch(url)
107
108
 
108
- // change in https://github.com/jprichardson/exodus-rn/pull/4336 may break Insight compatibility
109
- // we're probably past the point of just spinning up an Insight server and have it plug n' play for Magnifier
110
-
111
109
  if (response.status === 404) return null
112
110
  return response.json()
113
111
  }
114
112
 
113
+ async fetchTxObject(txId) {
114
+ const url = urlJoin(this._baseURL, `/fulltx?${new URLSearchParams({ hash: txId })}`)
115
+ const response = await fetch(url)
116
+
117
+ if (response.status === 404) return null
118
+ const object = await response.json()
119
+ if (isEmpty(object)) {
120
+ return null
121
+ }
122
+ return object
123
+ }
124
+
115
125
  async fetchRawTx(txId) {
116
126
  const encodedTxId = encodeURIComponent(txId)
117
127
  const url = urlJoin(this._baseURL, `/rawtx/${encodedTxId}`)
@@ -1,11 +1,12 @@
1
1
  import { orderTxs } from '../insight-api-client/util'
2
2
  import { Address, UtxoCollection } from '@exodus/models'
3
- import { isEqual, compact, uniq } from 'lodash'
3
+ import { compact, isEqual, uniq } from 'lodash'
4
4
  import ms from 'ms'
5
5
  import assert from 'minimalistic-assert'
6
6
  import { isChangeAddress, isReceiveAddress } from '../address-utils'
7
7
  import { getOrdinalsUtxos, getUtxos, partitionUtxos } from '../utxos-utils'
8
8
  import { getOrdinalAddress } from '../ordinals-utils'
9
+ import { indexOrdinalUnconfirmedTx } from './ordinals-indexer-utils'
9
10
 
10
11
  // Time to check whether to drop a sent tx
11
12
  const SENT_TIME_TO_DROP = ms('2m')
@@ -67,7 +68,6 @@ export class BitcoinMonitorScanner {
67
68
 
68
69
  const storedUtxos = getUtxos({ asset, accountState })
69
70
  const storedOrdinalUtxos = getOrdinalsUtxos({ asset, accountState })
70
- const ordinalAddress = await this.getOrdinalAddress({ walletAccount })
71
71
 
72
72
  const currentStoredUtxos = storedUtxos.union(storedOrdinalUtxos)
73
73
 
@@ -90,7 +90,9 @@ export class BitcoinMonitorScanner {
90
90
  : (txs) =>
91
91
  txs.filter((tx) => {
92
92
  const txItem = currentTxs.get(tx.txid)
93
- return txItem && txItem.confirmed
93
+ const confirmed = txItem && txItem.confirmed
94
+ const inscriptionsIndexed = txItem?.data?.inscriptionsIndexed
95
+ return confirmed && (!this.#ordinalsEnabled || inscriptionsIndexed)
94
96
  }).length >= txs.length
95
97
 
96
98
  const unusedAddressIndexes = await assetClientInterface.getUnusedAddressIndexes({
@@ -190,8 +192,30 @@ export class BitcoinMonitorScanner {
190
192
  promises.push(promise)
191
193
  }
192
194
 
193
- const txArrays = await Promise.all(promises)
194
- return txArrays.reduce((total, some) => total.concat(some), [])
195
+ const insightTxs = (await Promise.all(promises)).reduce(
196
+ (total, some) => total.concat(some),
197
+ []
198
+ )
199
+ if (!this.#ordinalsEnabled) {
200
+ return insightTxs
201
+ }
202
+ return Promise.all(
203
+ insightTxs.map((tx) => {
204
+ try {
205
+ return indexOrdinalUnconfirmedTx({
206
+ tx,
207
+ currency: this.#asset.currency,
208
+ insightClient,
209
+ })
210
+ } catch (e) {
211
+ console.warn(
212
+ `Could not index ${asset.name} ordinal tx ${tx.txid} for wallet account ${walletAccount}. message: ${e.message}`,
213
+ e
214
+ )
215
+ return tx
216
+ }
217
+ })
218
+ )
195
219
  }
196
220
 
197
221
  const gapSearchParameters = newChains.map(({ purpose, chain }) => {
@@ -344,6 +368,15 @@ export class BitcoinMonitorScanner {
344
368
  currencies: { [assetName]: currency },
345
369
  }
346
370
 
371
+ if (this.#ordinalsEnabled) {
372
+ txLogItem.data = {
373
+ ...txLogItem.data,
374
+ inscriptionsIndexed: txItem.inscriptionsIndexed,
375
+ sentInscriptions: [],
376
+ receivedInscriptions: [],
377
+ }
378
+ }
379
+
347
380
  let from = []
348
381
 
349
382
  // if txItem.vin has an address that matches ours, means we've spent this tx
@@ -361,8 +394,13 @@ export class BitcoinMonitorScanner {
361
394
  txLogItem.coinAmount = txLogItem.coinAmount.sub(currency.defaultUnit(vin.value))
362
395
  isSent = true
363
396
  txLogItem.data.sent = []
364
- if (ordinalAddress && address.toString() === ordinalAddress.toString()) {
365
- txLogItem.data.isInscriptionSent = true
397
+ if (this.#ordinalsEnabled && vin.inscriptions) {
398
+ txLogItem.data.sentInscriptions.push(
399
+ ...vin.inscriptions.map((i) => ({
400
+ ...i,
401
+ value: currency.defaultUnit(vin.value).toBaseNumber(),
402
+ }))
403
+ )
366
404
  }
367
405
 
368
406
  // this is only used to exclude the utxos in the reducer which is why we don't care about the other fields
@@ -432,8 +470,13 @@ export class BitcoinMonitorScanner {
432
470
  txLogItem.data.changeAddress = address
433
471
  }
434
472
 
435
- if (ordinalAddress && address.toString() === ordinalAddress.toString()) {
436
- txLogItem.data.isInscriptionReceived = true
473
+ if (this.#ordinalsEnabled && vout.inscriptions) {
474
+ txLogItem.data.receivedInscriptions.push(
475
+ ...vout.inscriptions.map((i) => ({
476
+ ...i,
477
+ value: currency.defaultUnit(vout.value).toBaseNumber(),
478
+ }))
479
+ )
437
480
  }
438
481
 
439
482
  // it was sent to us...
@@ -450,6 +493,11 @@ export class BitcoinMonitorScanner {
450
493
  rbfEnabled: txItem.rbf,
451
494
  }
452
495
 
496
+ if (this.#ordinalsEnabled) {
497
+ output.inscriptionsIndexed = txItem.inscriptionsIndexed
498
+ output.inscriptions = vout.inscriptions || []
499
+ }
500
+
453
501
  if (this.#shouldExcludeVoutUtxo({ asset, output, txItem, vout })) {
454
502
  return
455
503
  }
@@ -540,10 +588,13 @@ export class BitcoinMonitorScanner {
540
588
  return !isEqual(chain, originalChain.chain)
541
589
  })
542
590
 
591
+ const ordinalAddress = await this.getOrdinalAddress({ walletAccount })
543
592
  const utxosData = utxoCol
544
593
  ? partitionUtxos({
545
594
  allUtxos: utxoCol,
595
+ ordinalsEnabled: this.#ordinalsEnabled,
546
596
  ordinalAddress,
597
+ knownBalanceUtxoIds: accountState.knownBalanceUtxoIds,
547
598
  })
548
599
  : {}
549
600
 
@@ -603,7 +654,9 @@ export class BitcoinMonitorScanner {
603
654
 
604
655
  const { utxos, ordinalsUtxos } = partitionUtxos({
605
656
  allUtxos: txConfirmedUtxos,
657
+ ordinalsEnabled: this.#ordinalsEnabled,
606
658
  ordinalAddress: await this.getOrdinalAddress({ walletAccount }),
659
+ knownBalanceUtxoIds: accountState.knownBalanceUtxoIds,
607
660
  })
608
661
 
609
662
  return {
@@ -0,0 +1,53 @@
1
+ import { memoizeLruCache } from '@exodus/asset-lib'
2
+ import { cloneDeep } from 'lodash'
3
+
4
+ export const indexOutputs = ({ tx, currency }) => {
5
+ const inscriptions = []
6
+
7
+ let inputOffset = 0
8
+ for (let i = 0; i < tx.vin.length; i++) {
9
+ const vin = tx.vin[i]
10
+ const value = currency.defaultUnit(vin.value).toBaseNumber()
11
+ inscriptions.push(
12
+ ...(vin.inscriptions || []).map((i) => ({ ...i, offset: i.offset + inputOffset }))
13
+ )
14
+ inputOffset = value
15
+ }
16
+
17
+ let outputOffset = 0
18
+ for (let i = 0; i < tx.vout.length; i++) {
19
+ const vout = tx.vout[i]
20
+ const value = currency.defaultUnit(vout.value).toBaseNumber()
21
+ vout.inscriptions = inscriptions
22
+ .map((i) => ({ ...i, offset: i.offset - outputOffset }))
23
+ .filter((i) => i.offset >= 0 && i.offset < value)
24
+ outputOffset = value
25
+ }
26
+ tx.inscriptionsMemoryIndexed = true // avoids btc being spent even when the mempool index was done in memory
27
+ return tx
28
+ }
29
+
30
+ export const indexOrdinalUnconfirmedTx = memoizeLruCache(
31
+ async ({ insightClient, currency, tx }) => {
32
+ if (tx.inscriptionsIndexed || tx.inscriptionsMemoryIndexed) {
33
+ return tx
34
+ }
35
+ const copyTx = cloneDeep(tx)
36
+ await Promise.all(
37
+ copyTx.vin.map(async (vin) => {
38
+ const outputTx = await indexOrdinalUnconfirmedTx({
39
+ insightClient,
40
+ currency,
41
+ tx: await insightClient.fetchTxObject(vin.txid),
42
+ })
43
+ if (!outputTx.inscriptionsIndexed && !outputTx.inscriptionsMemoryIndexed) {
44
+ throw new Error(`Cannot index ${tx.txid}. Input tx ${outputTx.txid} is not indexed. `)
45
+ }
46
+ vin.inscriptions = outputTx.vout[vin.vout].inscriptions
47
+ })
48
+ )
49
+ return indexOutputs({ tx: copyTx, currency })
50
+ },
51
+ ({ tx }) => tx.txid,
52
+ 100
53
+ )
@@ -14,7 +14,12 @@ import {
14
14
  createOutput as dogecoinCreateOutput,
15
15
  } from './dogecoin'
16
16
  import { findUnconfirmedSentRbfTxs } from '../tx-utils'
17
- import { getOrdinalsUtxos, getUsableUtxos, getUtxos } from '../utxos-utils'
17
+ import {
18
+ getOrdinalsUtxos,
19
+ getTransferOrdinalsUtxos,
20
+ getUsableUtxos,
21
+ getUtxos,
22
+ } from '../utxos-utils'
18
23
 
19
24
  import * as defaultBitcoinjsLib from '@exodus/bitcoinjs-lib'
20
25
 
@@ -132,6 +137,7 @@ export const createAndBroadcastTXFactory = ({
132
137
  getFeeEstimator,
133
138
  getSizeAndChangeScript = getSizeAndChangeScriptFactory(), // for decred customizations
134
139
  allowUnconfirmedRbfEnabledUtxos,
140
+ ordinalsEnabled = false,
135
141
  }) => async (
136
142
  { asset: maybeToken, walletAccount, address, amount: tokenAmount, options },
137
143
  { assetClientInterface }
@@ -167,6 +173,11 @@ export const createAndBroadcastTXFactory = ({
167
173
 
168
174
  const inscriptionIds = nft?.tokenId ? [nft?.tokenId] : brc20 ? brc20.inscriptionIds : undefined
169
175
 
176
+ assert(
177
+ ordinalsEnabled || !inscriptionIds,
178
+ 'inscriptions cannot be sent when ordinalsEnabled=false '
179
+ )
180
+
170
181
  const shuffle = (list) => {
171
182
  return inscriptionIds ? list : doShuffle(list) // don't shuffle when sending ordinal!!!!
172
183
  }
@@ -192,26 +203,9 @@ export const createAndBroadcastTXFactory = ({
192
203
 
193
204
  const currentOrdinalsUtxos = getOrdinalsUtxos({ accountState, asset })
194
205
  const transferOrdinalsUtxos = inscriptionIds
195
- ? currentOrdinalsUtxos.filter((utxo) => inscriptionIds.includes(utxo.inscriptionId)) /// this could be bulk transfer if multiple inscription ids are provided
206
+ ? getTransferOrdinalsUtxos({ inscriptionIds, ordinalsUtxos: currentOrdinalsUtxos })
196
207
  : undefined
197
208
 
198
- if (inscriptionIds) {
199
- assert(
200
- transferOrdinalsUtxos?.size === inscriptionIds.length,
201
- `Expected ordinal utxos ${inscriptionIds.length}. Found: ${transferOrdinalsUtxos?.size || 0}`
202
- )
203
-
204
- // const unconfirmedOrdinalUtxos = transferOrdinalsUtxos
205
- // .toArray()
206
- // .filter((ordinalUtxo) => !(ordinalUtxo.confirmations > 0))
207
- // assert(
208
- // !unconfirmedOrdinalUtxos.length,
209
- // `OrdinalUtxo with inscription ids ${unconfirmedOrdinalUtxos
210
- // .map((utxo) => utxo.inscriptionId)
211
- // .join(', ')} have not confirmed yet`
212
- // )
213
- }
214
-
215
209
  const insightClient = asset.baseAsset.insightClient
216
210
  const currency = asset.currency
217
211
  const feeData = await assetClientInterface.getFeeConfig({ assetName })
@@ -434,6 +428,8 @@ export const createAndBroadcastTXFactory = ({
434
428
 
435
429
  const { script, size } = getSizeAndChangeScript({ assetName, tx, rawTx, changeUtxoIndex, txId })
436
430
 
431
+ // for ordinals, used to allow users spending change utxos even when unconfirmed and ordinals are unknown
432
+ const knownBalanceUtxoIds = accountState.knownBalanceUtxoIds || []
437
433
  let remainingUtxos = usableUtxos.difference(selectedUtxos)
438
434
  if (changeUtxoIndex !== -1) {
439
435
  const address = Address.create(ourAddress.address, ourAddress.meta)
@@ -446,6 +442,8 @@ export const createAndBroadcastTXFactory = ({
446
442
  confirmations: 0,
447
443
  rbfEnabled,
448
444
  }
445
+
446
+ knownBalanceUtxoIds.push(`${changeUtxo.txId}:${changeUtxo.vout}`.toLowerCase())
449
447
  remainingUtxos = remainingUtxos.addUtxo(changeUtxo)
450
448
  }
451
449
  if (replaceTx) {
@@ -454,7 +452,7 @@ export const createAndBroadcastTXFactory = ({
454
452
 
455
453
  const remainingOrdinalsUtxos = transferOrdinalsUtxos
456
454
  ? currentOrdinalsUtxos.difference(transferOrdinalsUtxos)
457
- : undefined
455
+ : currentOrdinalsUtxos
458
456
 
459
457
  await assetClientInterface.updateAccountState({
460
458
  assetName,
@@ -462,6 +460,7 @@ export const createAndBroadcastTXFactory = ({
462
460
  newData: {
463
461
  utxos: remainingUtxos,
464
462
  ordinalsUtxos: remainingOrdinalsUtxos,
463
+ knownBalanceUtxoIds,
465
464
  },
466
465
  })
467
466
 
@@ -522,7 +521,16 @@ export const createAndBroadcastTXFactory = ({
522
521
  inputs: selectedUtxos.toJSON(),
523
522
  replacedTxId: replaceTx ? replaceTx.txId : undefined,
524
523
  nftId: nft ? `${assetName}:${nft.tokenId}` : undefined, // it allows BE to load the nft info while the nft is in transit
525
- inscriptionIds: inscriptionIds,
524
+ inscriptionsIndexed: ordinalsEnabled ? true : undefined,
525
+ sentInscriptions: inscriptionIds
526
+ ? inscriptionIds.map((inscriptionId) => {
527
+ return {
528
+ inscriptionId,
529
+ offset: 0,
530
+ value: 0,
531
+ }
532
+ })
533
+ : undefined,
526
534
  },
527
535
  },
528
536
  ],
@@ -0,0 +1,29 @@
1
+ export function extractTransaction({ psbt, skipFinalize }) {
2
+ // If a dapp authored the TX, it expects a serialized PSBT response.
3
+ // Note: we wouldn't be able to finalise inputs in some cases that's why we serialize before finalizing inputs.
4
+
5
+ if (skipFinalize) {
6
+ const rawPSBT = psbt.toBuffer()
7
+
8
+ return { plainTx: { rawPSBT } }
9
+ } else {
10
+ // Serialize tx
11
+ psbt.finalizeAllInputs()
12
+ const tx = psbt.extractTransaction()
13
+ const rawTx = tx.toBuffer()
14
+ const txId = tx.getId()
15
+
16
+ // tx needs to be serializable for desktop RPC send => sign communication
17
+ return { rawTx, txId, tx: serializeTx({ tx }) }
18
+ }
19
+ }
20
+
21
+ export const serializeTx = ({ tx }) => {
22
+ // for desktop compatibility
23
+ return {
24
+ virtualSize: tx.virtualSize?.(),
25
+ outs: tx.outs?.map((out) => ({
26
+ script: out.script.toString('hex'),
27
+ })),
28
+ }
29
+ }
@@ -0,0 +1,47 @@
1
+ import lodash from 'lodash'
2
+ import assert from 'minimalistic-assert'
3
+ import { getOwnProperty } from '@exodus/basic-utils'
4
+ import { getECPair } from '../bitcoinjs-lib'
5
+
6
+ import secp256k1 from 'secp256k1'
7
+
8
+ const ECPair = getECPair()
9
+
10
+ export const createGetKeyAndPurpose = ({
11
+ keys,
12
+ hdkeys,
13
+ resolvePurpose,
14
+ addressPathsMap,
15
+ privateKeysAddressMap,
16
+ coinInfo,
17
+ }) =>
18
+ lodash.memoize((address) => {
19
+ const purpose = resolvePurpose(address)
20
+ const networkInfo = coinInfo.toBitcoinJS()
21
+ if (privateKeysAddressMap) {
22
+ return getPrivateKeyFromMap(privateKeysAddressMap, networkInfo, purpose, address)
23
+ } else {
24
+ return getPrivateKeyFromHDKeys(hdkeys, addressPathsMap, keys, networkInfo, purpose, address)
25
+ }
26
+ })
27
+
28
+ function getPrivateKeyFromMap(privateKeysAddressMap, networkInfo, purpose, address) {
29
+ const privateKey = getOwnProperty(privateKeysAddressMap, address, 'string')
30
+ assert(privateKey, `there is no private key for address ${address}`)
31
+ const key = ECPair.fromWIF(privateKey, networkInfo)
32
+ const publicKey = secp256k1.publicKeyCreate(key.privateKey, true)
33
+ return { key, purpose, publicKey }
34
+ }
35
+
36
+ function getPrivateKeyFromHDKeys(hdkeys, addressPathsMap, keys, networkInfo, purpose, address) {
37
+ const path = getOwnProperty(addressPathsMap, address, 'string')
38
+ assert(hdkeys, 'hdkeys must be provided')
39
+ assert(purpose, `purpose for address ${address} could not be resolved`)
40
+ const hdkey = hdkeys[purpose]
41
+ assert(hdkey, `hdkey for purpose for ${purpose} and address ${address} could not be resolved`)
42
+ const derivedhdkey = hdkey.derive(path)
43
+ const privateEncoded = keys.encodePrivate(derivedhdkey.privateKey)
44
+ const key = ECPair.fromWIF(privateEncoded, networkInfo)
45
+ const publicKey = derivedhdkey.publicKey
46
+ return { key, publicKey, purpose }
47
+ }
@@ -0,0 +1,70 @@
1
+ import { Transaction, payments } from '@exodus/bitcoinjs-lib'
2
+
3
+ import { getECPair } from '../bitcoinjs-lib'
4
+ import { toXOnly } from '../bitcoinjs-lib/ecc-utils'
5
+ import { createGetKeyAndPurpose } from './create-get-key-and-purpose'
6
+ import { toAsyncSigner, tweakSigner } from './taproot'
7
+
8
+ const ECPair = getECPair()
9
+
10
+ export function createSignWithWallet({
11
+ keys,
12
+ hdkeys,
13
+ resolvePurpose,
14
+ privateKeysAddressMap,
15
+ addressPathsMap,
16
+ coinInfo,
17
+ network,
18
+ }) {
19
+ const getKeyAndPurpose = createGetKeyAndPurpose({
20
+ keys,
21
+ hdkeys,
22
+ resolvePurpose,
23
+ privateKeysAddressMap,
24
+ addressPathsMap,
25
+ coinInfo,
26
+ })
27
+
28
+ return async (psbt, inputsToSign) => {
29
+ // The Taproot SIGHASH flag includes all previous outputs,
30
+ // so signing is only done AFTER all inputs have been updated
31
+ for (let index = 0; index < psbt.inputCount; index++) {
32
+ const inputInfo = inputsToSign[index]
33
+ // dApps request to sign only specific transaction inputs.
34
+ if (!inputInfo) continue
35
+ const { address, sigHash } = inputInfo
36
+ // TODO: we can remove SIGHASH_ALL, it is default.
37
+ const sigHashTypes = sigHash !== undefined ? [sigHash || Transaction.SIGHASH_ALL] : undefined
38
+ const { key, purpose, publicKey } = getKeyAndPurpose(address)
39
+
40
+ const isP2SH = purpose === 49
41
+ const isTaprootAddress = purpose === 86
42
+
43
+ if (isP2SH) {
44
+ // If spending from a P2SH address, we assume the address is P2SH wrapping
45
+ // P2WPKH. Exodus doesn't use P2SH addresses so we should only ever be
46
+ // signing a P2SH input if we are importing a private key
47
+ // BIP143: As a default policy, only compressed public keys are accepted in P2WPKH and P2WSH
48
+ const p2wpkh = payments.p2wpkh({ pubkey: publicKey })
49
+ const p2sh = payments.p2sh({ redeem: p2wpkh })
50
+ if (address === p2sh.address && !Buffer.isBuffer(psbt.data.inputs[index].redeemScript)) {
51
+ psbt.updateInput(index, {
52
+ redeemScript: p2sh.redeem.output,
53
+ })
54
+ } else {
55
+ throw new Error('Expected P2SH script to be a nested segwit input')
56
+ }
57
+ } else if (isTaprootAddress && !Buffer.isBuffer(psbt.data.inputs[index].tapInternalKey)) {
58
+ // tapInternalKey is metadata for signing and not part of the hash to sign.
59
+ // so modifying it here is fine.
60
+ psbt.updateInput(index, {
61
+ tapInternalKey: toXOnly(publicKey),
62
+ })
63
+ }
64
+
65
+ // desktop / BE / mobile with bip-schnorr signing
66
+ const signingKey = isTaprootAddress ? tweakSigner({ signer: key, ECPair, network }) : key
67
+ await psbt.signInputAsync(index, toAsyncSigner({ keyPair: signingKey }), sigHashTypes)
68
+ }
69
+ }
70
+ }
@@ -1,91 +1,8 @@
1
1
  import assert from 'minimalistic-assert'
2
- import lodash from 'lodash'
3
- import { payments, Psbt, Transaction } from '@exodus/bitcoinjs-lib'
4
- import { getOwnProperty } from '@exodus/basic-utils'
5
2
 
6
- import secp256k1 from 'secp256k1'
7
-
8
- import { toXOnly } from '../bitcoinjs-lib/ecc-utils'
9
- import { toAsyncSigner, tweakSigner } from './taproot'
10
- import { getECPair } from '../bitcoinjs-lib'
11
-
12
- let ECPair
13
-
14
- const _MAXIMUM_FEE_RATES = {
15
- qtumignition: 25000,
16
- ravencoin: 1000000,
17
- }
18
-
19
- const canParseTx = (rawTxBuffer) => {
20
- try {
21
- Transaction.fromBuffer(rawTxBuffer)
22
- return true
23
- } catch (e) {
24
- return false
25
- }
26
- }
27
-
28
- export const serializeTx = ({ tx }) => {
29
- // for desktop compatibility
30
- return {
31
- virtualSize: tx.virtualSize?.(),
32
- outs: tx.outs?.map((out) => ({
33
- script: out.script.toString('hex'),
34
- })),
35
- }
36
- }
37
-
38
- // Creates a PSBT instance from the passed inputs, outputs etc. The wallet itself provides this data.
39
- function createPSBT({ inputs, outputs, rawTxs, networkInfo, getKeyAndPurpose, assetName }) {
40
- // use harcoded max fee rates for specific assets
41
- // if undefined, will be set to default value by PSBT (2500)
42
- const maximumFeeRate = _MAXIMUM_FEE_RATES[assetName]
43
-
44
- const psbt = new Psbt({ maximumFeeRate, network: networkInfo })
45
-
46
- // Fill tx
47
- for (const { txId, vout, address, value, script, sequence } of inputs) {
48
- const { purpose, publicKey } = getKeyAndPurpose(address)
49
-
50
- const isSegwitAddress = purpose === 84
51
- const isTaprootAddress = purpose === 86
52
- const txIn = { hash: txId, index: vout, sequence }
53
- if (isSegwitAddress || isTaprootAddress) {
54
- // witness outputs only require the value and the script, not the full transaction
55
- txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
56
- if (isTaprootAddress) {
57
- txIn.tapInternalKey = toXOnly(publicKey)
58
- }
59
- } else {
60
- const rawTx = (rawTxs || []).find((t) => t.txId === txId)
61
- // non-witness outptus require the full transaction
62
- assert(!!rawTx, 'Non-witness outputs require the full previous transaction.')
63
- const rawTxBuffer = Buffer.from(rawTx.rawData, 'hex')
64
- if (canParseTx(rawTxBuffer)) {
65
- txIn.nonWitnessUtxo = rawTxBuffer
66
- } else {
67
- // temp fix for https://exodusio.slack.com/archives/CP202D90Q/p1671014704829939 until bitcoinjs could parse a mweb tx without failing
68
- console.warn(`Setting psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true for asset ${assetName}`)
69
- psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true
70
- txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
71
- }
72
- }
73
- psbt.addInput(txIn)
74
- }
75
-
76
- for (const [address, amount] of outputs) {
77
- psbt.addOutput({ value: amount, address })
78
- }
79
-
80
- return psbt
81
- }
82
-
83
- // Creates a PSBT instance from the passed transaction buffer provided by 3rd parties (e.g. dApps).
84
- function createPSBTFromBuffer({ psbtBuffer, ecc }) {
85
- const psbt = Psbt.fromBuffer(psbtBuffer, { eccLib: ecc })
86
-
87
- return psbt
88
- }
3
+ import { createPrepareForSigning } from './default-prepare-for-signing'
4
+ import { createSignWithWallet } from './create-sign-with-wallet'
5
+ import { extractTransaction } from './common'
89
6
 
90
7
  export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, network }) => {
91
8
  assert(assetName, 'assetName is required')
@@ -93,91 +10,34 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, netwo
93
10
  assert(keys, 'keys is required')
94
11
  assert(coinInfo, 'coinInfo is required')
95
12
 
13
+ const prepareForSigning = createPrepareForSigning({
14
+ assetName,
15
+ resolvePurpose,
16
+ coinInfo,
17
+ })
18
+
96
19
  return async ({ unsignedTx, hdkeys, privateKeysAddressMap }) => {
97
20
  assert(unsignedTx, 'unsignedTx is required')
98
21
  assert(hdkeys || privateKeysAddressMap, 'hdkeys or privateKeysAddressMap is required')
99
- const { addressPathsMap } = unsignedTx.txMeta
100
- const networkInfo = { ...coinInfo.toBitcoinJS(), messagePrefix: '' }
101
22
 
102
- ECPair = getECPair()
23
+ const { addressPathsMap } = unsignedTx.txMeta
103
24
 
104
- const getKeyAndPurpose = lodash.memoize((address) => {
105
- const purpose = resolvePurpose(address)
106
- if (privateKeysAddressMap) {
107
- const privateKey = getOwnProperty(privateKeysAddressMap, address, 'string')
108
- assert(privateKey, `there is no private key for address ${address}`)
109
- const key = ECPair.fromWIF(privateKey, networkInfo)
110
- const publicKey = secp256k1.publicKeyCreate(key.privateKey, true)
111
- return { key, purpose, publicKey }
112
- }
113
- const path = getOwnProperty(addressPathsMap, address, 'string')
114
- assert(hdkeys, 'hdkeys must be provided')
115
- assert(purpose, `purpose for address ${address} could not be resolved`)
116
- const hdkey = hdkeys[purpose]
117
- assert(hdkey, `hdkey for purpose for ${purpose} and address ${address} could not be resolved`)
118
- const derivedhdkey = hdkey.derive(path)
119
- const privateEncoded = keys.encodePrivate(derivedhdkey.privateKey)
120
- const key = ECPair.fromWIF(privateEncoded, networkInfo)
121
- const publicKey = derivedhdkey.publicKey
122
- return { key, publicKey, purpose }
25
+ const psbt = prepareForSigning({ unsignedTx })
26
+
27
+ const inputsToSign = unsignedTx.txMeta.inputsToSign || unsignedTx.txData.inputs
28
+ const signWithWallet = createSignWithWallet({
29
+ keys,
30
+ hdkeys,
31
+ resolvePurpose,
32
+ privateKeysAddressMap,
33
+ addressPathsMap,
34
+ coinInfo,
35
+ network,
123
36
  })
124
37
 
125
- const isPsbtBufferPassed =
126
- unsignedTx.txData.psbtBuffer &&
127
- unsignedTx.txMeta.addressPathsMap &&
128
- unsignedTx.txMeta.inputsToSign
129
- const psbt = isPsbtBufferPassed
130
- ? createPSBTFromBuffer({ psbtBuffer: unsignedTx.txData.psbtBuffer })
131
- : createPSBT({ ...unsignedTx.txData, ...unsignedTx.txMeta, getKeyAndPurpose, networkInfo })
132
- const { inputs } = unsignedTx.txData
133
-
134
- if (!['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)) psbt.setVersion(1)
135
-
136
- const inputsToSign = isPsbtBufferPassed ? unsignedTx.txMeta.inputsToSign : inputs
137
-
138
- // The Taproot SIGHASH flag includes all previous outputs,
139
- // so signing is only done AFTER all inputs have been updated
140
- for (let index = 0; index < psbt.inputCount; index++) {
141
- const inputInfo = inputsToSign[index]
142
- // dApps request to sign only specific transaction inputs.
143
- if (!inputInfo) continue
144
- const { address, sigHash } = inputInfo
145
- const sigHashTypes = sigHash !== undefined ? [sigHash || Transaction.SIGHASH_ALL] : undefined
146
- const { key, purpose, publicKey } = getKeyAndPurpose(address)
147
-
148
- // We shouldn't modify dApp PSBTs.
149
- if (purpose === 49 && !isPsbtBufferPassed) {
150
- // If spending from a P2SH address, we assume the address is P2SH wrapping
151
- // P2WPKH. Exodus doesn't use P2SH addresses so we should only ever be
152
- // signing a P2SH input if we are importing a private key
153
- // BIP143: As a default policy, only compressed public keys are accepted in P2WPKH and P2WSH
154
- const p2wpkh = payments.p2wpkh({ pubkey: publicKey })
155
- const p2sh = payments.p2sh({ redeem: p2wpkh })
156
- psbt.updateInput(index, {
157
- redeemScript: p2sh.redeem.output,
158
- })
159
- }
160
-
161
- // desktop / BE / mobile with bip-schnorr signing
162
- const isTaprootAddress = purpose === 86
163
- const signingKey = isTaprootAddress ? tweakSigner({ signer: key, ECPair, network }) : key
164
- await psbt.signInputAsync(index, toAsyncSigner({ keyPair: signingKey }), sigHashTypes)
165
- }
166
-
167
- // If a dapp authored the TX, it expects a serialized PSBT response.
168
- // Note: we wouldn't be able to finalise inputs in some cases that's why we serialize before finalizing inputs.
169
- if (isPsbtBufferPassed) {
170
- const rawPSBT = psbt.toBuffer()
171
-
172
- return { plainTx: { rawPSBT } }
173
- }
174
- // Serialize tx
175
- psbt.finalizeAllInputs()
176
- const tx = psbt.extractTransaction()
177
- const rawTx = tx.toBuffer()
178
- const txId = tx.getId()
38
+ await signWithWallet(psbt, inputsToSign)
179
39
 
180
- // tx needs to be serializable for desktop RPC send => sign communication
181
- return { rawTx, txId, tx: serializeTx({ tx }) }
40
+ const skipFinalize = !!unsignedTx.txData.psbtBuffer
41
+ return extractTransaction({ psbt, skipFinalize })
182
42
  }
183
43
  }
@@ -0,0 +1,103 @@
1
+ import assert from 'minimalistic-assert'
2
+ import { Psbt, Transaction } from '@exodus/bitcoinjs-lib'
3
+
4
+ const _MAXIMUM_FEE_RATES = {
5
+ qtumignition: 25000,
6
+ ravencoin: 1000000,
7
+ }
8
+
9
+ /**
10
+ * Factory function that create the prepareForSigning function for a bitcoin-like asset.
11
+ * @param { assetName, resolvePurpose, coinInfo} dependencies
12
+ * @returns A prepareForSigning function that returns a PSBTv1 as buffer
13
+ */
14
+ export function createPrepareForSigning({ assetName, resolvePurpose, coinInfo }) {
15
+ assert(assetName, 'assetName is required')
16
+ assert(resolvePurpose, 'resolvePurpose is required')
17
+ assert(coinInfo, 'coinInfo is required')
18
+
19
+ return ({ unsignedTx }) => {
20
+ const isPsbtBufferPassed =
21
+ unsignedTx.txData.psbtBuffer &&
22
+ unsignedTx.txMeta.addressPathsMap &&
23
+ unsignedTx.txMeta.inputsToSign
24
+ if (isPsbtBufferPassed) {
25
+ // PSBT created externally (Web3, etc..)
26
+ return createPsbtFromBuffer({ psbtBuffer: unsignedTx.txData.psbtBuffer })
27
+ } else {
28
+ // Create PSBT based on internal Exodus data structure
29
+ const networkInfo = coinInfo.toBitcoinJS()
30
+ const psbt = createPsbtFromTxData({
31
+ ...unsignedTx.txData,
32
+ ...unsignedTx.txMeta,
33
+ resolvePurpose,
34
+ networkInfo,
35
+ })
36
+ if (!['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)) psbt.setVersion(1)
37
+
38
+ return psbt
39
+ }
40
+ }
41
+ }
42
+
43
+ // Creates a PSBT instance from the passed transaction buffer provided by 3rd parties (e.g. dApps).
44
+ function createPsbtFromBuffer({ psbtBuffer, ecc }) {
45
+ const psbt = Psbt.fromBuffer(psbtBuffer, { eccLib: ecc })
46
+
47
+ return psbt
48
+ }
49
+
50
+ // Creates a PSBT instance from the passed inputs, outputs etc. The wallet itself provides this data.
51
+ function createPsbtFromTxData({ inputs, outputs, rawTxs, networkInfo, resolvePurpose, assetName }) {
52
+ // use harcoded max fee rates for specific assets
53
+ // if undefined, will be set to default value by PSBT (2500)
54
+ const maximumFeeRate = _MAXIMUM_FEE_RATES[assetName]
55
+
56
+ const psbt = new Psbt({ maximumFeeRate, network: networkInfo })
57
+
58
+ // Fill tx
59
+ for (const { txId, vout, address, value, script, sequence } of inputs) {
60
+ // TODO: don't use the purpose as intermediate variable
61
+ // see internals of `resolvePurposes`, just use `isP2TR, isP2SH etc directly
62
+ const purpose = resolvePurpose(address)
63
+
64
+ const isSegwitAddress = purpose === 84
65
+ const isTaprootAddress = purpose === 86
66
+
67
+ const txIn = { hash: txId, index: vout, sequence }
68
+ if (isSegwitAddress || isTaprootAddress) {
69
+ // witness outputs only require the value and the script, not the full transaction
70
+ txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
71
+ } else {
72
+ const rawTx = (rawTxs || []).find((t) => t.txId === txId)
73
+ // non-witness outptus require the full transaction
74
+ assert(!!rawTx, 'Non-witness outputs require the full previous transaction.')
75
+ const rawTxBuffer = Buffer.from(rawTx.rawData, 'hex')
76
+ if (canParseTx(rawTxBuffer)) {
77
+ txIn.nonWitnessUtxo = rawTxBuffer
78
+ } else {
79
+ // temp fix for https://exodusio.slack.com/archives/CP202D90Q/p1671014704829939 until bitcoinjs could parse a mweb tx without failing
80
+ console.warn(`Setting psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true for asset ${assetName}`)
81
+ psbt.__CACHE.__UNSAFE_SIGN_NONSEGWIT = true
82
+ txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
83
+ }
84
+ }
85
+
86
+ psbt.addInput(txIn)
87
+ }
88
+
89
+ for (const [address, amount] of outputs) {
90
+ psbt.addOutput({ value: amount, address })
91
+ }
92
+
93
+ return psbt
94
+ }
95
+
96
+ const canParseTx = (rawTxBuffer) => {
97
+ try {
98
+ Transaction.fromBuffer(rawTxBuffer)
99
+ return true
100
+ } catch (e) {
101
+ return false
102
+ }
103
+ }
@@ -0,0 +1,98 @@
1
+ import assert from 'minimalistic-assert'
2
+
3
+ import { createPrepareForSigning } from './default-prepare-for-signing'
4
+ import { extractTransaction } from './common'
5
+
6
+ export const signHardwareFactory = ({ assetName, resolvePurpose, keys, coinInfo }) => {
7
+ assert(assetName, 'assetName is required')
8
+ assert(resolvePurpose, 'resolvePurpose is required')
9
+ assert(keys, 'keys is required')
10
+ assert(coinInfo, 'coinInfo is required')
11
+
12
+ const prepareForSigning = createPrepareForSigning({
13
+ assetName,
14
+ resolvePurpose,
15
+ coinInfo,
16
+ })
17
+
18
+ const signWithHardwareWallet = createSignWithHardwareWallet({
19
+ assetName,
20
+ resolvePurpose,
21
+ })
22
+
23
+ return async ({ unsignedTx, hardwareDevice, accountIndex }) => {
24
+ assert(unsignedTx, 'unsignedTx is required')
25
+ assert(hardwareDevice, 'hardwareDevice is required')
26
+ assert(Number.isInteger(accountIndex), 'accountIndex must be integer')
27
+
28
+ const { addressPathsMap } = unsignedTx.txMeta
29
+
30
+ const psbt = prepareForSigning({ unsignedTx })
31
+
32
+ const inputsToSign = unsignedTx.txMeta.inputsToSign || unsignedTx.txData.inputs
33
+
34
+ await signWithHardwareWallet({
35
+ psbt,
36
+ inputsToSign,
37
+ addressPathsMap,
38
+ hardwareDevice,
39
+ accountIndex,
40
+ })
41
+
42
+ const skipFinalize = !!unsignedTx.txData.psbtBuffer
43
+ return extractTransaction({ psbt, skipFinalize })
44
+ }
45
+ }
46
+
47
+ function createSignWithHardwareWallet({ assetName, resolvePurpose }) {
48
+ return async ({ psbt, inputsToSign, addressPathsMap, accountIndex, hardwareDevice }) => {
49
+ const derivationPaths = getDerivationPaths({ resolvePurpose, addressPathsMap, accountIndex })
50
+ const signatures = await hardwareDevice.signTransaction({
51
+ assetName,
52
+ signableTransaction: psbt.toBuffer(),
53
+ derivationPaths,
54
+ })
55
+
56
+ applySignatures(psbt, signatures, inputsToSign)
57
+ }
58
+ }
59
+
60
+ function getDerivationPaths({ resolvePurpose, accountIndex, addressPathsMap }) {
61
+ let derivationPaths = []
62
+ for (const [address, path] of Object.entries(addressPathsMap)) {
63
+ const purpose = resolvePurpose(address)
64
+ const derivationPath = `m/${purpose}'/0'/${accountIndex}'/${path.slice(2)}` // TODO: coinindex
65
+ derivationPaths.push(derivationPath)
66
+ }
67
+ return derivationPaths
68
+ }
69
+
70
+ export function applySignatures(psbt, signatures, inputsToSign) {
71
+ for (let inputIndex = 0; inputIndex < psbt.inputCount; inputIndex++) {
72
+ const shouldSign = !!inputsToSign[inputIndex]
73
+ if (shouldSign) {
74
+ const signature = signatures.find((signature) => signature.inputIndex === inputIndex)
75
+ if (signature) {
76
+ const isTaprootSig = signature.publicKey.length === 32
77
+ if (isTaprootSig) {
78
+ psbt.data.updateInput(inputIndex, {
79
+ tapKeySig: signature.signature,
80
+ })
81
+ } else {
82
+ psbt.data.updateInput(inputIndex, {
83
+ partialSig: [
84
+ {
85
+ pubkey: signature.publicKey,
86
+ signature: signature.signature,
87
+ },
88
+ ],
89
+ })
90
+ }
91
+ } else {
92
+ throw new Error(
93
+ `expected to sign for inputIndex ${inputIndex} but no signature was produced`
94
+ )
95
+ }
96
+ }
97
+ }
98
+ }
@@ -1 +1,3 @@
1
- export { signTxFactory, serializeTx } from './default-create-tx'
1
+ export { signTxFactory } from './default-create-tx'
2
+ export { serializeTx } from './common'
3
+ export { signHardwareFactory } from './default-sign-hardware'
@@ -3,12 +3,14 @@ import assert from 'minimalistic-assert'
3
3
  import { getSchnorrEntropy } from './default-entropy'
4
4
  import { toXOnly } from '../bitcoinjs-lib/ecc-utils'
5
5
  import { eccFactory } from '../bitcoinjs-lib/ecc'
6
+ import { getECPair } from '../bitcoinjs-lib'
6
7
 
7
8
  const ecc = eccFactory()
9
+ const ECPair = getECPair()
8
10
 
9
- export function tweakSigner({ signer, ECPair, tweakHash, network }) {
11
+ export function tweakSigner({ signer, tweakHash, network }) {
10
12
  assert(signer, 'signer is required')
11
- assert(ECPair, 'ECPair is required')
13
+
12
14
  let privateKey = signer.privateKey
13
15
  if (!privateKey) {
14
16
  throw new Error('Private key is required for tweaking signer!')
@@ -5,6 +5,46 @@ import assert from 'minimalistic-assert'
5
5
 
6
6
  const MAX_ORDINAL_VALUE_POSTAGE = 10000
7
7
 
8
+ export function getTransferOrdinalsUtxos({ inscriptionIds, ordinalsUtxos }) {
9
+ const transferOrdinalsUtxos = ordinalsUtxos.filter((utxo) =>
10
+ utxo.inscriptions?.some((i) => inscriptionIds.includes(i.inscriptionId))
11
+ )
12
+ const unsafeInscriptions = transferOrdinalsUtxos.toArray().flatMap(
13
+ (utxo) =>
14
+ utxo.inscriptions?.filter((i) => {
15
+ const validInscription = isValidInscription({
16
+ value: utxo.value.toBaseNumber(),
17
+ offset: i.offset,
18
+ })
19
+ return validInscription && !inscriptionIds.includes(i.inscriptionId)
20
+ }) || []
21
+ )
22
+ assert(
23
+ !unsafeInscriptions.length,
24
+ `The following inscriptions are unsafe ${unsafeInscriptions.map(
25
+ (i) => i.inscriptionId
26
+ )} when ${inscriptionIds} should be spent`
27
+ )
28
+
29
+ const transferInscriptionIds = transferOrdinalsUtxos
30
+ .toArray()
31
+ .flatMap((utxo) => utxo.inscriptions)
32
+ .filter(({ inscriptionId }) => inscriptionIds.includes(inscriptionId))
33
+
34
+ assert(
35
+ transferInscriptionIds.length === inscriptionIds.length,
36
+ `Expected inscriptions ${inscriptionIds.length}. Found: ${transferInscriptionIds.length}`
37
+ )
38
+ return transferOrdinalsUtxos
39
+ }
40
+
41
+ export function isValidInscription({ value, offset }) {
42
+ assert(typeof value === 'number', 'value must be a number')
43
+ assert(typeof offset === 'number', 'offset must be a number')
44
+ // value >= 0 in case offset, alternatively convert to string/ln
45
+ return (value >= 0 && value <= MAX_ORDINAL_VALUE_POSTAGE) || offset === 0
46
+ }
47
+
8
48
  export function getUtxos({ accountState, asset }) {
9
49
  return (
10
50
  accountState?.utxos ||
@@ -23,31 +63,41 @@ export function getOrdinalsUtxos({ accountState, asset }) {
23
63
  )
24
64
  }
25
65
 
26
- function isOrdinalUtxo({ utxo, ordinalAddress }) {
27
- if (utxo.inscriptionId) {
28
- return true
29
- }
30
- if (!ordinalAddress) {
31
- // exclude utxos splitting
66
+ export function getValidInscriptions({ utxo }) {
67
+ return (utxo.inscriptions || []).filter((i) =>
68
+ isValidInscription({ value: utxo.value.toBaseNumber(), offset: i.offset })
69
+ )
70
+ }
71
+
72
+ function isOrdinalUtxo({ utxo, ordinalsEnabled, knownBalanceUtxoIds }) {
73
+ if (!ordinalsEnabled) {
32
74
  return false
33
75
  }
34
76
 
35
- if (utxo.address.toString() === ordinalAddress.toString()) {
36
- return true // we assume any utxo to the ordinal address is a ordinal utxos just in case
77
+ const utxoId = `${utxo.txId}:${utxo.vout}`.toLowerCase()
78
+
79
+ if (knownBalanceUtxoIds?.includes(utxoId) && !utxo.inscriptionsIndexed) {
80
+ return false // this allows users see and spend change balance after sending before hiro confirmation
37
81
  }
38
82
 
39
- if (utxo.confirmations) {
40
- return false
83
+ if (!utxo.inscriptionsIndexed) {
84
+ return true
41
85
  }
42
86
 
43
- return utxo.value.toBaseNumber() <= MAX_ORDINAL_VALUE_POSTAGE // while unconfirmed, put < 10000- sats in the ordinal utxos box just in case
87
+ const validInscriptions = getValidInscriptions({ utxo })
88
+ return validInscriptions.length > 0
44
89
  }
45
90
 
46
- export function partitionUtxos({ allUtxos, ordinalAddress }) {
91
+ export function partitionUtxos({ allUtxos, ordinalsEnabled, knownBalanceUtxoIds }) {
47
92
  assert(allUtxos, 'allUtxos is required')
93
+ // assert(ordinalAddress, 'ordinalAddress is required') // not used atm we may need to tune by ordinalAddress when unconfirmed or rubbish inscriptions
48
94
  return {
49
- utxos: allUtxos.filter((utxo) => !isOrdinalUtxo({ utxo, ordinalAddress })),
50
- ordinalsUtxos: allUtxos.filter((utxo) => isOrdinalUtxo({ utxo, ordinalAddress })),
95
+ utxos: allUtxos.filter(
96
+ (utxo) => !isOrdinalUtxo({ utxo, ordinalsEnabled, knownBalanceUtxoIds })
97
+ ),
98
+ ordinalsUtxos: allUtxos.filter((utxo) =>
99
+ isOrdinalUtxo({ utxo, ordinalsEnabled, knownBalanceUtxoIds })
100
+ ),
51
101
  }
52
102
  }
53
103
 
@@ -68,12 +118,20 @@ export function getConfirmedOrRfbDisabledUtxos({ utxos, allowUnconfirmedRbfEnabl
68
118
  return utxos.filter((utxo) => utxo.confirmations > 0 || !utxo.rbfEnabled)
69
119
  }
70
120
 
121
+ function filterDustUtxos({ utxos, feeData }) {
122
+ if (feeData.utxoDustValue) {
123
+ return utxos.filter((utxo) => utxo.value.toBaseNumber() > feeData.utxoDustValue)
124
+ }
125
+ return utxos
126
+ }
127
+
71
128
  export function getUsableUtxos({ asset, utxos, feeData, txSet }) {
72
129
  assert(asset, 'asset is required')
73
130
  assert(utxos, 'utxos is required')
74
131
  assert(feeData, 'feeData is required')
75
132
  assert(txSet, 'txSet is required')
76
- if (!['bitcoin', 'bitcointestnet', 'bitcoinregtest'].includes(asset.name)) return utxos
133
+ if (!['bitcoin', 'bitcointestnet', 'bitcoinregtest'].includes(asset.name))
134
+ return filterDustUtxos({ utxos, feeData })
77
135
  const { fastestFee } = feeData
78
136
  const feeRate = fastestFee.toBaseNumber()
79
137
  const maxFee = feeData.maxExtraCpfpFee
@@ -85,7 +143,9 @@ export function getUsableUtxos({ asset, utxos, feeData, txSet }) {
85
143
  feeRate,
86
144
  maxFee,
87
145
  })
88
- return largeUnconfirmedTxs.size === 0
89
- ? utxos
90
- : utxos.filter((utxo) => !largeUnconfirmedTxs.has(utxo.txId))
146
+ const confirmedAndSmallUtxos =
147
+ largeUnconfirmedTxs.size === 0
148
+ ? utxos
149
+ : utxos.filter((utxo) => !largeUnconfirmedTxs.has(utxo.txId))
150
+ return filterDustUtxos({ utxos: confirmedAndSmallUtxos, feeData })
91
151
  }