@exodus/bitcoin-api 2.3.16 → 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 +83 -22
- 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,30 +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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
165
|
+
if (inscriptionIds) {
|
|
166
|
+
assert(!bumpTxId, 'only inscriptionIds or bumpTxId must be provided')
|
|
167
|
+
assert(address, 'address must be provided when sending ordinals')
|
|
168
|
+
}
|
|
156
169
|
|
|
157
|
-
const
|
|
158
|
-
walletAccount,
|
|
159
|
-
assetName,
|
|
160
|
-
multiAddressMode: true,
|
|
161
|
-
})
|
|
170
|
+
const useCashAddress = asset.address.isCashAddress?.(address)
|
|
162
171
|
|
|
163
172
|
const changeAddress = multipleAddressesEnabled
|
|
164
173
|
? await assetClientInterface.getNextChangeAddress({ assetName, walletAccount })
|
|
165
174
|
: await assetClientInterface.getReceiveAddressObject({ assetName, walletAccount })
|
|
166
175
|
|
|
167
176
|
const txSet = await assetClientInterface.getTxLog({ assetName, walletAccount })
|
|
168
|
-
|
|
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
|
+
|
|
169
200
|
const insightClient = asset.baseAsset.insightClient
|
|
170
201
|
const currency = asset.currency
|
|
171
202
|
const feeData = await assetClientInterface.getFeeConfig({ assetName })
|
|
@@ -188,7 +219,7 @@ export const createAndBroadcastTXFactory = ({
|
|
|
188
219
|
}
|
|
189
220
|
}
|
|
190
221
|
|
|
191
|
-
const rbfEnabled = feeData.rbfEnabled && !isExchange && !isBip70 && isRbfAllowed
|
|
222
|
+
const rbfEnabled = feeData.rbfEnabled && !isExchange && !isBip70 && isRbfAllowed && !nft && !brc20
|
|
192
223
|
|
|
193
224
|
let utxosToBump
|
|
194
225
|
if (bumpTxId) {
|
|
@@ -204,9 +235,10 @@ export const createAndBroadcastTXFactory = ({
|
|
|
204
235
|
}
|
|
205
236
|
}
|
|
206
237
|
|
|
207
|
-
const sendAmount = bumpTxId ? asset.currency.ZERO : amount.toDefault()
|
|
208
|
-
|
|
238
|
+
const sendAmount = bumpTxId || transferOrdinalsUtxos ? asset.currency.ZERO : amount.toDefault()
|
|
239
|
+
const receiveAddress = bumpTxId ? (replaceableTxs.length > 0 ? null : 'P2WPKH') : address
|
|
209
240
|
const feeRate = feeData.feePerKB
|
|
241
|
+
const resolvedIsSendAll = (!rbfEnabled && feePerKB) || transferOrdinalsUtxos ? false : isSendAll
|
|
210
242
|
|
|
211
243
|
let { selectedUtxos, fee, replaceTx } = selectUtxos({
|
|
212
244
|
asset,
|
|
@@ -214,11 +246,12 @@ export const createAndBroadcastTXFactory = ({
|
|
|
214
246
|
replaceableTxs,
|
|
215
247
|
amount: sendAmount,
|
|
216
248
|
feeRate: customFee || feeRate,
|
|
217
|
-
receiveAddress,
|
|
218
|
-
isSendAll:
|
|
249
|
+
receiveAddress: receiveAddress,
|
|
250
|
+
isSendAll: resolvedIsSendAll,
|
|
219
251
|
getFeeEstimator: (asset, { feePerKB }) => getFeeEstimator(asset, feePerKB),
|
|
220
252
|
mustSpendUtxos: utxosToBump,
|
|
221
253
|
allowUnconfirmedRbfEnabledUtxos,
|
|
254
|
+
inscriptionIds,
|
|
222
255
|
})
|
|
223
256
|
|
|
224
257
|
if (!selectedUtxos && !replaceTx) throw new Error('Not enough funds.')
|
|
@@ -231,7 +264,9 @@ export const createAndBroadcastTXFactory = ({
|
|
|
231
264
|
if (bumpTxId && (!selectedUtxos || (replaceTx && replaceTx.txId !== bumpTxId))) {
|
|
232
265
|
throw new Error(`Unable to bump ${bumpTxId}`)
|
|
233
266
|
}
|
|
234
|
-
|
|
267
|
+
if (transferOrdinalsUtxos) {
|
|
268
|
+
selectedUtxos = transferOrdinalsUtxos.union(selectedUtxos)
|
|
269
|
+
}
|
|
235
270
|
if (replaceTx) {
|
|
236
271
|
replaceTx = replaceTx.clone()
|
|
237
272
|
replaceTx = replaceTx.update({ data: { ...replaceTx.data } })
|
|
@@ -239,6 +274,7 @@ export const createAndBroadcastTXFactory = ({
|
|
|
239
274
|
return { ...to, amount: parseCurrency(to.amount, asset.currency) }
|
|
240
275
|
})
|
|
241
276
|
selectedUtxos = selectedUtxos.union(
|
|
277
|
+
// how to avoid replace tx inputs when inputs are ordinals? !!!!
|
|
242
278
|
UtxoCollection.fromJSON(replaceTx.data.inputs, { currency: asset.currency })
|
|
243
279
|
)
|
|
244
280
|
}
|
|
@@ -255,14 +291,25 @@ export const createAndBroadcastTXFactory = ({
|
|
|
255
291
|
outputs = []
|
|
256
292
|
}
|
|
257
293
|
if (address) {
|
|
258
|
-
|
|
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
|
+
}
|
|
259
303
|
}
|
|
260
304
|
|
|
261
305
|
const totalAmount = replaceTx
|
|
262
306
|
? replaceTx.data.sent.reduce((total, { amount }) => total.add(amount), sendAmount)
|
|
263
307
|
: sendAmount
|
|
264
308
|
|
|
265
|
-
const change = selectedUtxos.value
|
|
309
|
+
const change = selectedUtxos.value
|
|
310
|
+
.sub(totalAmount)
|
|
311
|
+
.sub(transferOrdinalsUtxos?.value || currency.ZERO)
|
|
312
|
+
.sub(fee)
|
|
266
313
|
const dust = getDustValue(asset)
|
|
267
314
|
let ourAddress = replaceTx?.data?.changeAddress || changeAddress
|
|
268
315
|
if (asset.address.toLegacyAddress) {
|
|
@@ -377,18 +424,29 @@ export const createAndBroadcastTXFactory = ({
|
|
|
377
424
|
remainingUtxos = remainingUtxos.difference(remainingUtxos.getTxIdUtxos(replaceTx.txId))
|
|
378
425
|
}
|
|
379
426
|
|
|
427
|
+
const remainingOrdinalsUtxos = transferOrdinalsUtxos
|
|
428
|
+
? currentOrdinalsUtxos.difference(transferOrdinalsUtxos)
|
|
429
|
+
: undefined
|
|
430
|
+
|
|
380
431
|
await assetClientInterface.updateAccountState({
|
|
381
432
|
assetName,
|
|
382
433
|
walletAccount,
|
|
383
|
-
|
|
384
|
-
|
|
434
|
+
newData: {
|
|
435
|
+
utxos: remainingUtxos,
|
|
436
|
+
ordinalsUtxos: remainingOrdinalsUtxos,
|
|
437
|
+
},
|
|
385
438
|
})
|
|
386
439
|
|
|
440
|
+
const walletAddresses = await assetClientInterface.getReceiveAddresses({
|
|
441
|
+
walletAccount,
|
|
442
|
+
assetName,
|
|
443
|
+
multiAddressMode: true,
|
|
444
|
+
})
|
|
387
445
|
// There are two cases of bumping, replacing or chaining a self-send.
|
|
388
446
|
// If we have a bumpTxId, but we aren't replacing, then it is a self-send.
|
|
389
447
|
const selfSend = bumpTxId
|
|
390
448
|
? !replaceTx
|
|
391
|
-
:
|
|
449
|
+
: walletAddresses.some((receiveAddress) => String(receiveAddress) === String(address))
|
|
392
450
|
|
|
393
451
|
const displayReceiveAddress = asset.address.displayAddress?.(receiveAddress) || receiveAddress
|
|
394
452
|
|
|
@@ -421,6 +479,8 @@ export const createAndBroadcastTXFactory = ({
|
|
|
421
479
|
blocksSeen: 0,
|
|
422
480
|
inputs: selectedUtxos.toJSON(),
|
|
423
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,
|
|
424
484
|
},
|
|
425
485
|
},
|
|
426
486
|
],
|
|
@@ -457,6 +517,7 @@ function defaultCreateInputs(utxos, rbfEnabled) {
|
|
|
457
517
|
value: parseInt(utxo.value.toBaseString(), 10),
|
|
458
518
|
script: utxo.script,
|
|
459
519
|
sequence: getTxSequence(rbfEnabled),
|
|
520
|
+
inscriptionId: utxo.inscriptionId,
|
|
460
521
|
}))
|
|
461
522
|
}
|
|
462
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)
|