@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 +2 -3
- package/src/account-state.js +17 -6
- package/src/address-utils.js +24 -0
- package/src/fee/get-fee-resolver.js +37 -9
- package/src/fee/utxo-selector.js +43 -11
- package/src/tx-log/bitcoin-monitor-scanner.js +51 -25
- package/src/tx-log/bitcoin-monitor.js +23 -7
- package/src/tx-send/index.js +108 -26
- package/src/utxos-utils.js +38 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bitcoin-api",
|
|
3
|
-
"version": "2.
|
|
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
|
}
|
package/src/account-state.js
CHANGED
|
@@ -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 = ({
|
|
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 = ({
|
|
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
|
-
|
|
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
|
}
|
package/src/fee/utxo-selector.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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 =
|
|
82
|
-
|
|
83
|
-
|
|
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 ?
|
|
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:
|
|
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 =
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
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
|
|
470
|
-
utxoCol =
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
package/src/tx-send/index.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
384
|
-
|
|
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
|
-
:
|
|
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
|
|
411
|
-
coinName:
|
|
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
|
|
package/src/utxos-utils.js
CHANGED
|
@@ -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)
|