@exodus/bitcoin-api 2.6.6 → 2.6.8-hiro.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.6.6",
3
+ "version": "2.6.8-hiro.0",
4
4
  "description": "Exodus bitcoin-api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -42,6 +42,5 @@
42
42
  "@scure/base": "^1.1.3",
43
43
  "@scure/btc-signer": "^1.1.0",
44
44
  "jest-when": "^3.5.1"
45
- },
46
- "gitHead": "b2ed2bffe712facdd412df1a0ebc6a8ac92c4d1c"
45
+ }
47
46
  }
@@ -1,6 +1,6 @@
1
1
  import assert from 'minimalistic-assert'
2
2
 
3
- export function getOrdinalAddress({
3
+ export async function getOrdinalAddress({
4
4
  asset,
5
5
  assetClientInterface,
6
6
  walletAccount,
@@ -12,10 +12,22 @@ export function getOrdinalAddress({
12
12
  if (ordinalChainIndex === undefined) {
13
13
  return undefined
14
14
  }
15
+
16
+ const purposes = await assetClientInterface.getSupportedPurposes({
17
+ assetName: asset.name,
18
+ walletAccount,
19
+ })
20
+
21
+ const purpose = 86
22
+
23
+ if (!purposes.includes(purpose)) {
24
+ return undefined
25
+ }
26
+
15
27
  return assetClientInterface.getAddress({
16
28
  assetName: asset.name,
17
29
  walletAccount,
18
- purpose: 86,
30
+ purpose,
19
31
  chainIndex: ordinalChainIndex,
20
32
  addressIndex: 0,
21
33
  })
@@ -67,7 +67,6 @@ export class BitcoinMonitorScanner {
67
67
 
68
68
  const storedUtxos = getUtxos({ asset, accountState })
69
69
  const storedOrdinalUtxos = getOrdinalsUtxos({ asset, accountState })
70
- const ordinalAddress = await this.getOrdinalAddress({ walletAccount })
71
70
 
72
71
  const currentStoredUtxos = storedUtxos.union(storedOrdinalUtxos)
73
72
 
@@ -90,7 +89,9 @@ export class BitcoinMonitorScanner {
90
89
  : (txs) =>
91
90
  txs.filter((tx) => {
92
91
  const txItem = currentTxs.get(tx.txid)
93
- return txItem && txItem.confirmed
92
+ const confirmed = txItem && txItem.confirmed
93
+ const inscriptionsIndexed = txItem?.data?.inscriptionsIndexed
94
+ return confirmed && (!this.#ordinalsEnabled || inscriptionsIndexed)
94
95
  }).length >= txs.length
95
96
 
96
97
  const unusedAddressIndexes = await assetClientInterface.getUnusedAddressIndexes({
@@ -344,6 +345,15 @@ export class BitcoinMonitorScanner {
344
345
  currencies: { [assetName]: currency },
345
346
  }
346
347
 
348
+ if (this.#ordinalsEnabled) {
349
+ txLogItem.data = {
350
+ ...txLogItem.data,
351
+ inscriptionsIndexed: txItem.inscriptionsIndexed,
352
+ sentInscriptions: [],
353
+ receivedInscriptions: [],
354
+ }
355
+ }
356
+
347
357
  let from = []
348
358
 
349
359
  // if txItem.vin has an address that matches ours, means we've spent this tx
@@ -361,8 +371,13 @@ export class BitcoinMonitorScanner {
361
371
  txLogItem.coinAmount = txLogItem.coinAmount.sub(currency.defaultUnit(vin.value))
362
372
  isSent = true
363
373
  txLogItem.data.sent = []
364
- if (ordinalAddress && address.toString() === ordinalAddress.toString()) {
365
- txLogItem.data.isInscriptionSent = true
374
+ if (this.#ordinalsEnabled && vin.inscriptions) {
375
+ txLogItem.data.sentInscriptions.push(
376
+ ...vin.inscriptions.map((i) => ({
377
+ ...i,
378
+ value: currency.defaultUnit(vin.value).toBaseNumber(),
379
+ }))
380
+ )
366
381
  }
367
382
 
368
383
  // this is only used to exclude the utxos in the reducer which is why we don't care about the other fields
@@ -432,8 +447,13 @@ export class BitcoinMonitorScanner {
432
447
  txLogItem.data.changeAddress = address
433
448
  }
434
449
 
435
- if (ordinalAddress && address.toString() === ordinalAddress.toString()) {
436
- txLogItem.data.isInscriptionReceived = true
450
+ if (this.#ordinalsEnabled && vout.inscriptions) {
451
+ txLogItem.data.receivedInscriptions.push(
452
+ ...vout.inscriptions.map((i) => ({
453
+ ...i,
454
+ value: currency.defaultUnit(vout.value).toBaseNumber(),
455
+ }))
456
+ )
437
457
  }
438
458
 
439
459
  // it was sent to us...
@@ -450,6 +470,11 @@ export class BitcoinMonitorScanner {
450
470
  rbfEnabled: txItem.rbf,
451
471
  }
452
472
 
473
+ if (this.#ordinalsEnabled) {
474
+ output.inscriptionsIndexed = txItem.inscriptionsIndexed
475
+ output.inscriptions = vout.inscriptions || []
476
+ }
477
+
453
478
  if (this.#shouldExcludeVoutUtxo({ asset, output, txItem, vout })) {
454
479
  return
455
480
  }
@@ -508,6 +533,16 @@ export class BitcoinMonitorScanner {
508
533
  // Keep new utxos when they intersect with the stored utxos.
509
534
  utxoCol = utxoCol.union(currentStoredUtxos).difference(utxosToRemoveCol)
510
535
 
536
+ if (this.#ordinalsEnabled) {
537
+ // copy ordinalsChange over
538
+ utxoCol.toArray().forEach((utxo) => {
539
+ const storedUtxo = currentStoredUtxos
540
+ .toArray()
541
+ .find((u2) => u2.txid === utxos.txid && u2.vout === utxo.vout)
542
+ utxo.ordinalsChange = storedUtxo?.ordinalsChange
543
+ })
544
+ }
545
+
511
546
  for (let tx of Object.values(unconfirmedTxsToCheck)) {
512
547
  existingTxs.push({ ...tx, dropped: true }) // TODO: this will decrease the chain index, it shouldn't be an issue considering the gap limit
513
548
  utxoCol = utxoCol.difference(utxoCol.getTxIdUtxos(tx.txId))
@@ -540,9 +575,11 @@ export class BitcoinMonitorScanner {
540
575
  return !isEqual(chain, originalChain.chain)
541
576
  })
542
577
 
578
+ const ordinalAddress = await this.getOrdinalAddress({ walletAccount })
543
579
  const utxosData = utxoCol
544
580
  ? partitionUtxos({
545
581
  allUtxos: utxoCol,
582
+ ordinalsEnabled: this.#ordinalsEnabled,
546
583
  ordinalAddress,
547
584
  })
548
585
  : {}
@@ -603,6 +640,7 @@ export class BitcoinMonitorScanner {
603
640
 
604
641
  const { utxos, ordinalsUtxos } = partitionUtxos({
605
642
  allUtxos: txConfirmedUtxos,
643
+ ordinalsEnabled: this.#ordinalsEnabled,
606
644
  ordinalAddress: await this.getOrdinalAddress({ walletAccount }),
607
645
  })
608
646
 
@@ -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 })
@@ -293,6 +287,7 @@ export const createAndBroadcastTXFactory = ({
293
287
  UtxoCollection.fromJSON(replaceTx.data.inputs, { currency: asset.currency })
294
288
  )
295
289
  }
290
+ const addressPathsMap = selectedUtxos.getAddressPathsMap()
296
291
 
297
292
  // transform UTXO object to raw
298
293
  const inputs = shuffle(createInputs(assetName, selectedUtxos.toArray(), rbfEnabled))
@@ -341,6 +336,9 @@ export const createAndBroadcastTXFactory = ({
341
336
  ourAddress.address ? ourAddress.address : ourAddress.toString(),
342
337
  change
343
338
  )
339
+ // Add the keypath of change address to support Trezor detect the change output.
340
+ // Output is change and does not need approval from user which shows the strange address that user never seen.
341
+ addressPathsMap[changeAddress] = ourAddress.meta.path
344
342
  outputs.push(changeOutput)
345
343
  } else {
346
344
  // If we don't have enough for a change output, then all remaining dust is just added to fee
@@ -360,7 +358,7 @@ export const createAndBroadcastTXFactory = ({
360
358
  },
361
359
  txMeta: {
362
360
  useCashAddress, // for trezor to show the receiver cash address
363
- addressPathsMap: selectedUtxos.getAddressPathsMap(),
361
+ addressPathsMap,
364
362
  blockHeight,
365
363
  },
366
364
  }
@@ -441,6 +439,7 @@ export const createAndBroadcastTXFactory = ({
441
439
  value: change,
442
440
  confirmations: 0,
443
441
  rbfEnabled,
442
+ ordinalsChange: ordinalsEnabled ? true : undefined,
444
443
  }
445
444
  remainingUtxos = remainingUtxos.addUtxo(changeUtxo)
446
445
  }
@@ -518,7 +517,16 @@ export const createAndBroadcastTXFactory = ({
518
517
  inputs: selectedUtxos.toJSON(),
519
518
  replacedTxId: replaceTx ? replaceTx.txId : undefined,
520
519
  nftId: nft ? `${assetName}:${nft.tokenId}` : undefined, // it allows BE to load the nft info while the nft is in transit
521
- inscriptionIds: inscriptionIds,
520
+ inscriptionsIndexed: ordinalsEnabled ? true : undefined,
521
+ sentInscriptions: inscriptionIds
522
+ ? inscriptionIds.map((inscriptionId) => {
523
+ return {
524
+ inscriptionId,
525
+ offset: 0,
526
+ value: 0,
527
+ }
528
+ })
529
+ : undefined,
522
530
  },
523
531
  },
524
532
  ],
@@ -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,36 @@ 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 }) {
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
+ if (utxo.ordinalsChange && !utxo.inscriptionsIndexed) {
78
+ return false // this allows users see and spend change balance after sending before hiro confirmation
37
79
  }
38
80
 
39
- if (utxo.confirmations) {
40
- return false
81
+ if (!utxo.inscriptionsIndexed) {
82
+ return true
41
83
  }
42
84
 
43
- return utxo.value.toBaseNumber() <= MAX_ORDINAL_VALUE_POSTAGE // while unconfirmed, put < 10000- sats in the ordinal utxos box just in case
85
+ const validInscriptions = getValidInscriptions({ utxo })
86
+ return validInscriptions.length > 0
44
87
  }
45
88
 
46
- export function partitionUtxos({ allUtxos, ordinalAddress }) {
89
+ export function partitionUtxos({ allUtxos, ordinalsEnabled }) {
90
+ assert(allUtxos, 'allUtxos is required')
47
91
  assert(allUtxos, 'allUtxos is required')
92
+ // assert(ordinalAddress, 'ordinalAddress is required') // not used atm we may need to tune by ordinalAddress when unconfirmed or rubbish inscriptions
48
93
  return {
49
- utxos: allUtxos.filter((utxo) => !isOrdinalUtxo({ utxo, ordinalAddress })),
50
- ordinalsUtxos: allUtxos.filter((utxo) => isOrdinalUtxo({ utxo, ordinalAddress })),
94
+ utxos: allUtxos.filter((utxo) => !isOrdinalUtxo({ utxo, ordinalsEnabled })),
95
+ ordinalsUtxos: allUtxos.filter((utxo) => isOrdinalUtxo({ utxo, ordinalsEnabled })),
51
96
  }
52
97
  }
53
98