@exodus/bitcoin-api 2.6.6 → 2.6.8-hiro.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -3
- package/src/ordinals-utils.js +14 -2
- package/src/tx-log/bitcoin-monitor-scanner.js +44 -6
- package/src/tx-send/index.js +29 -21
- package/src/utxos-utils.js +59 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bitcoin-api",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.8-hiro.0",
|
|
4
4
|
"description": "Exodus bitcoin-api",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -42,6 +42,5 @@
|
|
|
42
42
|
"@scure/base": "^1.1.3",
|
|
43
43
|
"@scure/btc-signer": "^1.1.0",
|
|
44
44
|
"jest-when": "^3.5.1"
|
|
45
|
-
}
|
|
46
|
-
"gitHead": "b2ed2bffe712facdd412df1a0ebc6a8ac92c4d1c"
|
|
45
|
+
}
|
|
47
46
|
}
|
package/src/ordinals-utils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'minimalistic-assert'
|
|
2
2
|
|
|
3
|
-
export function getOrdinalAddress({
|
|
3
|
+
export async function getOrdinalAddress({
|
|
4
4
|
asset,
|
|
5
5
|
assetClientInterface,
|
|
6
6
|
walletAccount,
|
|
@@ -12,10 +12,22 @@ export function getOrdinalAddress({
|
|
|
12
12
|
if (ordinalChainIndex === undefined) {
|
|
13
13
|
return undefined
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
const purposes = await assetClientInterface.getSupportedPurposes({
|
|
17
|
+
assetName: asset.name,
|
|
18
|
+
walletAccount,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const purpose = 86
|
|
22
|
+
|
|
23
|
+
if (!purposes.includes(purpose)) {
|
|
24
|
+
return undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
return assetClientInterface.getAddress({
|
|
16
28
|
assetName: asset.name,
|
|
17
29
|
walletAccount,
|
|
18
|
-
purpose
|
|
30
|
+
purpose,
|
|
19
31
|
chainIndex: ordinalChainIndex,
|
|
20
32
|
addressIndex: 0,
|
|
21
33
|
})
|
|
@@ -67,7 +67,6 @@ export class BitcoinMonitorScanner {
|
|
|
67
67
|
|
|
68
68
|
const storedUtxos = getUtxos({ asset, accountState })
|
|
69
69
|
const storedOrdinalUtxos = getOrdinalsUtxos({ asset, accountState })
|
|
70
|
-
const ordinalAddress = await this.getOrdinalAddress({ walletAccount })
|
|
71
70
|
|
|
72
71
|
const currentStoredUtxos = storedUtxos.union(storedOrdinalUtxos)
|
|
73
72
|
|
|
@@ -90,7 +89,9 @@ export class BitcoinMonitorScanner {
|
|
|
90
89
|
: (txs) =>
|
|
91
90
|
txs.filter((tx) => {
|
|
92
91
|
const txItem = currentTxs.get(tx.txid)
|
|
93
|
-
|
|
92
|
+
const confirmed = txItem && txItem.confirmed
|
|
93
|
+
const inscriptionsIndexed = txItem?.data?.inscriptionsIndexed
|
|
94
|
+
return confirmed && (!this.#ordinalsEnabled || inscriptionsIndexed)
|
|
94
95
|
}).length >= txs.length
|
|
95
96
|
|
|
96
97
|
const unusedAddressIndexes = await assetClientInterface.getUnusedAddressIndexes({
|
|
@@ -344,6 +345,15 @@ export class BitcoinMonitorScanner {
|
|
|
344
345
|
currencies: { [assetName]: currency },
|
|
345
346
|
}
|
|
346
347
|
|
|
348
|
+
if (this.#ordinalsEnabled) {
|
|
349
|
+
txLogItem.data = {
|
|
350
|
+
...txLogItem.data,
|
|
351
|
+
inscriptionsIndexed: txItem.inscriptionsIndexed,
|
|
352
|
+
sentInscriptions: [],
|
|
353
|
+
receivedInscriptions: [],
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
347
357
|
let from = []
|
|
348
358
|
|
|
349
359
|
// if txItem.vin has an address that matches ours, means we've spent this tx
|
|
@@ -361,8 +371,13 @@ export class BitcoinMonitorScanner {
|
|
|
361
371
|
txLogItem.coinAmount = txLogItem.coinAmount.sub(currency.defaultUnit(vin.value))
|
|
362
372
|
isSent = true
|
|
363
373
|
txLogItem.data.sent = []
|
|
364
|
-
if (
|
|
365
|
-
txLogItem.data.
|
|
374
|
+
if (this.#ordinalsEnabled && vin.inscriptions) {
|
|
375
|
+
txLogItem.data.sentInscriptions.push(
|
|
376
|
+
...vin.inscriptions.map((i) => ({
|
|
377
|
+
...i,
|
|
378
|
+
value: currency.defaultUnit(vin.value).toBaseNumber(),
|
|
379
|
+
}))
|
|
380
|
+
)
|
|
366
381
|
}
|
|
367
382
|
|
|
368
383
|
// this is only used to exclude the utxos in the reducer which is why we don't care about the other fields
|
|
@@ -432,8 +447,13 @@ export class BitcoinMonitorScanner {
|
|
|
432
447
|
txLogItem.data.changeAddress = address
|
|
433
448
|
}
|
|
434
449
|
|
|
435
|
-
if (
|
|
436
|
-
txLogItem.data.
|
|
450
|
+
if (this.#ordinalsEnabled && vout.inscriptions) {
|
|
451
|
+
txLogItem.data.receivedInscriptions.push(
|
|
452
|
+
...vout.inscriptions.map((i) => ({
|
|
453
|
+
...i,
|
|
454
|
+
value: currency.defaultUnit(vout.value).toBaseNumber(),
|
|
455
|
+
}))
|
|
456
|
+
)
|
|
437
457
|
}
|
|
438
458
|
|
|
439
459
|
// it was sent to us...
|
|
@@ -450,6 +470,11 @@ export class BitcoinMonitorScanner {
|
|
|
450
470
|
rbfEnabled: txItem.rbf,
|
|
451
471
|
}
|
|
452
472
|
|
|
473
|
+
if (this.#ordinalsEnabled) {
|
|
474
|
+
output.inscriptionsIndexed = txItem.inscriptionsIndexed
|
|
475
|
+
output.inscriptions = vout.inscriptions || []
|
|
476
|
+
}
|
|
477
|
+
|
|
453
478
|
if (this.#shouldExcludeVoutUtxo({ asset, output, txItem, vout })) {
|
|
454
479
|
return
|
|
455
480
|
}
|
|
@@ -508,6 +533,16 @@ export class BitcoinMonitorScanner {
|
|
|
508
533
|
// Keep new utxos when they intersect with the stored utxos.
|
|
509
534
|
utxoCol = utxoCol.union(currentStoredUtxos).difference(utxosToRemoveCol)
|
|
510
535
|
|
|
536
|
+
if (this.#ordinalsEnabled) {
|
|
537
|
+
// copy ordinalsChange over
|
|
538
|
+
utxoCol.toArray().forEach((utxo) => {
|
|
539
|
+
const storedUtxo = currentStoredUtxos
|
|
540
|
+
.toArray()
|
|
541
|
+
.find((u2) => u2.txid === utxos.txid && u2.vout === utxo.vout)
|
|
542
|
+
utxo.ordinalsChange = storedUtxo?.ordinalsChange
|
|
543
|
+
})
|
|
544
|
+
}
|
|
545
|
+
|
|
511
546
|
for (let tx of Object.values(unconfirmedTxsToCheck)) {
|
|
512
547
|
existingTxs.push({ ...tx, dropped: true }) // TODO: this will decrease the chain index, it shouldn't be an issue considering the gap limit
|
|
513
548
|
utxoCol = utxoCol.difference(utxoCol.getTxIdUtxos(tx.txId))
|
|
@@ -540,9 +575,11 @@ export class BitcoinMonitorScanner {
|
|
|
540
575
|
return !isEqual(chain, originalChain.chain)
|
|
541
576
|
})
|
|
542
577
|
|
|
578
|
+
const ordinalAddress = await this.getOrdinalAddress({ walletAccount })
|
|
543
579
|
const utxosData = utxoCol
|
|
544
580
|
? partitionUtxos({
|
|
545
581
|
allUtxos: utxoCol,
|
|
582
|
+
ordinalsEnabled: this.#ordinalsEnabled,
|
|
546
583
|
ordinalAddress,
|
|
547
584
|
})
|
|
548
585
|
: {}
|
|
@@ -603,6 +640,7 @@ export class BitcoinMonitorScanner {
|
|
|
603
640
|
|
|
604
641
|
const { utxos, ordinalsUtxos } = partitionUtxos({
|
|
605
642
|
allUtxos: txConfirmedUtxos,
|
|
643
|
+
ordinalsEnabled: this.#ordinalsEnabled,
|
|
606
644
|
ordinalAddress: await this.getOrdinalAddress({ walletAccount }),
|
|
607
645
|
})
|
|
608
646
|
|
package/src/tx-send/index.js
CHANGED
|
@@ -14,7 +14,12 @@ import {
|
|
|
14
14
|
createOutput as dogecoinCreateOutput,
|
|
15
15
|
} from './dogecoin'
|
|
16
16
|
import { findUnconfirmedSentRbfTxs } from '../tx-utils'
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
getOrdinalsUtxos,
|
|
19
|
+
getTransferOrdinalsUtxos,
|
|
20
|
+
getUsableUtxos,
|
|
21
|
+
getUtxos,
|
|
22
|
+
} from '../utxos-utils'
|
|
18
23
|
|
|
19
24
|
import * as defaultBitcoinjsLib from '@exodus/bitcoinjs-lib'
|
|
20
25
|
|
|
@@ -132,6 +137,7 @@ export const createAndBroadcastTXFactory = ({
|
|
|
132
137
|
getFeeEstimator,
|
|
133
138
|
getSizeAndChangeScript = getSizeAndChangeScriptFactory(), // for decred customizations
|
|
134
139
|
allowUnconfirmedRbfEnabledUtxos,
|
|
140
|
+
ordinalsEnabled = false,
|
|
135
141
|
}) => async (
|
|
136
142
|
{ asset: maybeToken, walletAccount, address, amount: tokenAmount, options },
|
|
137
143
|
{ assetClientInterface }
|
|
@@ -167,6 +173,11 @@ export const createAndBroadcastTXFactory = ({
|
|
|
167
173
|
|
|
168
174
|
const inscriptionIds = nft?.tokenId ? [nft?.tokenId] : brc20 ? brc20.inscriptionIds : undefined
|
|
169
175
|
|
|
176
|
+
assert(
|
|
177
|
+
ordinalsEnabled || !inscriptionIds,
|
|
178
|
+
'inscriptions cannot be sent when ordinalsEnabled=false '
|
|
179
|
+
)
|
|
180
|
+
|
|
170
181
|
const shuffle = (list) => {
|
|
171
182
|
return inscriptionIds ? list : doShuffle(list) // don't shuffle when sending ordinal!!!!
|
|
172
183
|
}
|
|
@@ -192,26 +203,9 @@ export const createAndBroadcastTXFactory = ({
|
|
|
192
203
|
|
|
193
204
|
const currentOrdinalsUtxos = getOrdinalsUtxos({ accountState, asset })
|
|
194
205
|
const transferOrdinalsUtxos = inscriptionIds
|
|
195
|
-
?
|
|
206
|
+
? getTransferOrdinalsUtxos({ inscriptionIds, ordinalsUtxos: currentOrdinalsUtxos })
|
|
196
207
|
: undefined
|
|
197
208
|
|
|
198
|
-
if (inscriptionIds) {
|
|
199
|
-
assert(
|
|
200
|
-
transferOrdinalsUtxos?.size === inscriptionIds.length,
|
|
201
|
-
`Expected ordinal utxos ${inscriptionIds.length}. Found: ${transferOrdinalsUtxos?.size || 0}`
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
// const unconfirmedOrdinalUtxos = transferOrdinalsUtxos
|
|
205
|
-
// .toArray()
|
|
206
|
-
// .filter((ordinalUtxo) => !(ordinalUtxo.confirmations > 0))
|
|
207
|
-
// assert(
|
|
208
|
-
// !unconfirmedOrdinalUtxos.length,
|
|
209
|
-
// `OrdinalUtxo with inscription ids ${unconfirmedOrdinalUtxos
|
|
210
|
-
// .map((utxo) => utxo.inscriptionId)
|
|
211
|
-
// .join(', ')} have not confirmed yet`
|
|
212
|
-
// )
|
|
213
|
-
}
|
|
214
|
-
|
|
215
209
|
const insightClient = asset.baseAsset.insightClient
|
|
216
210
|
const currency = asset.currency
|
|
217
211
|
const feeData = await assetClientInterface.getFeeConfig({ assetName })
|
|
@@ -293,6 +287,7 @@ export const createAndBroadcastTXFactory = ({
|
|
|
293
287
|
UtxoCollection.fromJSON(replaceTx.data.inputs, { currency: asset.currency })
|
|
294
288
|
)
|
|
295
289
|
}
|
|
290
|
+
const addressPathsMap = selectedUtxos.getAddressPathsMap()
|
|
296
291
|
|
|
297
292
|
// transform UTXO object to raw
|
|
298
293
|
const inputs = shuffle(createInputs(assetName, selectedUtxos.toArray(), rbfEnabled))
|
|
@@ -341,6 +336,9 @@ export const createAndBroadcastTXFactory = ({
|
|
|
341
336
|
ourAddress.address ? ourAddress.address : ourAddress.toString(),
|
|
342
337
|
change
|
|
343
338
|
)
|
|
339
|
+
// Add the keypath of change address to support Trezor detect the change output.
|
|
340
|
+
// Output is change and does not need approval from user which shows the strange address that user never seen.
|
|
341
|
+
addressPathsMap[changeAddress] = ourAddress.meta.path
|
|
344
342
|
outputs.push(changeOutput)
|
|
345
343
|
} else {
|
|
346
344
|
// If we don't have enough for a change output, then all remaining dust is just added to fee
|
|
@@ -360,7 +358,7 @@ export const createAndBroadcastTXFactory = ({
|
|
|
360
358
|
},
|
|
361
359
|
txMeta: {
|
|
362
360
|
useCashAddress, // for trezor to show the receiver cash address
|
|
363
|
-
addressPathsMap
|
|
361
|
+
addressPathsMap,
|
|
364
362
|
blockHeight,
|
|
365
363
|
},
|
|
366
364
|
}
|
|
@@ -441,6 +439,7 @@ export const createAndBroadcastTXFactory = ({
|
|
|
441
439
|
value: change,
|
|
442
440
|
confirmations: 0,
|
|
443
441
|
rbfEnabled,
|
|
442
|
+
ordinalsChange: ordinalsEnabled ? true : undefined,
|
|
444
443
|
}
|
|
445
444
|
remainingUtxos = remainingUtxos.addUtxo(changeUtxo)
|
|
446
445
|
}
|
|
@@ -518,7 +517,16 @@ export const createAndBroadcastTXFactory = ({
|
|
|
518
517
|
inputs: selectedUtxos.toJSON(),
|
|
519
518
|
replacedTxId: replaceTx ? replaceTx.txId : undefined,
|
|
520
519
|
nftId: nft ? `${assetName}:${nft.tokenId}` : undefined, // it allows BE to load the nft info while the nft is in transit
|
|
521
|
-
|
|
520
|
+
inscriptionsIndexed: ordinalsEnabled ? true : undefined,
|
|
521
|
+
sentInscriptions: inscriptionIds
|
|
522
|
+
? inscriptionIds.map((inscriptionId) => {
|
|
523
|
+
return {
|
|
524
|
+
inscriptionId,
|
|
525
|
+
offset: 0,
|
|
526
|
+
value: 0,
|
|
527
|
+
}
|
|
528
|
+
})
|
|
529
|
+
: undefined,
|
|
522
530
|
},
|
|
523
531
|
},
|
|
524
532
|
],
|
package/src/utxos-utils.js
CHANGED
|
@@ -5,6 +5,46 @@ import assert from 'minimalistic-assert'
|
|
|
5
5
|
|
|
6
6
|
const MAX_ORDINAL_VALUE_POSTAGE = 10000
|
|
7
7
|
|
|
8
|
+
export function getTransferOrdinalsUtxos({ inscriptionIds, ordinalsUtxos }) {
|
|
9
|
+
const transferOrdinalsUtxos = ordinalsUtxos.filter((utxo) =>
|
|
10
|
+
utxo.inscriptions?.some((i) => inscriptionIds.includes(i.inscriptionId))
|
|
11
|
+
)
|
|
12
|
+
const unsafeInscriptions = transferOrdinalsUtxos.toArray().flatMap(
|
|
13
|
+
(utxo) =>
|
|
14
|
+
utxo.inscriptions?.filter((i) => {
|
|
15
|
+
const validInscription = isValidInscription({
|
|
16
|
+
value: utxo.value.toBaseNumber(),
|
|
17
|
+
offset: i.offset,
|
|
18
|
+
})
|
|
19
|
+
return validInscription && !inscriptionIds.includes(i.inscriptionId)
|
|
20
|
+
}) || []
|
|
21
|
+
)
|
|
22
|
+
assert(
|
|
23
|
+
!unsafeInscriptions.length,
|
|
24
|
+
`The following inscriptions are unsafe ${unsafeInscriptions.map(
|
|
25
|
+
(i) => i.inscriptionId
|
|
26
|
+
)} when ${inscriptionIds} should be spent`
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const transferInscriptionIds = transferOrdinalsUtxos
|
|
30
|
+
.toArray()
|
|
31
|
+
.flatMap((utxo) => utxo.inscriptions)
|
|
32
|
+
.filter(({ inscriptionId }) => inscriptionIds.includes(inscriptionId))
|
|
33
|
+
|
|
34
|
+
assert(
|
|
35
|
+
transferInscriptionIds.length === inscriptionIds.length,
|
|
36
|
+
`Expected inscriptions ${inscriptionIds.length}. Found: ${transferInscriptionIds.length}`
|
|
37
|
+
)
|
|
38
|
+
return transferOrdinalsUtxos
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isValidInscription({ value, offset }) {
|
|
42
|
+
assert(typeof value === 'number', 'value must be a number')
|
|
43
|
+
assert(typeof offset === 'number', 'offset must be a number')
|
|
44
|
+
// value >= 0 in case offset, alternatively convert to string/ln
|
|
45
|
+
return (value >= 0 && value <= MAX_ORDINAL_VALUE_POSTAGE) || offset === 0
|
|
46
|
+
}
|
|
47
|
+
|
|
8
48
|
export function getUtxos({ accountState, asset }) {
|
|
9
49
|
return (
|
|
10
50
|
accountState?.utxos ||
|
|
@@ -23,31 +63,36 @@ export function getOrdinalsUtxos({ accountState, asset }) {
|
|
|
23
63
|
)
|
|
24
64
|
}
|
|
25
65
|
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
66
|
+
export function getValidInscriptions({ utxo }) {
|
|
67
|
+
return (utxo.inscriptions || []).filter((i) =>
|
|
68
|
+
isValidInscription({ value: utxo.value.toBaseNumber(), offset: i.offset })
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isOrdinalUtxo({ utxo, ordinalsEnabled }) {
|
|
73
|
+
if (!ordinalsEnabled) {
|
|
32
74
|
return false
|
|
33
75
|
}
|
|
34
76
|
|
|
35
|
-
if (utxo.
|
|
36
|
-
return
|
|
77
|
+
if (utxo.ordinalsChange && !utxo.inscriptionsIndexed) {
|
|
78
|
+
return false // this allows users see and spend change balance after sending before hiro confirmation
|
|
37
79
|
}
|
|
38
80
|
|
|
39
|
-
if (utxo.
|
|
40
|
-
return
|
|
81
|
+
if (!utxo.inscriptionsIndexed) {
|
|
82
|
+
return true
|
|
41
83
|
}
|
|
42
84
|
|
|
43
|
-
|
|
85
|
+
const validInscriptions = getValidInscriptions({ utxo })
|
|
86
|
+
return validInscriptions.length > 0
|
|
44
87
|
}
|
|
45
88
|
|
|
46
|
-
export function partitionUtxos({ allUtxos,
|
|
89
|
+
export function partitionUtxos({ allUtxos, ordinalsEnabled }) {
|
|
90
|
+
assert(allUtxos, 'allUtxos is required')
|
|
47
91
|
assert(allUtxos, 'allUtxos is required')
|
|
92
|
+
// assert(ordinalAddress, 'ordinalAddress is required') // not used atm we may need to tune by ordinalAddress when unconfirmed or rubbish inscriptions
|
|
48
93
|
return {
|
|
49
|
-
utxos: allUtxos.filter((utxo) => !isOrdinalUtxo({ utxo,
|
|
50
|
-
ordinalsUtxos: allUtxos.filter((utxo) => isOrdinalUtxo({ utxo,
|
|
94
|
+
utxos: allUtxos.filter((utxo) => !isOrdinalUtxo({ utxo, ordinalsEnabled })),
|
|
95
|
+
ordinalsUtxos: allUtxos.filter((utxo) => isOrdinalUtxo({ utxo, ordinalsEnabled })),
|
|
51
96
|
}
|
|
52
97
|
}
|
|
53
98
|
|