@exodus/bitcoin-api 2.3.15 → 2.4.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 +2 -2
- package/src/account-state.js +17 -6
- package/src/address-utils.js +24 -0
- package/src/fee/get-fee-resolver.js +36 -8
- 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 +84 -20
- 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.0",
|
|
4
4
|
"description": "Exodus bitcoin-api",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -41,5 +41,5 @@
|
|
|
41
41
|
"@exodus/bitcoin-meta": "^1.0.1",
|
|
42
42
|
"jest-when": "^3.5.1"
|
|
43
43
|
},
|
|
44
|
-
"gitHead": "
|
|
44
|
+
"gitHead": "de21cbd727cb6cbe4fac07b58cc6e3fa8af318ff"
|
|
45
45
|
}
|
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,7 +14,30 @@ 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,
|
|
@@ -23,6 +46,7 @@ export class GetFeeResolver {
|
|
|
23
46
|
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
|
|
|
@@ -142,28 +142,61 @@ export const createAndBroadcastTXFactory = ({
|
|
|
142
142
|
isBip70,
|
|
143
143
|
bumpTxId,
|
|
144
144
|
isRbfAllowed = true,
|
|
145
|
+
nft,
|
|
146
|
+
brc20,
|
|
145
147
|
} = options
|
|
146
148
|
|
|
149
|
+
const assetName = asset.name
|
|
150
|
+
|
|
151
|
+
const accountState = await assetClientInterface.getAccountState({ assetName, walletAccount })
|
|
152
|
+
|
|
153
|
+
const inscriptionIds = nft?.tokenId ? [nft?.tokenId] : brc20 ? brc20.inscriptionIds : undefined
|
|
154
|
+
|
|
155
|
+
const shuffle = (list) => {
|
|
156
|
+
return inscriptionIds ? list : doShuffle(list) // don't shuffle when sending ordinal!!!!
|
|
157
|
+
}
|
|
158
|
+
|
|
147
159
|
assert(assetClientInterface, `assetClientInterface must be supplied in sendTx for ${asset.name}`)
|
|
148
160
|
assert(
|
|
149
161
|
address || bumpTxId,
|
|
150
162
|
'should not be called without either a receiving address or to bump a tx'
|
|
151
163
|
)
|
|
152
164
|
|
|
153
|
-
|
|
165
|
+
if (inscriptionIds) {
|
|
166
|
+
assert(!bumpTxId, 'only inscriptionIds or bumpTxId must be provided')
|
|
167
|
+
assert(address, 'address must be provided when sending ordinals')
|
|
168
|
+
}
|
|
154
169
|
|
|
155
|
-
const
|
|
156
|
-
walletAccount,
|
|
157
|
-
assetName,
|
|
158
|
-
multiAddressMode: true,
|
|
159
|
-
})
|
|
170
|
+
const useCashAddress = asset.address.isCashAddress?.(address)
|
|
160
171
|
|
|
161
172
|
const changeAddress = multipleAddressesEnabled
|
|
162
173
|
? await assetClientInterface.getNextChangeAddress({ assetName, walletAccount })
|
|
163
174
|
: await assetClientInterface.getReceiveAddressObject({ assetName, walletAccount })
|
|
164
175
|
|
|
165
176
|
const txSet = await assetClientInterface.getTxLog({ assetName, walletAccount })
|
|
166
|
-
|
|
177
|
+
|
|
178
|
+
const currentOrdinalsUtxos = getOrdinalsUtxos({ accountState, asset })
|
|
179
|
+
const transferOrdinalsUtxos = inscriptionIds
|
|
180
|
+
? currentOrdinalsUtxos.filter((utxo) => inscriptionIds.includes(utxo.inscriptionId)) /// this could be bulk transfer if multiple inscription ids are provided
|
|
181
|
+
: undefined
|
|
182
|
+
|
|
183
|
+
if (inscriptionIds) {
|
|
184
|
+
assert(
|
|
185
|
+
transferOrdinalsUtxos?.size === inscriptionIds.length,
|
|
186
|
+
`Expected ordinal utxos ${inscriptionIds.length}. Found: ${transferOrdinalsUtxos?.size || 0}`
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
const unconfirmedOrdinalUtxos = transferOrdinalsUtxos
|
|
190
|
+
.toArray()
|
|
191
|
+
.filter((ordinalUtxo) => !(ordinalUtxo.confirmations > 0))
|
|
192
|
+
assert(
|
|
193
|
+
!unconfirmedOrdinalUtxos.length,
|
|
194
|
+
`OrdinalUtxo with inscription ids ${unconfirmedOrdinalUtxos
|
|
195
|
+
.map((utxo) => utxo.inscriptionId)
|
|
196
|
+
.join(', ')} have not confirmed yet`
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
167
200
|
const insightClient = asset.baseAsset.insightClient
|
|
168
201
|
const currency = asset.currency
|
|
169
202
|
const feeData = await assetClientInterface.getFeeConfig({ assetName })
|
|
@@ -186,7 +219,7 @@ export const createAndBroadcastTXFactory = ({
|
|
|
186
219
|
}
|
|
187
220
|
}
|
|
188
221
|
|
|
189
|
-
const rbfEnabled = feeData.rbfEnabled && !isExchange && !isBip70 && isRbfAllowed
|
|
222
|
+
const rbfEnabled = feeData.rbfEnabled && !isExchange && !isBip70 && isRbfAllowed && !nft && !brc20
|
|
190
223
|
|
|
191
224
|
let utxosToBump
|
|
192
225
|
if (bumpTxId) {
|
|
@@ -202,9 +235,10 @@ export const createAndBroadcastTXFactory = ({
|
|
|
202
235
|
}
|
|
203
236
|
}
|
|
204
237
|
|
|
205
|
-
const sendAmount = bumpTxId ? asset.currency.ZERO : amount.toDefault()
|
|
206
|
-
|
|
238
|
+
const sendAmount = bumpTxId || transferOrdinalsUtxos ? asset.currency.ZERO : amount.toDefault()
|
|
239
|
+
const receiveAddress = bumpTxId ? (replaceableTxs.length > 0 ? null : 'P2WPKH') : address
|
|
207
240
|
const feeRate = feeData.feePerKB
|
|
241
|
+
const resolvedIsSendAll = (!rbfEnabled && feePerKB) || transferOrdinalsUtxos ? false : isSendAll
|
|
208
242
|
|
|
209
243
|
let { selectedUtxos, fee, replaceTx } = selectUtxos({
|
|
210
244
|
asset,
|
|
@@ -212,11 +246,12 @@ export const createAndBroadcastTXFactory = ({
|
|
|
212
246
|
replaceableTxs,
|
|
213
247
|
amount: sendAmount,
|
|
214
248
|
feeRate: customFee || feeRate,
|
|
215
|
-
receiveAddress,
|
|
216
|
-
isSendAll:
|
|
249
|
+
receiveAddress: receiveAddress,
|
|
250
|
+
isSendAll: resolvedIsSendAll,
|
|
217
251
|
getFeeEstimator: (asset, { feePerKB }) => getFeeEstimator(asset, feePerKB),
|
|
218
252
|
mustSpendUtxos: utxosToBump,
|
|
219
253
|
allowUnconfirmedRbfEnabledUtxos,
|
|
254
|
+
inscriptionIds,
|
|
220
255
|
})
|
|
221
256
|
|
|
222
257
|
if (!selectedUtxos && !replaceTx) throw new Error('Not enough funds.')
|
|
@@ -229,7 +264,9 @@ export const createAndBroadcastTXFactory = ({
|
|
|
229
264
|
if (bumpTxId && (!selectedUtxos || (replaceTx && replaceTx.txId !== bumpTxId))) {
|
|
230
265
|
throw new Error(`Unable to bump ${bumpTxId}`)
|
|
231
266
|
}
|
|
232
|
-
|
|
267
|
+
if (transferOrdinalsUtxos) {
|
|
268
|
+
selectedUtxos = transferOrdinalsUtxos.union(selectedUtxos)
|
|
269
|
+
}
|
|
233
270
|
if (replaceTx) {
|
|
234
271
|
replaceTx = replaceTx.clone()
|
|
235
272
|
replaceTx = replaceTx.update({ data: { ...replaceTx.data } })
|
|
@@ -237,6 +274,7 @@ export const createAndBroadcastTXFactory = ({
|
|
|
237
274
|
return { ...to, amount: parseCurrency(to.amount, asset.currency) }
|
|
238
275
|
})
|
|
239
276
|
selectedUtxos = selectedUtxos.union(
|
|
277
|
+
// how to avoid replace tx inputs when inputs are ordinals? !!!!
|
|
240
278
|
UtxoCollection.fromJSON(replaceTx.data.inputs, { currency: asset.currency })
|
|
241
279
|
)
|
|
242
280
|
}
|
|
@@ -253,14 +291,25 @@ export const createAndBroadcastTXFactory = ({
|
|
|
253
291
|
outputs = []
|
|
254
292
|
}
|
|
255
293
|
if (address) {
|
|
256
|
-
|
|
294
|
+
if (transferOrdinalsUtxos) {
|
|
295
|
+
outputs.push(
|
|
296
|
+
...transferOrdinalsUtxos
|
|
297
|
+
.toArray()
|
|
298
|
+
.map((ordinalUtxo) => createOutput(assetName, address, ordinalUtxo.value))
|
|
299
|
+
)
|
|
300
|
+
} else {
|
|
301
|
+
outputs.push(createOutput(assetName, address, sendAmount))
|
|
302
|
+
}
|
|
257
303
|
}
|
|
258
304
|
|
|
259
305
|
const totalAmount = replaceTx
|
|
260
306
|
? replaceTx.data.sent.reduce((total, { amount }) => total.add(amount), sendAmount)
|
|
261
307
|
: sendAmount
|
|
262
308
|
|
|
263
|
-
const change = selectedUtxos.value
|
|
309
|
+
const change = selectedUtxos.value
|
|
310
|
+
.sub(totalAmount)
|
|
311
|
+
.sub(transferOrdinalsUtxos?.value || currency.ZERO)
|
|
312
|
+
.sub(fee)
|
|
264
313
|
const dust = getDustValue(asset)
|
|
265
314
|
let ourAddress = replaceTx?.data?.changeAddress || changeAddress
|
|
266
315
|
if (asset.address.toLegacyAddress) {
|
|
@@ -292,6 +341,7 @@ export const createAndBroadcastTXFactory = ({
|
|
|
292
341
|
outputs,
|
|
293
342
|
},
|
|
294
343
|
txMeta: {
|
|
344
|
+
useCashAddress, // for trezor to show the receiver cash address
|
|
295
345
|
addressPathsMap: selectedUtxos.getAddressPathsMap(),
|
|
296
346
|
blockHeight,
|
|
297
347
|
},
|
|
@@ -374,18 +424,29 @@ export const createAndBroadcastTXFactory = ({
|
|
|
374
424
|
remainingUtxos = remainingUtxos.difference(remainingUtxos.getTxIdUtxos(replaceTx.txId))
|
|
375
425
|
}
|
|
376
426
|
|
|
427
|
+
const remainingOrdinalsUtxos = transferOrdinalsUtxos
|
|
428
|
+
? currentOrdinalsUtxos.difference(transferOrdinalsUtxos)
|
|
429
|
+
: undefined
|
|
430
|
+
|
|
377
431
|
await assetClientInterface.updateAccountState({
|
|
378
432
|
assetName,
|
|
379
433
|
walletAccount,
|
|
380
|
-
|
|
381
|
-
|
|
434
|
+
newData: {
|
|
435
|
+
utxos: remainingUtxos,
|
|
436
|
+
ordinalsUtxos: remainingOrdinalsUtxos,
|
|
437
|
+
},
|
|
382
438
|
})
|
|
383
439
|
|
|
440
|
+
const walletAddresses = await assetClientInterface.getReceiveAddresses({
|
|
441
|
+
walletAccount,
|
|
442
|
+
assetName,
|
|
443
|
+
multiAddressMode: true,
|
|
444
|
+
})
|
|
384
445
|
// There are two cases of bumping, replacing or chaining a self-send.
|
|
385
446
|
// If we have a bumpTxId, but we aren't replacing, then it is a self-send.
|
|
386
447
|
const selfSend = bumpTxId
|
|
387
448
|
? !replaceTx
|
|
388
|
-
:
|
|
449
|
+
: walletAddresses.some((receiveAddress) => String(receiveAddress) === String(address))
|
|
389
450
|
|
|
390
451
|
const displayReceiveAddress = asset.address.displayAddress?.(receiveAddress) || receiveAddress
|
|
391
452
|
|
|
@@ -418,6 +479,8 @@ export const createAndBroadcastTXFactory = ({
|
|
|
418
479
|
blocksSeen: 0,
|
|
419
480
|
inputs: selectedUtxos.toJSON(),
|
|
420
481
|
replacedTxId: replaceTx ? replaceTx.txId : undefined,
|
|
482
|
+
nftId: nft ? `${assetName}:${nft.tokenId}` : undefined, // it allows BE to load the nft info while the nft is in transit
|
|
483
|
+
inscriptionIds: inscriptionIds,
|
|
421
484
|
},
|
|
422
485
|
},
|
|
423
486
|
],
|
|
@@ -454,6 +517,7 @@ function defaultCreateInputs(utxos, rbfEnabled) {
|
|
|
454
517
|
value: parseInt(utxo.value.toBaseString(), 10),
|
|
455
518
|
script: utxo.script,
|
|
456
519
|
sequence: getTxSequence(rbfEnabled),
|
|
520
|
+
inscriptionId: utxo.inscriptionId,
|
|
457
521
|
}))
|
|
458
522
|
}
|
|
459
523
|
|
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)
|