@exodus/bitcoin-api 2.0.0 → 2.1.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/bitcoinjs-lib/ecc/index.native.js +3 -6
- package/src/bitcoinjs-lib/ecc/mobile.js +20 -10
- package/src/btc-like-keys.js +12 -12
- package/src/fee/can-bump-tx.js +0 -2
- package/src/fee/get-fee-resolver.js +2 -6
- package/src/tx-log/bitcoin-monitor-scanner.js +19 -9
- package/src/tx-send/index.js +5 -8
- package/src/utxos-utils.js +5 -17
- package/src/bitcoinjs-lib/ecc/mobile-schnorr.js +0 -58
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bitcoin-api",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Exodus bitcoin-api",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -40,5 +40,5 @@
|
|
|
40
40
|
"@exodus/bip-schnorr": "0.6.6-fork-1",
|
|
41
41
|
"@noble/secp256k1": "~1.5.3"
|
|
42
42
|
},
|
|
43
|
-
"gitHead": "
|
|
43
|
+
"gitHead": "667523b6a37cb6d9755082df52c812a86cda594d"
|
|
44
44
|
}
|
|
@@ -3,27 +3,37 @@ import secp256k1 from '@exodus/secp256k1'
|
|
|
3
3
|
// TODO: temp import until '@noble/secp256k1' can be used
|
|
4
4
|
import { isPoint } from 'tiny-secp256k1'
|
|
5
5
|
import { common, toPubKey } from './common'
|
|
6
|
+
import schnorr from '@exodus/bip-schnorr'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Wrapper around `secp256k1` in order to follow the bitcoinjs-lib `TinySecp256k1Interface`
|
|
9
|
-
* Schnorr signatures are offered by @
|
|
10
|
+
* Schnorr signatures are offered by @exodus/bip-schnorr
|
|
11
|
+
*
|
|
10
12
|
*/
|
|
13
|
+
|
|
11
14
|
export const mobileEcc: TinySecp256k1Interface = {
|
|
12
15
|
...common,
|
|
13
16
|
|
|
14
17
|
signAsync: async (h: Uint8Array, d: Uint8Array, extraEntropy?: Uint8Array): Uint8Array =>
|
|
15
18
|
secp256k1.ecdsaSign(h, d, { data: extraEntropy }).signature,
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
signSchnorr: (h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array =>
|
|
21
|
+
schnorr.sign(d.toString('hex'), h, e),
|
|
22
|
+
|
|
23
|
+
signSchnorrAsync: async (h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array =>
|
|
24
|
+
mobileEcc.signSchnorr(h, d, e),
|
|
25
|
+
|
|
26
|
+
verifySchnorr: (h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean => {
|
|
27
|
+
try {
|
|
28
|
+
schnorr.verify(Q, h, signature)
|
|
29
|
+
return true
|
|
30
|
+
} catch (e) {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
},
|
|
22
34
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
signSchnorr: null,
|
|
26
|
-
verifySchnorr: null,
|
|
35
|
+
verifySchnorrAsync: async (h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean =>
|
|
36
|
+
mobileEcc.verifySchnorr(h, Q, signature),
|
|
27
37
|
|
|
28
38
|
isPoint: (p: Uint8Array): boolean => {
|
|
29
39
|
try {
|
package/src/btc-like-keys.js
CHANGED
|
@@ -6,6 +6,17 @@ import assert from 'minimalistic-assert'
|
|
|
6
6
|
import { identity, pickBy } from 'lodash'
|
|
7
7
|
import * as defaultBitcoinjsLib from '@exodus/bitcoinjs-lib'
|
|
8
8
|
|
|
9
|
+
export const publicKeyToHashFactory = (p2pkh) => (publicKey) => {
|
|
10
|
+
const sha = createHash('sha256')
|
|
11
|
+
.update(publicKey)
|
|
12
|
+
.digest()
|
|
13
|
+
const pubKeyHash = createHash('rmd160')
|
|
14
|
+
.update(sha)
|
|
15
|
+
.digest()
|
|
16
|
+
const payload = Buffer.concat([Buffer.from([p2pkh]), pubKeyHash])
|
|
17
|
+
return bs58check.encode(payload)
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
export const createBtcLikeKeys = ({
|
|
10
21
|
coinInfo,
|
|
11
22
|
versions,
|
|
@@ -32,18 +43,7 @@ export const createBtcLikeKeys = ({
|
|
|
32
43
|
((privateKey, compressed = true) => {
|
|
33
44
|
return wif.encode(coinInfo.versions.private, privateKey, compressed)
|
|
34
45
|
})
|
|
35
|
-
const encodePublicPurpose44 =
|
|
36
|
-
encodePublicCustom ||
|
|
37
|
-
((publicKey) => {
|
|
38
|
-
const sha = createHash('sha256')
|
|
39
|
-
.update(publicKey)
|
|
40
|
-
.digest()
|
|
41
|
-
const pubKeyHash = createHash('rmd160')
|
|
42
|
-
.update(sha)
|
|
43
|
-
.digest()
|
|
44
|
-
const payload = Buffer.concat([Buffer.from([versions.p2pkh]), pubKeyHash])
|
|
45
|
-
return bs58check.encode(payload)
|
|
46
|
-
})
|
|
46
|
+
const encodePublicPurpose44 = encodePublicCustom || publicKeyToHashFactory(versions.p2pkh)
|
|
47
47
|
const encodePublicFromWIF =
|
|
48
48
|
encodePublicFromWIFCustom ||
|
|
49
49
|
((privateKeyWIF) => {
|
package/src/fee/can-bump-tx.js
CHANGED
|
@@ -15,7 +15,6 @@ const _canBumpTx = ({
|
|
|
15
15
|
accountState,
|
|
16
16
|
feeData,
|
|
17
17
|
getFeeEstimator,
|
|
18
|
-
taprootEnabled,
|
|
19
18
|
allowUnconfirmedRbfEnabledUtxos,
|
|
20
19
|
}) => {
|
|
21
20
|
assert(asset, 'asset must be provided')
|
|
@@ -58,7 +57,6 @@ const _canBumpTx = ({
|
|
|
58
57
|
utxos,
|
|
59
58
|
feeData,
|
|
60
59
|
txSet,
|
|
61
|
-
taprootEnabled,
|
|
62
60
|
allowUnconfirmedRbfEnabledUtxos,
|
|
63
61
|
})
|
|
64
62
|
if (usableUtxos.value.isZero) return { errorMessage: 'insufficient funds' }
|
|
@@ -6,13 +6,11 @@ import { canBumpTx } from './can-bump-tx'
|
|
|
6
6
|
|
|
7
7
|
export class GetFeeResolver {
|
|
8
8
|
#getFeeEstimator
|
|
9
|
-
#taprootEnabled
|
|
10
9
|
#allowUnconfirmedRbfEnabledUtxos
|
|
11
10
|
|
|
12
|
-
constructor({ getFeeEstimator,
|
|
11
|
+
constructor({ getFeeEstimator, allowUnconfirmedRbfEnabledUtxos }) {
|
|
13
12
|
assert(getFeeEstimator, 'getFeeEstimator must be provided')
|
|
14
13
|
this.#getFeeEstimator = (asset, { feePerKB }) => getFeeEstimator(asset, feePerKB)
|
|
15
|
-
this.#taprootEnabled = taprootEnabled
|
|
16
14
|
this.#allowUnconfirmedRbfEnabledUtxos = allowUnconfirmedRbfEnabledUtxos
|
|
17
15
|
}
|
|
18
16
|
|
|
@@ -48,7 +46,6 @@ export class GetFeeResolver {
|
|
|
48
46
|
utxos,
|
|
49
47
|
feeData,
|
|
50
48
|
txSet,
|
|
51
|
-
taprootEnabled: this.#taprootEnabled,
|
|
52
49
|
allowUnconfirmedRbfEnabledUtxos: this.#allowUnconfirmedRbfEnabledUtxos,
|
|
53
50
|
})
|
|
54
51
|
return spendableUtxos.value
|
|
@@ -67,7 +64,6 @@ export class GetFeeResolver {
|
|
|
67
64
|
utxos,
|
|
68
65
|
feeData,
|
|
69
66
|
txSet,
|
|
70
|
-
taprootEnabled: this.#taprootEnabled,
|
|
71
67
|
})
|
|
72
68
|
const replaceableTxs = findUnconfirmedSentRbfTxs(txSet)
|
|
73
69
|
|
|
@@ -97,7 +93,7 @@ export class GetFeeResolver {
|
|
|
97
93
|
accountState,
|
|
98
94
|
feeData,
|
|
99
95
|
getFeeEstimator: this.#getFeeEstimator,
|
|
100
|
-
|
|
96
|
+
|
|
101
97
|
allowUnconfirmedRbfEnabledUtxos: this.#allowUnconfirmedRbfEnabledUtxos,
|
|
102
98
|
})
|
|
103
99
|
}
|
|
@@ -144,7 +144,17 @@ export class BitcoinMonitorScanner {
|
|
|
144
144
|
chainIndex,
|
|
145
145
|
addressIndex,
|
|
146
146
|
})
|
|
147
|
-
.then((address) =>
|
|
147
|
+
.then((address) => {
|
|
148
|
+
return {
|
|
149
|
+
address: this.#asset.address.toLegacyAddress
|
|
150
|
+
? Address.create(
|
|
151
|
+
this.#asset.address.toLegacyAddress(String(address)),
|
|
152
|
+
address.meta
|
|
153
|
+
)
|
|
154
|
+
: address,
|
|
155
|
+
purpose,
|
|
156
|
+
}
|
|
157
|
+
})
|
|
148
158
|
)
|
|
149
159
|
}
|
|
150
160
|
}
|
|
@@ -365,15 +375,17 @@ export class BitcoinMonitorScanner {
|
|
|
365
375
|
// this is an array because legacy multisig has multiple addresses
|
|
366
376
|
if (!Array.isArray(vout.scriptPubKey.addresses)) return
|
|
367
377
|
if (vout.scriptPubKey.addresses.length === 0) return
|
|
368
|
-
|
|
378
|
+
const sentAddress = vout.scriptPubKey.addresses[0]
|
|
379
|
+
if (!addrMap[sentAddress]) {
|
|
369
380
|
if (isSent && !txLogItem.to) {
|
|
370
381
|
const val = currency.defaultUnit(vout.value)
|
|
371
|
-
|
|
382
|
+
const sentDisplayAddress = asset.address.displayAddress?.(sentAddress) || sentAddress
|
|
383
|
+
txLogItem.data.sent.push({ address: sentDisplayAddress, amount: val })
|
|
372
384
|
}
|
|
373
385
|
return
|
|
374
386
|
}
|
|
375
387
|
|
|
376
|
-
const address = addrMap[
|
|
388
|
+
const address = addrMap[sentAddress]
|
|
377
389
|
if (isReceiveAddress(address)) {
|
|
378
390
|
txLogItem.addresses.push(address)
|
|
379
391
|
}
|
|
@@ -403,13 +415,11 @@ export class BitcoinMonitorScanner {
|
|
|
403
415
|
utxos.push(output) // but save the unspent ones for state.utxos
|
|
404
416
|
})
|
|
405
417
|
|
|
406
|
-
|
|
407
|
-
if (assetName === 'bcash') {
|
|
418
|
+
if (this.#asset.address.displayAddress) {
|
|
408
419
|
if (txLogItem.to) {
|
|
409
|
-
txLogItem.to = asset.address.
|
|
420
|
+
txLogItem.to = asset.address.displayAddress(txLogItem.to)
|
|
410
421
|
}
|
|
411
|
-
|
|
412
|
-
from = from.map(asset.address.toCashAddress)
|
|
422
|
+
from = from.map(asset.address.displayAddress)
|
|
413
423
|
}
|
|
414
424
|
|
|
415
425
|
if (isSent) {
|
package/src/tx-send/index.js
CHANGED
|
@@ -84,7 +84,7 @@ export async function getNonWitnessTxs(asset, utxos, insightClient) {
|
|
|
84
84
|
|
|
85
85
|
export const createAndBroadcastTXFactory = ({
|
|
86
86
|
getFeeEstimator,
|
|
87
|
-
|
|
87
|
+
|
|
88
88
|
allowUnconfirmedRbfEnabledUtxos,
|
|
89
89
|
}) => async ({ asset, walletAccount, address, amount, options }, { assetClientInterface }) => {
|
|
90
90
|
const {
|
|
@@ -126,7 +126,6 @@ export const createAndBroadcastTXFactory = ({
|
|
|
126
126
|
utxos: getUtxos({ accountState, asset }),
|
|
127
127
|
feeData,
|
|
128
128
|
txSet,
|
|
129
|
-
taprootEnabled,
|
|
130
129
|
})
|
|
131
130
|
|
|
132
131
|
let replaceableTxs = findUnconfirmedSentRbfTxs(txSet)
|
|
@@ -339,16 +338,15 @@ export const createAndBroadcastTXFactory = ({
|
|
|
339
338
|
? !replaceTx
|
|
340
339
|
: receiveAddresses.some((receiveAddress) => String(receiveAddress) === String(address))
|
|
341
340
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
341
|
+
const displayReceiveAddress = asset.address.displayAddress?.(receiveAddress) || receiveAddress
|
|
342
|
+
|
|
345
343
|
const receivers = bumpTxId
|
|
346
344
|
? replaceTx
|
|
347
345
|
? replaceTx.data.sent
|
|
348
346
|
: []
|
|
349
347
|
: replaceTx
|
|
350
|
-
? replaceTx.data.sent.concat([{ address:
|
|
351
|
-
: [{ address:
|
|
348
|
+
? replaceTx.data.sent.concat([{ address: displayReceiveAddress, amount }])
|
|
349
|
+
: [{ address: displayReceiveAddress, amount }]
|
|
352
350
|
|
|
353
351
|
await assetClientInterface.updateTxLogAndNotify({
|
|
354
352
|
assetName,
|
|
@@ -366,7 +364,6 @@ export const createAndBroadcastTXFactory = ({
|
|
|
366
364
|
feePerKB: ['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)
|
|
367
365
|
? fee.div(tx.virtualSize() / 1000).toBaseNumber()
|
|
368
366
|
: undefined,
|
|
369
|
-
additionalTo: bumpTxId ? undefined : [{ address: receiveAddress, amount }],
|
|
370
367
|
changeAddress: changeOutput ? ourAddress : undefined,
|
|
371
368
|
blockHeight,
|
|
372
369
|
blocksSeen: 0,
|
package/src/utxos-utils.js
CHANGED
|
@@ -12,11 +12,7 @@ export function getUtxos({ accountState, asset }) {
|
|
|
12
12
|
)
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export const getBalancesFactory = ({
|
|
16
|
-
taprootEnabled,
|
|
17
|
-
feeData,
|
|
18
|
-
allowUnconfirmedRbfEnabledUtxos,
|
|
19
|
-
}) => {
|
|
15
|
+
export const getBalancesFactory = ({ feeData, allowUnconfirmedRbfEnabledUtxos }) => {
|
|
20
16
|
assert(feeData, 'feeData is required')
|
|
21
17
|
return ({ asset, accountState, txLog }) => {
|
|
22
18
|
assert(asset, 'asset is required')
|
|
@@ -29,15 +25,13 @@ export const getBalancesFactory = ({
|
|
|
29
25
|
utxos,
|
|
30
26
|
txSet: txLog,
|
|
31
27
|
feeData,
|
|
32
|
-
|
|
28
|
+
|
|
33
29
|
allowUnconfirmedRbfEnabledUtxos,
|
|
34
30
|
}).value
|
|
35
31
|
return { balance, spendableBalance }
|
|
36
32
|
}
|
|
37
33
|
}
|
|
38
34
|
|
|
39
|
-
const isTaprootUtxo = ({ utxo }) => String(utxo.address).length === 62
|
|
40
|
-
|
|
41
35
|
export function getConfirmedUtxos({ asset, utxos }) {
|
|
42
36
|
assert(asset, 'asset is required')
|
|
43
37
|
assert(utxos, 'utxos is required')
|
|
@@ -65,18 +59,12 @@ export function getConfirmedOrRfbDisabledUtxos({ asset, utxos, allowUnconfirmedR
|
|
|
65
59
|
)
|
|
66
60
|
}
|
|
67
61
|
|
|
68
|
-
export function getUsableUtxos({ asset, utxos, feeData, txSet
|
|
62
|
+
export function getUsableUtxos({ asset, utxos, feeData, txSet }) {
|
|
69
63
|
assert(asset, 'asset is required')
|
|
70
64
|
assert(utxos, 'utxos is required')
|
|
71
65
|
assert(feeData, 'feeData is required')
|
|
72
66
|
assert(txSet, 'txSet is required')
|
|
73
67
|
if (!['bitcoin', 'bitcointestnet', 'bitcoinregtest'].includes(asset.name)) return utxos
|
|
74
|
-
if (!taprootEnabled) {
|
|
75
|
-
utxos = UtxoCollection.fromArray(
|
|
76
|
-
utxos.toArray().filter((utxo) => !isTaprootUtxo({ utxo })),
|
|
77
|
-
{ currency: asset.currency }
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
68
|
const { fastestFee } = feeData
|
|
81
69
|
const feeRate = fastestFee.toBaseNumber()
|
|
82
70
|
const maxFee = feeData.maxExtraCpfpFee
|
|
@@ -101,10 +89,10 @@ export function getSpendableUtxos({
|
|
|
101
89
|
utxos,
|
|
102
90
|
feeData,
|
|
103
91
|
txSet,
|
|
104
|
-
|
|
92
|
+
|
|
105
93
|
allowUnconfirmedRbfEnabledUtxos,
|
|
106
94
|
}) {
|
|
107
|
-
const usableUtxos = getUsableUtxos({ asset, utxos, feeData, txSet
|
|
95
|
+
const usableUtxos = getUsableUtxos({ asset, utxos, feeData, txSet })
|
|
108
96
|
return getConfirmedOrRfbDisabledUtxos({
|
|
109
97
|
asset,
|
|
110
98
|
utxos: usableUtxos,
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { TinySecp256k1Interface } from '@exodus/bitcoinjs-lib'
|
|
2
|
-
import secp256k1 from '@exodus/secp256k1'
|
|
3
|
-
// TODO: temp import until '@noble/secp256k1' can be used
|
|
4
|
-
import { isPoint } from 'tiny-secp256k1'
|
|
5
|
-
import { common, toPubKey } from './common'
|
|
6
|
-
import schnorr from '@exodus/bip-schnorr'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Wrapper around `secp256k1` in order to follow the bitcoinjs-lib `TinySecp256k1Interface`
|
|
10
|
-
* Schnorr signatures are offered by @exodus/bip-schnorr
|
|
11
|
-
*
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export const mobileSchnorrEcc: TinySecp256k1Interface = {
|
|
15
|
-
...common,
|
|
16
|
-
|
|
17
|
-
signAsync: async (h: Uint8Array, d: Uint8Array, extraEntropy?: Uint8Array): Uint8Array =>
|
|
18
|
-
secp256k1.ecdsaSign(h, d, { data: extraEntropy }).signature,
|
|
19
|
-
|
|
20
|
-
signSchnorr: (h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array =>
|
|
21
|
-
schnorr.sign(d.toString('hex'), h, e),
|
|
22
|
-
|
|
23
|
-
signSchnorrAsync: async (h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array =>
|
|
24
|
-
mobileSchnorrEcc.signSchnorr(h, d, e),
|
|
25
|
-
|
|
26
|
-
verifySchnorr: (h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean => {
|
|
27
|
-
try {
|
|
28
|
-
schnorr.verify(Q, h, signature)
|
|
29
|
-
return true
|
|
30
|
-
} catch (e) {
|
|
31
|
-
return false
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
|
|
35
|
-
verifySchnorrAsync: async (h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean =>
|
|
36
|
-
mobileSchnorrEcc.verifySchnorr(h, Q, signature),
|
|
37
|
-
|
|
38
|
-
isPoint: (p: Uint8Array): boolean => {
|
|
39
|
-
try {
|
|
40
|
-
// temp solution secp256k1 does not actually verify the value range, only the data length
|
|
41
|
-
return isPoint(Buffer.from(p))
|
|
42
|
-
} catch (err) {
|
|
43
|
-
return false
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
isXOnlyPoint: (p: Uint8Array): boolean => {
|
|
48
|
-
try {
|
|
49
|
-
// temp solution secp256k1 does not actually verify the value range, only the data length
|
|
50
|
-
return isPoint(Buffer.from(toPubKey(p)))
|
|
51
|
-
} catch (err) {
|
|
52
|
-
return false
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
pointCompress: (p: Uint8Array, compressed?: boolean): Uint8Array =>
|
|
57
|
-
secp256k1.publicKeyConvert(p, compressed),
|
|
58
|
-
}
|