@exodus/bitcoin-api 2.3.16 → 2.4.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.3.16",
3
+ "version": "2.4.1",
4
4
  "description": "Exodus bitcoin-api",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -40,6 +40,5 @@
40
40
  "devDependencies": {
41
41
  "@exodus/bitcoin-meta": "^1.0.1",
42
42
  "jest-when": "^3.5.1"
43
- },
44
- "gitHead": "ea21eb60e35f84cf2086ae0304bf9fa8ce4058b1"
43
+ }
45
44
  }
@@ -1,11 +1,22 @@
1
1
  import { AccountState, UtxoCollection } from '@exodus/models'
2
2
 
3
- export function createAccountState({ asset }) {
3
+ export function createAccountState({ asset, ordinalsEnabled = false, brc20Enabled = false }) {
4
+ const empty = UtxoCollection.createEmpty({
5
+ currency: asset.currency,
6
+ })
7
+ const defaults = {
8
+ utxos: empty,
9
+ }
10
+
11
+ if (ordinalsEnabled) {
12
+ defaults.ordinalsUtxos = empty
13
+ }
14
+
15
+ if (brc20Enabled) {
16
+ defaults.brc20Balances = {}
17
+ }
18
+
4
19
  return class BitcoinAccountState extends AccountState {
5
- static defaults = {
6
- utxos: UtxoCollection.createEmpty({
7
- currency: asset.currency,
8
- }),
9
- }
20
+ static defaults = defaults
10
21
  }
11
22
  }
@@ -0,0 +1,24 @@
1
+ import assert from 'minimalistic-assert'
2
+
3
+ export function isOrdinalAddress(address, ordinalChainIndex) {
4
+ assert(typeof ordinalChainIndex === 'number', `ordinalChainIndex must be a number`)
5
+ return parsePath(address)[0] === ordinalChainIndex
6
+ }
7
+
8
+ export function isReceiveAddress(address): boolean {
9
+ return parsePath(address)[0] === 0
10
+ }
11
+
12
+ export function isChangeAddress(address): boolean {
13
+ return parsePath(address)[0] === 1
14
+ }
15
+
16
+ function parsePath(address) {
17
+ assert(
18
+ address.meta.path,
19
+ `address parameter ${address} does not have a meta.path. Is it a valid Address object?`
20
+ )
21
+ const path = address.meta.path
22
+ const p1 = path ? path.replace('m/', '').split('/') : ['0', '0']
23
+ return p1.map((i) => parseInt(i, 10))
24
+ }
@@ -14,15 +14,39 @@ export class GetFeeResolver {
14
14
  this.#allowUnconfirmedRbfEnabledUtxos = allowUnconfirmedRbfEnabledUtxos
15
15
  }
16
16
 
17
- getFee = ({ asset, accountState, txSet, feeData, amount, customFee, isSendAll }) => {
17
+ getFee = ({
18
+ asset,
19
+ accountState,
20
+ txSet,
21
+ feeData,
22
+ amount,
23
+ customFee,
24
+ isSendAll,
25
+ nft, // sending one nft
26
+ brc20, // sending multiple inscriptions ids (as in sending multiple transfer ordinals)
27
+ }) => {
28
+ if (nft) {
29
+ assert(!amount, 'amount must not be provided when nft is provided!!!')
30
+ assert(!isSendAll, 'isSendAll must not be provided when nft is provided!!!')
31
+ assert(!brc20, 'brc20 must not be provided when nft is provided!!!')
32
+ }
33
+ if (brc20) {
34
+ // assert(!amount, 'amount must not be provided when brc20 is provided!!!')
35
+ assert(!isSendAll, 'isSendAll must not be provided when brc20 is provided!!!')
36
+ assert(!nft, 'nft must not be provided when brc20 is provided!!!')
37
+ }
38
+
39
+ const inscriptionIds = nft?.tokenId ? [nft?.tokenId] : brc20?.inscriptionIds
40
+
18
41
  const { resolvedFee, extraFee } = this.#getUtxosData({
19
42
  asset,
20
43
  accountState,
21
44
  txSet,
22
45
  feeData,
23
- amount,
46
+ amount: brc20 ? undefined : amount,
24
47
  customFee,
25
48
  isSendAll,
49
+ inscriptionIds,
26
50
  })
27
51
  return { fee: resolvedFee, extraFee }
28
52
  }
@@ -49,7 +73,16 @@ export class GetFeeResolver {
49
73
  }).spendableBalance
50
74
  }
51
75
 
52
- #getUtxosData = ({ asset, accountState, txSet, feeData, amount, customFee, isSendAll }) => {
76
+ #getUtxosData = ({
77
+ asset,
78
+ accountState,
79
+ txSet,
80
+ feeData,
81
+ amount,
82
+ customFee,
83
+ isSendAll,
84
+ inscriptionIds,
85
+ }) => {
53
86
  assert(asset, 'asset must be provided')
54
87
  assert(feeData, 'feeData must be provided')
55
88
  assert(accountState, 'accountState must be provided')
@@ -65,10 +98,6 @@ export class GetFeeResolver {
65
98
  })
66
99
  const replaceableTxs = findUnconfirmedSentRbfTxs(txSet)
67
100
 
68
- const receiveAddress = ['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(asset.name)
69
- ? 'P2WSH'
70
- : 'P2PKH'
71
-
72
101
  const feePerKB = customFee || feeData.feePerKB
73
102
  return getUtxosData({
74
103
  asset,
@@ -76,7 +105,7 @@ export class GetFeeResolver {
76
105
  replaceableTxs,
77
106
  amount,
78
107
  feeRate: feePerKB,
79
- receiveAddress,
108
+ inscriptionIds,
80
109
  isSendAll: isSendAll,
81
110
  getFeeEstimator: this.#getFeeEstimator,
82
111
  allowUnconfirmedRbfEnabledUtxos: this.#allowUnconfirmedRbfEnabledUtxos,
@@ -91,7 +120,6 @@ export class GetFeeResolver {
91
120
  accountState,
92
121
  feeData,
93
122
  getFeeEstimator: this.#getFeeEstimator,
94
-
95
123
  allowUnconfirmedRbfEnabledUtxos: this.#allowUnconfirmedRbfEnabledUtxos,
96
124
  })
97
125
  }
@@ -6,19 +6,44 @@ import { getConfirmedUtxos, getConfirmedOrRfbDisabledUtxos } from '../utxos-util
6
6
 
7
7
  const MIN_RELAY_FEE = 1000
8
8
 
9
+ const getBestReceiveAddresses = ({ asset, receiveAddress, inscriptionIds }) => {
10
+ if (inscriptionIds) {
11
+ return receiveAddress || 'P2TR'
12
+ }
13
+ if (receiveAddress === null) {
14
+ return null
15
+ }
16
+
17
+ return ['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(asset.name) ? 'P2WSH' : 'P2PKH'
18
+ }
19
+
9
20
  export const selectUtxos = ({
10
21
  asset,
11
22
  usableUtxos,
12
23
  replaceableTxs,
13
24
  amount,
14
25
  feeRate,
15
- receiveAddress = 'P2WSH',
26
+ receiveAddress, // it could be null
16
27
  isSendAll,
17
28
  getFeeEstimator,
18
29
  disableReplacement = false,
19
30
  mustSpendUtxos,
20
31
  allowUnconfirmedRbfEnabledUtxos,
32
+ inscriptionIds, // for each inscription transfer, we need to calculate one more input and one more output
21
33
  }) => {
34
+ const resolvedReceiveAddresses = getBestReceiveAddresses({
35
+ asset,
36
+ receiveAddress,
37
+ inscriptionIds,
38
+ })
39
+
40
+ const receiveAddresses = []
41
+ if (inscriptionIds) {
42
+ receiveAddresses.push(...inscriptionIds.map(() => resolvedReceiveAddresses))
43
+ } else {
44
+ receiveAddresses.push(resolvedReceiveAddresses)
45
+ }
46
+
22
47
  assert(asset, 'asset is required')
23
48
  assert(usableUtxos, 'usableUtxos is required')
24
49
  assert(getFeeEstimator, 'getFeeEstimator is required')
@@ -34,6 +59,7 @@ export const selectUtxos = ({
34
59
  // We can only replace for a sendAll if only 1 replaceable tx and no unconfirmed utxos
35
60
  const confirmedUtxosArray = getConfirmedUtxos({ asset, utxos: usableUtxos }).toArray()
36
61
  const canReplace =
62
+ !inscriptionIds &&
37
63
  !mustSpendUtxos &&
38
64
  !disableReplacement &&
39
65
  replaceableTxs &&
@@ -53,6 +79,7 @@ export const selectUtxos = ({
53
79
  }
54
80
 
55
81
  const replaceFeeEstimator = getFeeEstimator(asset, { feePerKB })
82
+ // how to avoid replace tx inputs when inputs are ordinals? !!!!
56
83
  const inputs = UtxoCollection.fromJSON(tx.data.inputs, { currency })
57
84
  const outputs = isSendAll
58
85
  ? tx.data.sent.map(({ address }) => address)
@@ -60,9 +87,7 @@ export const selectUtxos = ({
60
87
  ...tx.data.sent.map(({ address }) => address),
61
88
  tx.data.changeAddress?.address || changeAddressType,
62
89
  ]
63
- if (receiveAddress) {
64
- outputs.push(receiveAddress)
65
- }
90
+ if (receiveAddresses.find(Boolean)) outputs.push(...receiveAddresses)
66
91
 
67
92
  let fee
68
93
  let additionalUtxos
@@ -78,9 +103,11 @@ export const selectUtxos = ({
78
103
  if (confirmedUtxosArray.length === 0) {
79
104
  // Try estimating fee with no change
80
105
  if (replaceTxAmount.add(additionalUtxos.value).lt(amount.add(fee))) {
81
- const noChangeOutputs = receiveAddress
82
- ? [...tx.data.sent.map(({ address }) => address), receiveAddress]
83
- : tx.data.sent.map(({ address }) => address)
106
+ const noChangeOutputs = [
107
+ ...tx.data.sent.map(({ address }) => address),
108
+ ...receiveAddresses,
109
+ ]
110
+
84
111
  fee = replaceFeeEstimator({
85
112
  inputs: inputs.union(additionalUtxos),
86
113
  outputs: noChangeOutputs,
@@ -95,7 +122,7 @@ export const selectUtxos = ({
95
122
  }
96
123
  }
97
124
  if (isSendAll || replaceTxAmount.add(additionalUtxos.value).gte(amount.add(fee))) {
98
- const chainOutputs = isSendAll ? [receiveAddress] : [receiveAddress, changeAddressType]
125
+ const chainOutputs = isSendAll ? receiveAddresses : [...receiveAddresses, changeAddressType]
99
126
  const chainFee = feeEstimator({
100
127
  inputs: changeUtxos.union(additionalUtxos),
101
128
  outputs: chainOutputs,
@@ -132,7 +159,7 @@ export const selectUtxos = ({
132
159
 
133
160
  if (isSendAll) {
134
161
  const selectedUtxos = UtxoCollection.fromArray(utxosArray, { currency })
135
- const fee = feeEstimator({ inputs: selectedUtxos, outputs: [receiveAddress] })
162
+ const fee = feeEstimator({ inputs: selectedUtxos, outputs: receiveAddresses })
136
163
  if (selectedUtxos.value.lt(amount.add(fee))) {
137
164
  return { fee }
138
165
  }
@@ -159,7 +186,10 @@ export const selectUtxos = ({
159
186
  let selectedUtxos = UtxoCollection.fromArray(selectedUtxosArray, { currency })
160
187
 
161
188
  // start figuring out fees
162
- const outputs = amount.isZero ? [changeAddressType] : [receiveAddress, changeAddressType]
189
+ const outputs =
190
+ amount.isZero && !inscriptionIds
191
+ ? [changeAddressType]
192
+ : [...receiveAddresses, changeAddressType]
163
193
 
164
194
  let fee = feeEstimator({ inputs: selectedUtxos, outputs })
165
195
 
@@ -167,7 +197,7 @@ export const selectUtxos = ({
167
197
  // We ran out of UTXOs, give up now
168
198
  if (remainingUtxosArray.length === 0) {
169
199
  // Try fee with no change
170
- fee = feeEstimator({ inputs: selectedUtxos, outputs: [receiveAddress] })
200
+ fee = feeEstimator({ inputs: selectedUtxos, outputs: receiveAddresses })
171
201
  break
172
202
  }
173
203
 
@@ -193,6 +223,7 @@ export const getUtxosData = ({
193
223
  disableReplacement,
194
224
  mustSpendUtxos,
195
225
  allowUnconfirmedRbfEnabledUtxos,
226
+ inscriptionIds,
196
227
  }) => {
197
228
  const { selectedUtxos, replaceTx, fee } = selectUtxos({
198
229
  asset,
@@ -206,6 +237,7 @@ export const getUtxosData = ({
206
237
  disableReplacement,
207
238
  mustSpendUtxos,
208
239
  allowUnconfirmedRbfEnabledUtxos,
240
+ inscriptionIds,
209
241
  })
210
242
 
211
243
  const resolvedFee = replaceTx ? fee.sub(replaceTx.feeAmount) : fee
@@ -3,26 +3,13 @@ import { orderTxs } from '../insight-api-client/util'
3
3
  import { Address, UtxoCollection } from '@exodus/models'
4
4
  import { isEqual, compact, uniq } from 'lodash'
5
5
  import ms from 'ms'
6
-
7
6
  import assert from 'minimalistic-assert'
8
- import { getUtxos } from '../utxos-utils'
7
+ import { isChangeAddress, isReceiveAddress } from '../address-utils'
8
+ import { getOrdinalsUtxos, getUtxos, partitionUtxos } from '../utxos-utils'
9
9
 
10
10
  // Time to check whether to drop a sent tx
11
11
  const SENT_TIME_TO_DROP = ms('2m')
12
12
 
13
- function isReceiveAddress(addr: Address): boolean {
14
- return parsePath(addr.meta.path)[0] === 0
15
- }
16
- function isChangeAddress(addr: Address): boolean {
17
- return parsePath(addr.meta.path)[0] === 1
18
- }
19
-
20
- function parsePath(path) {
21
- let p1 = path ? path.replace('m/', '').split('/') : ['0', '0']
22
- p1 = p1.map((i) => parseInt(i, 10))
23
- return p1
24
- }
25
-
26
13
  export class BitcoinMonitorScanner {
27
14
  #asset
28
15
  #insightClient
@@ -30,6 +17,8 @@ export class BitcoinMonitorScanner {
30
17
  #txFetchLimitResolver
31
18
  #shouldExcludeVoutUtxo
32
19
  #yieldToUI
20
+ #ordinalsEnabled
21
+ #ordinalChainIndex
33
22
 
34
23
  constructor({
35
24
  asset,
@@ -38,6 +27,8 @@ export class BitcoinMonitorScanner {
38
27
  yieldToUI = () => {},
39
28
  shouldExcludeVoutUtxo = () => false,
40
29
  txFetchLimitResolver = ({ refresh }) => (refresh ? 50 : 10),
30
+ ordinalsEnabled,
31
+ ordinalChainIndex,
41
32
  }) {
42
33
  assert(asset, 'asset is required!')
43
34
  assert(assetClientInterface, 'assetClientInterface is required!')
@@ -51,6 +42,8 @@ export class BitcoinMonitorScanner {
51
42
  this.#assetClientInterface = assetClientInterface
52
43
  this.#txFetchLimitResolver = txFetchLimitResolver
53
44
  this.#shouldExcludeVoutUtxo = shouldExcludeVoutUtxo
45
+ this.#ordinalsEnabled = ordinalsEnabled
46
+ this.#ordinalChainIndex = ordinalChainIndex
54
47
  }
55
48
 
56
49
  async rescanBlockchainInsight({ walletAccount, refresh }) {
@@ -64,7 +57,11 @@ export class BitcoinMonitorScanner {
64
57
  const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
65
58
  const currency = asset.currency
66
59
  const currentTxs = await assetClientInterface.getTxLog({ assetName, walletAccount })
67
- const currentUtxos = getUtxos({ asset, accountState })
60
+
61
+ const storedUtxos = getUtxos({ asset, accountState })
62
+ const storedOrdinalUtxos = getOrdinalsUtxos({ asset, accountState })
63
+
64
+ const currentStoredUtxos = storedUtxos.union(storedOrdinalUtxos)
68
65
 
69
66
  const currentTime = new Date().getTime()
70
67
  const unconfirmedTxsToCheck = Array.from(currentTxs).reduce((txs, tx) => {
@@ -221,6 +218,21 @@ export class BitcoinMonitorScanner {
221
218
  })
222
219
  .flat()
223
220
 
221
+ if (
222
+ fetchCount === 0 &&
223
+ this.#ordinalsEnabled &&
224
+ this.#ordinalChainIndex > 1 &&
225
+ purposes.includes(86)
226
+ ) {
227
+ // this is the ordinal address
228
+ chainObjects.push({
229
+ purpose: 86,
230
+ chainIndex: this.#ordinalChainIndex,
231
+ startAddressIndex: 0,
232
+ endAddressIndex: 1,
233
+ })
234
+ }
235
+
224
236
  const addresses = await aggregateAddresses(chainObjects)
225
237
 
226
238
  const txs = await fetchAllTxs(addresses)
@@ -243,6 +255,11 @@ export class BitcoinMonitorScanner {
243
255
  const metaAddressIndex = parseInt(pd[2])
244
256
  const addressString = String(address)
245
257
  const purposeToUpdate = purposeMap[addressString]
258
+
259
+ if (metaChainIndex === this.#ordinalChainIndex && this.#ordinalChainIndex > 1) {
260
+ return
261
+ }
262
+
246
263
  if (!purposeToUpdate) {
247
264
  console.warn(`${assetName}: Cannot resolve purpose from address ${addressString}`)
248
265
  return
@@ -466,8 +483,8 @@ export class BitcoinMonitorScanner {
466
483
  let utxoCol = UtxoCollection.fromArray(utxos, { currency })
467
484
 
468
485
  const utxosToRemoveCol = UtxoCollection.fromArray(utxosToRemove, { currency })
469
- // if something else can update currentUtxos in the meantime, we probably need to grab it from state again
470
- utxoCol = utxoCol.union(currentUtxos.difference(utxosToRemoveCol))
486
+ // if something else can update currentStoredUtxos in the meantime, we probably need to grab it from state again
487
+ utxoCol = currentStoredUtxos.union(utxoCol.difference(utxosToRemoveCol))
471
488
 
472
489
  for (let tx of Object.values(unconfirmedTxsToCheck)) {
473
490
  existingTxs.push({ ...tx, dropped: true }) // TODO: this will decrease the chain index, it shouldn't be an issue considering the gap limit
@@ -491,7 +508,7 @@ export class BitcoinMonitorScanner {
491
508
  }
492
509
 
493
510
  // no changes, ignore
494
- if (utxoCol.equals(currentUtxos)) {
511
+ if (utxoCol.equals(currentStoredUtxos)) {
495
512
  utxoCol = null
496
513
  }
497
514
 
@@ -501,10 +518,14 @@ export class BitcoinMonitorScanner {
501
518
  return !isEqual(chain, originalChain.chain)
502
519
  })
503
520
 
521
+ const utxosData = utxoCol
522
+ ? partitionUtxos({ allUtxos: utxoCol, ordinalChainIndex: this.#ordinalChainIndex })
523
+ : {}
524
+
504
525
  return {
505
526
  txsToUpdate: existingTxs,
506
527
  txsToAdd: newTxs,
507
- utxos: utxoCol,
528
+ ...utxosData,
508
529
  changedUnusedAddressIndexes,
509
530
  }
510
531
  }
@@ -517,6 +538,8 @@ export class BitcoinMonitorScanner {
517
538
  const accountState = await aci.getAccountState({ assetName, walletAccount })
518
539
 
519
540
  const storedUtxos = getUtxos({ accountState, asset })
541
+ const storedOrdinalsUtxos = getOrdinalsUtxos({ accountState, asset })
542
+ const allStoredUtxos = storedUtxos.union(storedOrdinalsUtxos)
520
543
 
521
544
  const currentTxs = Array.from(await aci.getTxLog({ assetName, walletAccount }))
522
545
 
@@ -551,13 +574,16 @@ export class BitcoinMonitorScanner {
551
574
  })
552
575
  .filter((tx) => Object.keys(tx).length > 1)
553
576
 
554
- // Note about storedUtxos.clone(): updateConfirmations mutates the ordinal utxos, it's not possible to perform the equals below if not cloned
555
- const updatedUtxos = confirmationsList.length
556
- ? storedUtxos.clone().updateConfirmations(confirmationsList)
557
- : null
577
+ const txConfirmedUtxos = allStoredUtxos.updateConfirmations(confirmationsList)
578
+
579
+ const { utxos, ordinalsUtxos } = partitionUtxos({
580
+ allUtxos: txConfirmedUtxos,
581
+ ordinalChainIndex: this.#ordinalChainIndex,
582
+ })
558
583
 
559
584
  return {
560
- utxos: !updatedUtxos || updatedUtxos.equals(storedUtxos) ? null : updatedUtxos,
585
+ utxos: utxos.equals(storedUtxos) ? null : utxos,
586
+ ordinalsUtxos: ordinalsUtxos.equals(storedOrdinalsUtxos) ? null : ordinalsUtxos,
561
587
  txsToUpdate: updatedPropertiesTxs,
562
588
  }
563
589
  }
@@ -1,5 +1,5 @@
1
1
  import assert from 'minimalistic-assert'
2
- import { isEmpty, isEqual } from 'lodash'
2
+ 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'
@@ -142,17 +142,21 @@ export class Monitor extends BaseMonitor {
142
142
  const assetName = asset.name
143
143
  const walletAccounts = await aci.getWalletAccounts({ assetName })
144
144
  for (const walletAccount of walletAccounts) {
145
- const { txsToUpdate, utxos } = await this.#scanner.rescanOnNewBlock({
145
+ const { txsToUpdate, utxos, ordinalsUtxos } = await this.#scanner.rescanOnNewBlock({
146
146
  walletAccount,
147
147
  })
148
148
 
149
- if (utxos) {
149
+ if (utxos || ordinalsUtxos) {
150
150
  await aci.updateAccountState({
151
151
  assetName,
152
152
  walletAccount,
153
- newData: {
154
- utxos,
155
- },
153
+ newData: pickBy(
154
+ {
155
+ utxos,
156
+ ordinalsUtxos,
157
+ },
158
+ Boolean
159
+ ),
156
160
  })
157
161
  }
158
162
 
@@ -211,6 +215,7 @@ export class Monitor extends BaseMonitor {
211
215
  txsToAdd,
212
216
  txsToUpdate,
213
217
  utxos,
218
+ ordinalsUtxos,
214
219
  changedUnusedAddressIndexes,
215
220
  } = await this.#scanner.rescanBlockchainInsight({
216
221
  walletAccount,
@@ -218,7 +223,18 @@ export class Monitor extends BaseMonitor {
218
223
  })
219
224
  const accountState = await aci.getAccountState({ assetName, walletAccount })
220
225
 
221
- if (utxos) await aci.updateAccountState({ assetName, walletAccount, newData: { utxos } })
226
+ if (utxos || ordinalsUtxos)
227
+ await aci.updateAccountState({
228
+ assetName,
229
+ walletAccount,
230
+ newData: pickBy(
231
+ {
232
+ utxos,
233
+ ordinalsUtxos,
234
+ },
235
+ Boolean
236
+ ),
237
+ })
222
238
 
223
239
  if (!isEmpty(changedUnusedAddressIndexes)) {
224
240
  // Only for mobile atm, browser and hydra calculates from the latest txLogs
@@ -1,6 +1,6 @@
1
1
  import assert from 'minimalistic-assert'
2
2
  // Using this notation so it can be mocked by jest
3
- import shuffle from 'lodash/shuffle'
3
+ import doShuffle from 'lodash/shuffle'
4
4
 
5
5
  import { UtxoCollection, Address } from '@exodus/models'
6
6
  import { retry } from '@exodus/simple-retry'
@@ -14,7 +14,7 @@ import {
14
14
  createOutput as dogecoinCreateOutput,
15
15
  } from './dogecoin'
16
16
  import { findUnconfirmedSentRbfTxs } from '../tx-utils'
17
- import { getUsableUtxos, getUtxos } from '../utxos-utils'
17
+ import { getOrdinalsUtxos, getUsableUtxos, getUtxos } from '../utxos-utils'
18
18
 
19
19
  import * as defaultBitcoinjsLib from '@exodus/bitcoinjs-lib'
20
20
 
@@ -132,7 +132,10 @@ export const createAndBroadcastTXFactory = ({
132
132
  getFeeEstimator,
133
133
  getSizeAndChangeScript = getSizeAndChangeScriptFactory(), // for decred customizations
134
134
  allowUnconfirmedRbfEnabledUtxos,
135
- }) => async ({ asset, walletAccount, address, amount, options }, { assetClientInterface }) => {
135
+ }) => async (
136
+ { asset: maybeToken, walletAccount, address, amount: tokenAmount, options },
137
+ { assetClientInterface }
138
+ ) => {
136
139
  const {
137
140
  multipleAddressesEnabled,
138
141
  feePerKB,
@@ -142,30 +145,73 @@ export const createAndBroadcastTXFactory = ({
142
145
  isBip70,
143
146
  bumpTxId,
144
147
  isRbfAllowed = true,
148
+ nft,
149
+ feeOpts,
145
150
  } = options
146
151
 
152
+ const brc20 = options.brc20 || feeOpts?.brc20 // feeOpts is the only way I've found atm to pass brc20 param without changing the tx-send hydra module
153
+
154
+ const asset = maybeToken.baseAsset
155
+
156
+ const isToken = maybeToken.name !== asset.name
157
+
158
+ if (isToken) {
159
+ assert(brc20, 'brc20 is required when sending bitcoin token')
160
+ }
161
+
162
+ const amount = isToken ? asset.currency.ZERO : tokenAmount
163
+
164
+ const assetName = asset.name
165
+
166
+ const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
167
+
168
+ const inscriptionIds = nft?.tokenId ? [nft?.tokenId] : brc20 ? brc20.inscriptionIds : undefined
169
+
170
+ const shuffle = (list) => {
171
+ return inscriptionIds ? list : doShuffle(list) // don't shuffle when sending ordinal!!!!
172
+ }
173
+
147
174
  assert(assetClientInterface, `assetClientInterface must be supplied in sendTx for ${asset.name}`)
148
175
  assert(
149
176
  address || bumpTxId,
150
177
  'should not be called without either a receiving address or to bump a tx'
151
178
  )
152
179
 
153
- const useCashAddress = asset.address.isCashAddress?.(address)
154
-
155
- const assetName = asset.name
180
+ if (inscriptionIds) {
181
+ assert(!bumpTxId, 'only inscriptionIds or bumpTxId must be provided')
182
+ assert(address, 'address must be provided when sending ordinals')
183
+ }
156
184
 
157
- const receiveAddresses = await assetClientInterface.getReceiveAddresses({
158
- walletAccount,
159
- assetName,
160
- multiAddressMode: true,
161
- })
185
+ const useCashAddress = asset.address.isCashAddress?.(address)
162
186
 
163
187
  const changeAddress = multipleAddressesEnabled
164
188
  ? await assetClientInterface.getNextChangeAddress({ assetName, walletAccount })
165
189
  : await assetClientInterface.getReceiveAddressObject({ assetName, walletAccount })
166
190
 
167
191
  const txSet = await assetClientInterface.getTxLog({ assetName, walletAccount })
168
- const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
192
+
193
+ const currentOrdinalsUtxos = getOrdinalsUtxos({ accountState, asset })
194
+ const transferOrdinalsUtxos = inscriptionIds
195
+ ? currentOrdinalsUtxos.filter((utxo) => inscriptionIds.includes(utxo.inscriptionId)) /// this could be bulk transfer if multiple inscription ids are provided
196
+ : undefined
197
+
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
+
169
215
  const insightClient = asset.baseAsset.insightClient
170
216
  const currency = asset.currency
171
217
  const feeData = await assetClientInterface.getFeeConfig({ assetName })
@@ -188,7 +234,7 @@ export const createAndBroadcastTXFactory = ({
188
234
  }
189
235
  }
190
236
 
191
- const rbfEnabled = feeData.rbfEnabled && !isExchange && !isBip70 && isRbfAllowed
237
+ const rbfEnabled = feeData.rbfEnabled && !isExchange && !isBip70 && isRbfAllowed && !nft && !brc20
192
238
 
193
239
  let utxosToBump
194
240
  if (bumpTxId) {
@@ -204,9 +250,10 @@ export const createAndBroadcastTXFactory = ({
204
250
  }
205
251
  }
206
252
 
207
- const sendAmount = bumpTxId ? asset.currency.ZERO : amount.toDefault()
208
- let receiveAddress = bumpTxId ? (replaceableTxs.length > 0 ? null : 'P2WPKH') : address
253
+ const sendAmount = bumpTxId || transferOrdinalsUtxos ? asset.currency.ZERO : amount.toDefault()
254
+ const receiveAddress = bumpTxId ? (replaceableTxs.length > 0 ? null : 'P2WPKH') : address
209
255
  const feeRate = feeData.feePerKB
256
+ const resolvedIsSendAll = (!rbfEnabled && feePerKB) || transferOrdinalsUtxos ? false : isSendAll
210
257
 
211
258
  let { selectedUtxos, fee, replaceTx } = selectUtxos({
212
259
  asset,
@@ -214,11 +261,12 @@ export const createAndBroadcastTXFactory = ({
214
261
  replaceableTxs,
215
262
  amount: sendAmount,
216
263
  feeRate: customFee || feeRate,
217
- receiveAddress,
218
- isSendAll: !rbfEnabled && feePerKB ? false : isSendAll,
264
+ receiveAddress: receiveAddress,
265
+ isSendAll: resolvedIsSendAll,
219
266
  getFeeEstimator: (asset, { feePerKB }) => getFeeEstimator(asset, feePerKB),
220
267
  mustSpendUtxos: utxosToBump,
221
268
  allowUnconfirmedRbfEnabledUtxos,
269
+ inscriptionIds,
222
270
  })
223
271
 
224
272
  if (!selectedUtxos && !replaceTx) throw new Error('Not enough funds.')
@@ -231,7 +279,9 @@ export const createAndBroadcastTXFactory = ({
231
279
  if (bumpTxId && (!selectedUtxos || (replaceTx && replaceTx.txId !== bumpTxId))) {
232
280
  throw new Error(`Unable to bump ${bumpTxId}`)
233
281
  }
234
-
282
+ if (transferOrdinalsUtxos) {
283
+ selectedUtxos = transferOrdinalsUtxos.union(selectedUtxos)
284
+ }
235
285
  if (replaceTx) {
236
286
  replaceTx = replaceTx.clone()
237
287
  replaceTx = replaceTx.update({ data: { ...replaceTx.data } })
@@ -239,6 +289,7 @@ export const createAndBroadcastTXFactory = ({
239
289
  return { ...to, amount: parseCurrency(to.amount, asset.currency) }
240
290
  })
241
291
  selectedUtxos = selectedUtxos.union(
292
+ // how to avoid replace tx inputs when inputs are ordinals? !!!!
242
293
  UtxoCollection.fromJSON(replaceTx.data.inputs, { currency: asset.currency })
243
294
  )
244
295
  }
@@ -255,14 +306,25 @@ export const createAndBroadcastTXFactory = ({
255
306
  outputs = []
256
307
  }
257
308
  if (address) {
258
- outputs.push(createOutput(assetName, address, sendAmount))
309
+ if (transferOrdinalsUtxos) {
310
+ outputs.push(
311
+ ...transferOrdinalsUtxos
312
+ .toArray()
313
+ .map((ordinalUtxo) => createOutput(assetName, address, ordinalUtxo.value))
314
+ )
315
+ } else {
316
+ outputs.push(createOutput(assetName, address, sendAmount))
317
+ }
259
318
  }
260
319
 
261
320
  const totalAmount = replaceTx
262
321
  ? replaceTx.data.sent.reduce((total, { amount }) => total.add(amount), sendAmount)
263
322
  : sendAmount
264
323
 
265
- const change = selectedUtxos.value.sub(totalAmount).sub(fee)
324
+ const change = selectedUtxos.value
325
+ .sub(totalAmount)
326
+ .sub(transferOrdinalsUtxos?.value || currency.ZERO)
327
+ .sub(fee)
266
328
  const dust = getDustValue(asset)
267
329
  let ourAddress = replaceTx?.data?.changeAddress || changeAddress
268
330
  if (asset.address.toLegacyAddress) {
@@ -377,18 +439,29 @@ export const createAndBroadcastTXFactory = ({
377
439
  remainingUtxos = remainingUtxos.difference(remainingUtxos.getTxIdUtxos(replaceTx.txId))
378
440
  }
379
441
 
442
+ const remainingOrdinalsUtxos = transferOrdinalsUtxos
443
+ ? currentOrdinalsUtxos.difference(transferOrdinalsUtxos)
444
+ : undefined
445
+
380
446
  await assetClientInterface.updateAccountState({
381
447
  assetName,
382
448
  walletAccount,
383
- accountState,
384
- newData: { utxos: remainingUtxos },
449
+ newData: {
450
+ utxos: remainingUtxos,
451
+ ordinalsUtxos: remainingOrdinalsUtxos,
452
+ },
385
453
  })
386
454
 
455
+ const walletAddresses = await assetClientInterface.getReceiveAddresses({
456
+ walletAccount,
457
+ assetName,
458
+ multiAddressMode: true,
459
+ })
387
460
  // There are two cases of bumping, replacing or chaining a self-send.
388
461
  // If we have a bumpTxId, but we aren't replacing, then it is a self-send.
389
462
  const selfSend = bumpTxId
390
463
  ? !replaceTx
391
- : receiveAddresses.some((receiveAddress) => String(receiveAddress) === String(address))
464
+ : walletAddresses.some((receiveAddress) => String(receiveAddress) === String(address))
392
465
 
393
466
  const displayReceiveAddress = asset.address.displayAddress?.(receiveAddress) || receiveAddress
394
467
 
@@ -400,15 +473,21 @@ export const createAndBroadcastTXFactory = ({
400
473
  ? replaceTx.data.sent.concat([{ address: displayReceiveAddress, amount }])
401
474
  : [{ address: displayReceiveAddress, amount }]
402
475
 
476
+ const coinAmount = selfSend
477
+ ? maybeToken.currency.ZERO
478
+ : isToken
479
+ ? tokenAmount.abs().negate()
480
+ : totalAmount.abs().negate()
481
+
403
482
  await assetClientInterface.updateTxLogAndNotify({
404
- assetName,
483
+ assetName: maybeToken.name,
405
484
  walletAccount,
406
485
  txs: [
407
486
  {
408
487
  txId,
409
488
  confirmations: 0,
410
- coinAmount: selfSend ? currency.ZERO : totalAmount.abs().negate(),
411
- coinName: assetName,
489
+ coinAmount,
490
+ coinName: maybeToken.name,
412
491
  feeAmount: fee,
413
492
  feeCoinName: assetName,
414
493
  selfSend,
@@ -421,6 +500,8 @@ export const createAndBroadcastTXFactory = ({
421
500
  blocksSeen: 0,
422
501
  inputs: selectedUtxos.toJSON(),
423
502
  replacedTxId: replaceTx ? replaceTx.txId : undefined,
503
+ nftId: nft ? `${assetName}:${nft.tokenId}` : undefined, // it allows BE to load the nft info while the nft is in transit
504
+ inscriptionIds: inscriptionIds,
424
505
  },
425
506
  },
426
507
  ],
@@ -457,6 +538,7 @@ function defaultCreateInputs(utxos, rbfEnabled) {
457
538
  value: parseInt(utxo.value.toBaseString(), 10),
458
539
  script: utxo.script,
459
540
  sequence: getTxSequence(rbfEnabled),
541
+ inscriptionId: utxo.inscriptionId,
460
542
  }))
461
543
  }
462
544
 
@@ -2,6 +2,9 @@
2
2
  import { UtxoCollection } from '@exodus/models'
3
3
  import { findLargeUnconfirmedTxs } from './tx-utils'
4
4
  import assert from 'minimalistic-assert'
5
+ import { isOrdinalAddress } from './address-utils'
6
+
7
+ const MAX_ORDINAL_VALUE_POSTAGE = 10000
5
8
 
6
9
  export function getUtxos({ accountState, asset }) {
7
10
  return (
@@ -12,6 +15,41 @@ export function getUtxos({ accountState, asset }) {
12
15
  )
13
16
  }
14
17
 
18
+ export function getOrdinalsUtxos({ accountState, asset }) {
19
+ return (
20
+ accountState?.ordinalsUtxos ||
21
+ UtxoCollection.createEmpty({
22
+ currency: asset.currency,
23
+ })
24
+ )
25
+ }
26
+
27
+ function isOrdinalUtxo({ utxo, ordinalChainIndex }) {
28
+ if (utxo.inscriptionId) {
29
+ return true
30
+ }
31
+ if (ordinalChainIndex === undefined || ordinalChainIndex < 0) {
32
+ // exclude utxos splitting
33
+ return false
34
+ }
35
+
36
+ const maybeAnOrdinalAddress =
37
+ ordinalChainIndex && isOrdinalAddress(utxo.address, ordinalChainIndex) // if wallet receives utxos to the special address, consider it as ordinal
38
+ if (utxo.confirmations) {
39
+ return maybeAnOrdinalAddress
40
+ }
41
+
42
+ return utxo.value.toBaseNumber() <= MAX_ORDINAL_VALUE_POSTAGE || maybeAnOrdinalAddress // while unconfirmed, put < 10000- sats in the ordinal utxos box just in case
43
+ }
44
+
45
+ export function partitionUtxos({ allUtxos, ordinalChainIndex }) {
46
+ assert(allUtxos, 'allUtxos is required')
47
+ return {
48
+ utxos: allUtxos.filter((utxo) => !isOrdinalUtxo({ utxo, ordinalChainIndex })),
49
+ ordinalsUtxos: allUtxos.filter((utxo) => isOrdinalUtxo({ utxo, ordinalChainIndex })),
50
+ }
51
+ }
52
+
15
53
  export function getConfirmedUtxos({ utxos }) {
16
54
  assert(utxos, 'utxos is required')
17
55
  return utxos.filter(({ confirmations }) => confirmations > 0)