@exodus/bitcoin-api 1.0.0-alpha.5 → 1.0.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 +4 -2
- package/src/bitcoinjs-lib/ecc/common.js +44 -0
- package/src/bitcoinjs-lib/ecc/desktop.js +49 -0
- package/src/bitcoinjs-lib/ecc/index.js +2 -81
- package/src/bitcoinjs-lib/ecc/mobile.js +48 -0
- package/src/bitcoinjs-lib/index.js +2 -4
- package/src/bitcoinjs-lib/script-classify/index.js +4 -3
- package/src/btc-like-address.js +2 -2
- package/src/btc-like-keys.js +4 -4
- package/src/fee/can-bump-tx.js +7 -7
- package/src/fee/fee-estimator.js +9 -6
- package/src/fee/get-fee-resolver.js +35 -32
- package/src/fee/index.js +1 -1
- package/src/fee/utxo-selector.js +49 -56
- package/src/tx-send/index.js +14 -14
- package/src/tx-sign/default-create-tx.js +24 -11
- package/src/tx-sign/taproot.js +49 -0
- package/src/utxos-utils.js +18 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/bitcoin-api",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Exodus bitcoin-api",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -33,5 +33,7 @@
|
|
|
33
33
|
"socket.io-client": "2.1.1",
|
|
34
34
|
"url-join": "4.0.0"
|
|
35
35
|
},
|
|
36
|
-
"
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@noble/secp256k1": "~1.5.3"
|
|
38
|
+
}
|
|
37
39
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import secp256k1 from '@exodus/secp256k1'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Common ecc functions between mobile and desktop. Once mobile accepts @noble/secp256k1, we can unify both
|
|
5
|
+
*/
|
|
6
|
+
export const common = {
|
|
7
|
+
// These methods have been addded in order to comply with the public interface
|
|
8
|
+
// In practice the async version will be used
|
|
9
|
+
sign: (h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array =>
|
|
10
|
+
secp256k1.ecdsaSign(h, d, { data: e }).signature,
|
|
11
|
+
verify: (h: Uint8Array, Q: Uint8Array, signature: Uint8Array, strict?: boolean): boolean =>
|
|
12
|
+
secp256k1.ecdsaVerify(signature, h, Q),
|
|
13
|
+
|
|
14
|
+
privateAdd: (d: Uint8Array, tweak: Uint8Array): Uint8Array | null =>
|
|
15
|
+
// cloning input. secp256k1 modifies it and it cannot be reused/cached
|
|
16
|
+
secp256k1.privateKeyTweakAdd(Buffer.from(d), tweak),
|
|
17
|
+
|
|
18
|
+
privateNegate: (d: Uint8Array): Uint8Array =>
|
|
19
|
+
// cloning input. secp256k1 modifies it and it cannot be reused/cached
|
|
20
|
+
secp256k1.privateKeyNegate(Buffer.from(d)),
|
|
21
|
+
|
|
22
|
+
xOnlyPointAddTweak: (p: Uint8Array, tweak: Uint8Array) => {
|
|
23
|
+
try {
|
|
24
|
+
const t = secp256k1.publicKeyTweakAdd(toPubKey(p), tweak)
|
|
25
|
+
return {
|
|
26
|
+
parity: t[0] === 0x02 ? 0 : 1,
|
|
27
|
+
xOnlyPubkey: t.slice(1, 33),
|
|
28
|
+
}
|
|
29
|
+
} catch (err) {
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
isPrivate: (d: Uint8Array): boolean => secp256k1.privateKeyVerify(d),
|
|
35
|
+
pointFromScalar: (d: Uint8Array, compressed?: boolean): Uint8Array | null =>
|
|
36
|
+
secp256k1.publicKeyCreate(d, compressed),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const toPubKey = (xOnly: Uint8Array) => {
|
|
40
|
+
const p = new Uint8Array(33)
|
|
41
|
+
p.set([0x02])
|
|
42
|
+
p.set(xOnly, 1)
|
|
43
|
+
return p
|
|
44
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { TinySecp256k1Interface } from '@exodus/bitcoinjs-lib'
|
|
2
|
+
import { Point, schnorr, sign } from '@noble/secp256k1'
|
|
3
|
+
import { common, toPubKey } from './common'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wrapper around `secp256k1` in order to follow the bitcoinjs-lib `TinySecp256k1Interface`
|
|
7
|
+
* Schnorr signatures are offered by @noble/secp256k1
|
|
8
|
+
*/
|
|
9
|
+
export const desktopEcc: TinySecp256k1Interface = {
|
|
10
|
+
...common,
|
|
11
|
+
|
|
12
|
+
signAsync: async (h: Uint8Array, d: Uint8Array, extraEntropy?: Uint8Array): Uint8Array =>
|
|
13
|
+
sign(h, d, {
|
|
14
|
+
extraEntropy,
|
|
15
|
+
canonical: true,
|
|
16
|
+
der: false,
|
|
17
|
+
}),
|
|
18
|
+
|
|
19
|
+
signSchnorrAsync: async (h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array =>
|
|
20
|
+
schnorr.sign(h, d, e),
|
|
21
|
+
verifySchnorrAsync: async (h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean =>
|
|
22
|
+
schnorr.verify(signature, h, Q),
|
|
23
|
+
|
|
24
|
+
// The underlying library does not expose sync functions for Schnorr sign and verify.
|
|
25
|
+
// These function are explicitly defined here as `null` for documentation purposes.
|
|
26
|
+
signSchnorr: null,
|
|
27
|
+
verifySchnorr: null,
|
|
28
|
+
|
|
29
|
+
isPoint: (p: Uint8Array): boolean => {
|
|
30
|
+
try {
|
|
31
|
+
Point.fromHex(p).assertValidity()
|
|
32
|
+
return true
|
|
33
|
+
} catch {
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
isXOnlyPoint: (p: Uint8Array): boolean => {
|
|
39
|
+
try {
|
|
40
|
+
Point.fromHex(toPubKey(p)).assertValidity()
|
|
41
|
+
return true
|
|
42
|
+
} catch {
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
pointCompress: (p: Uint8Array, compressed?: boolean): Uint8Array =>
|
|
48
|
+
Point.fromHex(p).toRawBytes(compressed),
|
|
49
|
+
}
|
|
@@ -1,81 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import tinySecp256k1 from 'tiny-secp256k1'
|
|
4
|
-
import secp256k1 from '@exodus/secp256k1'
|
|
5
|
-
|
|
6
|
-
// TODO: migrate library
|
|
7
|
-
// import { sign, schnorr, Point } from '@noble/secp256k1'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Wrapper around `secp256k1` in order to follow the bitcoinjs-lib `TinySecp256k1Interface`
|
|
11
|
-
* Schnorr signatures are offered by @noble/secp256k1
|
|
12
|
-
*/
|
|
13
|
-
export const ecc: TinySecp256k1Interface = {
|
|
14
|
-
// These methods have been addded in order to comply with the public interface
|
|
15
|
-
// In practice the async version will be used
|
|
16
|
-
sign: (h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array =>
|
|
17
|
-
secp256k1.ecdsaSign(h, d, { data: e }).signature,
|
|
18
|
-
verify: (h: Uint8Array, Q: Uint8Array, signature: Uint8Array, strict?: boolean): boolean =>
|
|
19
|
-
secp256k1.ecdsaVerify(signature, h, Q),
|
|
20
|
-
|
|
21
|
-
signAsync: async (h: Uint8Array, d: Uint8Array, extraEntropy?: Uint8Array): Uint8Array =>
|
|
22
|
-
secp256k1.ecdsaSign(h, d, { data: extraEntropy }).signature,
|
|
23
|
-
|
|
24
|
-
// TODO: waiting for the '@noble/secp256k1' lib
|
|
25
|
-
// signSchnorrAsync: async (h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array =>
|
|
26
|
-
// schnorr.sign(h, d, e),
|
|
27
|
-
// verifySchnorrAsync: async (h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean =>
|
|
28
|
-
// schnorr.verify(signature, h, Q),
|
|
29
|
-
|
|
30
|
-
// The underlying library does not expose sync functions for Schnorr sign and verify.
|
|
31
|
-
// These function are explicitly defined here as `null` for documentation purposes.
|
|
32
|
-
signSchnorr: null,
|
|
33
|
-
verifySchnorr: null,
|
|
34
|
-
|
|
35
|
-
isPoint: (p: Uint8Array): boolean => {
|
|
36
|
-
try {
|
|
37
|
-
// temp solution secp256k1 does not actually verify the value range, only the data length
|
|
38
|
-
return tinySecp256k1.isPoint(Buffer.from(p))
|
|
39
|
-
} catch {
|
|
40
|
-
return false
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
|
|
44
|
-
isXOnlyPoint: (p: Uint8Array): boolean => {
|
|
45
|
-
try {
|
|
46
|
-
// temp solution secp256k1 does not actually verify the value range, only the data length
|
|
47
|
-
return tinySecp256k1.isPoint(Buffer.from(toPubKey(p)))
|
|
48
|
-
} catch (err) {
|
|
49
|
-
return false
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
xOnlyPointAddTweak: (p: Uint8Array, tweak: Uint8Array) => {
|
|
53
|
-
try {
|
|
54
|
-
const t = secp256k1.publicKeyTweakAdd(toPubKey(p), tweak)
|
|
55
|
-
return {
|
|
56
|
-
parity: t[0] === 0x02 ? 0 : 1,
|
|
57
|
-
xOnlyPubkey: t.slice(1, 33),
|
|
58
|
-
}
|
|
59
|
-
} catch (err) {
|
|
60
|
-
return null
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
|
|
64
|
-
privateAdd: (d: Uint8Array, tweak: Uint8Array): Uint8Array | null =>
|
|
65
|
-
secp256k1.privateKeyTweakAdd(d, tweak),
|
|
66
|
-
|
|
67
|
-
privateNegate: (d: Uint8Array): Uint8Array => secp256k1.privateKeyNegate(d),
|
|
68
|
-
|
|
69
|
-
pointCompress: (p: Uint8Array, compressed?: boolean): Uint8Array =>
|
|
70
|
-
secp256k1.publicKeyConvert(p, compressed),
|
|
71
|
-
isPrivate: (d: Uint8Array): boolean => secp256k1.privateKeyVerify(d),
|
|
72
|
-
pointFromScalar: (d: Uint8Array, compressed?: boolean): Uint8Array | null =>
|
|
73
|
-
secp256k1.publicKeyCreate(d, compressed),
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const toPubKey = (xOnly: Uint8Array) => {
|
|
77
|
-
const p = new Uint8Array(33)
|
|
78
|
-
p.set([0x02])
|
|
79
|
-
p.set(xOnly, 1)
|
|
80
|
-
return p
|
|
81
|
-
}
|
|
1
|
+
export const eccFactory = (useDesktopEcc) =>
|
|
2
|
+
useDesktopEcc ? require('./desktop').desktopEcc : require('./mobile').mobileEcc
|
|
@@ -0,0 +1,48 @@
|
|
|
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 tinySecp256k1 from 'tiny-secp256k1'
|
|
5
|
+
import { common, toPubKey } from './common'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Wrapper around `secp256k1` in order to follow the bitcoinjs-lib `TinySecp256k1Interface`
|
|
9
|
+
* Schnorr signatures are offered by @noble/secp256k1
|
|
10
|
+
*/
|
|
11
|
+
export const mobileEcc: TinySecp256k1Interface = {
|
|
12
|
+
...common,
|
|
13
|
+
|
|
14
|
+
signAsync: async (h: Uint8Array, d: Uint8Array, extraEntropy?: Uint8Array): Uint8Array =>
|
|
15
|
+
secp256k1.ecdsaSign(h, d, { data: extraEntropy }).signature,
|
|
16
|
+
|
|
17
|
+
// TODO: waiting for the '@noble/secp256k1' lib
|
|
18
|
+
// signSchnorrAsync: async (h: Uint8Array, d: Uint8Array, e?: Uint8Array): Uint8Array =>
|
|
19
|
+
// schnorr.sign(h, d, e),
|
|
20
|
+
// verifySchnorrAsync: async (h: Uint8Array, Q: Uint8Array, signature: Uint8Array): boolean =>
|
|
21
|
+
// schnorr.verify(signature, h, Q),
|
|
22
|
+
|
|
23
|
+
// The underlying library does not expose sync functions for Schnorr sign and verify.
|
|
24
|
+
// These function are explicitly defined here as `null` for documentation purposes.
|
|
25
|
+
signSchnorr: null,
|
|
26
|
+
verifySchnorr: null,
|
|
27
|
+
|
|
28
|
+
isPoint: (p: Uint8Array): boolean => {
|
|
29
|
+
try {
|
|
30
|
+
// temp solution secp256k1 does not actually verify the value range, only the data length
|
|
31
|
+
return tinySecp256k1.isPoint(Buffer.from(p))
|
|
32
|
+
} catch {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
isXOnlyPoint: (p: Uint8Array): boolean => {
|
|
38
|
+
try {
|
|
39
|
+
// temp solution secp256k1 does not actually verify the value range, only the data length
|
|
40
|
+
return tinySecp256k1.isPoint(Buffer.from(toPubKey(p)))
|
|
41
|
+
} catch (err) {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
pointCompress: (p: Uint8Array, compressed?: boolean): Uint8Array =>
|
|
47
|
+
secp256k1.publicKeyConvert(p, compressed),
|
|
48
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { payments } from '@exodus/bitcoinjs-lib'
|
|
2
|
-
import
|
|
2
|
+
import assert from 'minimalistic-assert'
|
|
3
3
|
|
|
4
4
|
function isPaymentFactory(payment: any): (script: Buffer, eccLib?: any) => boolean {
|
|
5
5
|
return (script: Buffer, eccLib?: any): boolean => {
|
|
@@ -31,7 +31,8 @@ const types = {
|
|
|
31
31
|
NONSTANDARD: 'nonstandard',
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
const
|
|
34
|
+
const outputFactory = ({ ecc }) => (script: Buffer) => {
|
|
35
|
+
assert(ecc, 'ecc is required')
|
|
35
36
|
if (isP2WPKH(script)) return types.P2WPKH
|
|
36
37
|
if (isP2TR(script, ecc)) return types.P2TR
|
|
37
38
|
if (isP2PKH(script)) return types.P2PKH
|
|
@@ -45,5 +46,5 @@ const clasifyOutput = (script: Buffer) => {
|
|
|
45
46
|
|
|
46
47
|
export const scriptClassify = {
|
|
47
48
|
types,
|
|
48
|
-
|
|
49
|
+
outputFactory,
|
|
49
50
|
}
|
package/src/btc-like-address.js
CHANGED
|
@@ -2,19 +2,19 @@ import bs58check from 'bs58check'
|
|
|
2
2
|
import bech32 from 'bech32'
|
|
3
3
|
import assert from 'minimalistic-assert'
|
|
4
4
|
import { identity, pickBy } from 'lodash'
|
|
5
|
-
import { ecc as defaultEcc } from './bitcoinjs-lib'
|
|
6
5
|
|
|
7
6
|
export const createBtcLikeAddress = ({
|
|
8
7
|
versions,
|
|
9
8
|
coinInfo,
|
|
10
9
|
bitcoinjsLib,
|
|
11
|
-
ecc
|
|
10
|
+
ecc,
|
|
12
11
|
useBip86 = false,
|
|
13
12
|
validateFunctions = {},
|
|
14
13
|
extraFunctions = {},
|
|
15
14
|
}) => {
|
|
16
15
|
assert(versions, 'versions is required')
|
|
17
16
|
assert(coinInfo, 'coinInfo is required')
|
|
17
|
+
assert(ecc, 'ecc is required')
|
|
18
18
|
|
|
19
19
|
const bs58validateFactory = (version) =>
|
|
20
20
|
version === undefined
|
package/src/btc-like-keys.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import bs58check from 'bs58check'
|
|
2
|
-
import secp256k1 from 'tiny-secp256k1'
|
|
3
2
|
import wif from 'wif'
|
|
4
3
|
import createHash from 'create-hash'
|
|
5
4
|
import bech32 from 'bech32'
|
|
6
5
|
import assert from 'minimalistic-assert'
|
|
7
6
|
import { identity, pickBy } from 'lodash'
|
|
8
7
|
import * as defaultBitcoinjsLib from '@exodus/bitcoinjs-lib'
|
|
9
|
-
import { ecc } from './bitcoinjs-lib'
|
|
10
8
|
|
|
11
9
|
export const createBtcLikeKeys = ({
|
|
12
10
|
coinInfo,
|
|
13
11
|
versions,
|
|
12
|
+
ecc,
|
|
14
13
|
useBip86 = false,
|
|
15
14
|
bitcoinjsLib = defaultBitcoinjsLib,
|
|
16
15
|
extraFunctions = {},
|
|
17
16
|
}) => {
|
|
18
17
|
assert(coinInfo, 'coinInfo is required')
|
|
19
18
|
assert(versions, 'versions is required')
|
|
19
|
+
assert(ecc, 'ecc is required')
|
|
20
20
|
const {
|
|
21
21
|
encodePrivate: encodePrivateCustom,
|
|
22
22
|
encodePublic: encodePublicCustom,
|
|
@@ -48,7 +48,7 @@ export const createBtcLikeKeys = ({
|
|
|
48
48
|
encodePublicFromWIFCustom ||
|
|
49
49
|
((privateKeyWIF) => {
|
|
50
50
|
const { privateKey, compressed } = wif.decode(privateKeyWIF, coinInfo.versions.private)
|
|
51
|
-
const publicKey =
|
|
51
|
+
const publicKey = ecc.pointFromScalar(privateKey, compressed)
|
|
52
52
|
return encodePublicPurpose44(publicKey)
|
|
53
53
|
})
|
|
54
54
|
const encodePublicBech32 =
|
|
@@ -74,7 +74,7 @@ export const createBtcLikeKeys = ({
|
|
|
74
74
|
// NOTE: No password support here
|
|
75
75
|
const { versions } = coinInfo
|
|
76
76
|
const { privateKey, compressed } = wif.decode(privateKeyWIF, versions.private)
|
|
77
|
-
const publicKey =
|
|
77
|
+
const publicKey = ecc.pointFromScalar(privateKey, compressed)
|
|
78
78
|
return encodePublicBech32(publicKey)
|
|
79
79
|
}
|
|
80
80
|
: undefined
|
package/src/fee/can-bump-tx.js
CHANGED
|
@@ -50,21 +50,21 @@ const _canBumpTx = ({
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
const
|
|
53
|
+
const utxos = getUtxos({ accountState, asset })
|
|
54
54
|
|
|
55
|
-
const
|
|
55
|
+
const spendableUtxos = getSpendableUtxos({
|
|
56
56
|
asset,
|
|
57
|
-
utxos
|
|
57
|
+
utxos,
|
|
58
58
|
feeData,
|
|
59
59
|
txSet,
|
|
60
60
|
taprootEnabled,
|
|
61
61
|
})
|
|
62
|
-
if (!
|
|
62
|
+
if (!spendableUtxos) return { errorMessage: 'insufficient funds' }
|
|
63
63
|
|
|
64
64
|
const { txId } = tx
|
|
65
65
|
const replaceableTxs = findUnconfirmedSentRbfTxs(txSet)
|
|
66
66
|
const bumpTx = replaceableTxs.find((tx) => tx.txId === txId)
|
|
67
|
-
const changeUtxos =
|
|
67
|
+
const changeUtxos = spendableUtxos.getTxIdUtxos(txId)
|
|
68
68
|
|
|
69
69
|
// Can't bump a non-rbf tx with no change
|
|
70
70
|
if (!bumpTx && changeUtxos.size === 0) return { errorMessage: 'no change' }
|
|
@@ -80,7 +80,7 @@ const _canBumpTx = ({
|
|
|
80
80
|
if (bumpTx) {
|
|
81
81
|
const { replaceTx } = selectUtxos({
|
|
82
82
|
asset,
|
|
83
|
-
|
|
83
|
+
spendableUtxos,
|
|
84
84
|
replaceableTxs: [bumpTx],
|
|
85
85
|
feeRate,
|
|
86
86
|
receiveAddress: null,
|
|
@@ -91,7 +91,7 @@ const _canBumpTx = ({
|
|
|
91
91
|
|
|
92
92
|
const { fee } = selectUtxos({
|
|
93
93
|
asset,
|
|
94
|
-
|
|
94
|
+
spendableUtxos,
|
|
95
95
|
feeRate,
|
|
96
96
|
receiveAddress: 'P2WPKH',
|
|
97
97
|
getFeeEstimator,
|
package/src/fee/fee-estimator.js
CHANGED
|
@@ -53,17 +53,19 @@ const scriptPubKeyLengths = {
|
|
|
53
53
|
// 10 = version: 4, locktime: 4, inputs and outputs count: 1
|
|
54
54
|
// 148 = txId: 32, vout: 4, count: 1, script: 107 (max), sequence: 4
|
|
55
55
|
// 34 = value: 8, count: 1, scriptPubKey: 25 (P2PKH) and 23 (P2SH)
|
|
56
|
-
export
|
|
56
|
+
export const getSizeFactory = ({ ecc }) => (
|
|
57
57
|
asset: Object,
|
|
58
58
|
inputs: Array | UtxoCollection,
|
|
59
59
|
outputs: Array,
|
|
60
60
|
{ compressed = true } = {}
|
|
61
|
-
) {
|
|
61
|
+
) => {
|
|
62
|
+
assert(ecc, 'ecc is required')
|
|
62
63
|
if (inputs instanceof UtxoCollection) {
|
|
63
64
|
inputs = Array.from(inputs).map((utxo) => utxo.script || null)
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
// other bitcoin-like assets
|
|
68
|
+
const classifyOutput = scriptClassify.outputFactory({ ecc })
|
|
67
69
|
const baseSize =
|
|
68
70
|
4 + // n_version
|
|
69
71
|
4 + // n_locktime
|
|
@@ -74,7 +76,7 @@ export function getSize(
|
|
|
74
76
|
assert(isHex(script), 'script must be hex string')
|
|
75
77
|
|
|
76
78
|
const scriptBuffer = Buffer.from(script, 'hex')
|
|
77
|
-
const scriptType =
|
|
79
|
+
const scriptType = classifyOutput(scriptBuffer)
|
|
78
80
|
|
|
79
81
|
const supportedTypes = supportedInputTypes[asset.name] || supportedInputTypes.default
|
|
80
82
|
assert(
|
|
@@ -103,7 +105,7 @@ export function getSize(
|
|
|
103
105
|
else if (asset.address.isP2TR && asset.address.isP2TR(output)) scriptType = P2TR
|
|
104
106
|
else if (asset.address.isP2WSH && asset.address.isP2WSH(output)) scriptType = P2WSH
|
|
105
107
|
else {
|
|
106
|
-
scriptType =
|
|
108
|
+
scriptType = classifyOutput(asset.address.toScriptPubKey(output))
|
|
107
109
|
}
|
|
108
110
|
}
|
|
109
111
|
assert(
|
|
@@ -121,7 +123,7 @@ export function getSize(
|
|
|
121
123
|
// witnesses
|
|
122
124
|
inputs.reduce((t, script) => {
|
|
123
125
|
if (!script) return t + 1
|
|
124
|
-
const utxoScriptType =
|
|
126
|
+
const utxoScriptType = classifyOutput(Buffer.from(script, 'hex'))
|
|
125
127
|
if ([P2SH, P2WPKH].includes(utxoScriptType)) {
|
|
126
128
|
const pubKeyLength = 33
|
|
127
129
|
const signatureLength = 73 // maximum possible length
|
|
@@ -158,4 +160,5 @@ export function getSize(
|
|
|
158
160
|
return Math.ceil(weight / 4)
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
|
|
163
|
+
const getFeeEstimatorFactory = ({ ecc }) => createDefaultFeeEstimator(getSizeFactory({ ecc }))
|
|
164
|
+
export default getFeeEstimatorFactory
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import assert from 'minimalistic-assert'
|
|
2
|
-
import {
|
|
2
|
+
import { getUtxosData } from './utxo-selector'
|
|
3
3
|
import { findUnconfirmedSentRbfTxs } from '../tx-utils'
|
|
4
4
|
import { getSpendableUtxos, getUtxos } from '../utxos-utils'
|
|
5
|
-
import { getExtraFee } from './fee-utils'
|
|
6
5
|
import { canBumpTx } from './can-bump-tx'
|
|
7
6
|
|
|
8
7
|
export class GetFeeResolver {
|
|
@@ -16,13 +15,32 @@ export class GetFeeResolver {
|
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
getFee = ({ asset, accountState, txSet, feeData, amount, customFee, isSendAll }) => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
const { resolvedFee, extraFee } = this.#getUtxosData({
|
|
19
|
+
asset,
|
|
20
|
+
accountState,
|
|
21
|
+
txSet,
|
|
22
|
+
feeData,
|
|
23
|
+
amount,
|
|
24
|
+
customFee,
|
|
25
|
+
isSendAll,
|
|
26
|
+
})
|
|
27
|
+
return { fee: resolvedFee, extraFee }
|
|
28
|
+
}
|
|
23
29
|
|
|
24
|
-
|
|
30
|
+
getAvailableBalance = ({ asset, accountState, txSet, feeData, amount, customFee, isSendAll }) => {
|
|
31
|
+
return this.#getUtxosData({
|
|
32
|
+
asset,
|
|
33
|
+
accountState,
|
|
34
|
+
txSet,
|
|
35
|
+
feeData,
|
|
36
|
+
customFee,
|
|
37
|
+
isSendAll,
|
|
38
|
+
amount,
|
|
39
|
+
}).availableBalance
|
|
40
|
+
}
|
|
25
41
|
|
|
42
|
+
getSpendableBalance = ({ asset, accountState, txSet, feeData }) => {
|
|
43
|
+
const utxos = getUtxos({ accountState, asset })
|
|
26
44
|
const spendableUtxos = getSpendableUtxos({
|
|
27
45
|
asset,
|
|
28
46
|
utxos,
|
|
@@ -30,36 +48,17 @@ export class GetFeeResolver {
|
|
|
30
48
|
txSet,
|
|
31
49
|
taprootEnabled: this.#taprootEnabled,
|
|
32
50
|
})
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const receiveAddress = ['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(asset.name)
|
|
36
|
-
? 'P2WSH'
|
|
37
|
-
: 'P2PKH'
|
|
38
|
-
|
|
39
|
-
const feePerKB = customFee || feeData.feePerKB
|
|
40
|
-
const fee = getFee({
|
|
41
|
-
asset,
|
|
42
|
-
utxos: spendableUtxos,
|
|
43
|
-
replaceableTxs,
|
|
44
|
-
amount,
|
|
45
|
-
feeRate: feePerKB,
|
|
46
|
-
receiveAddress,
|
|
47
|
-
isSendAll,
|
|
48
|
-
getFeeEstimator: this.#getFeeEstimator,
|
|
49
|
-
})
|
|
50
|
-
const extraFee = asset.currency.baseUnit(
|
|
51
|
-
getExtraFee({ asset, inputs: spendableUtxos, feePerKB })
|
|
52
|
-
)
|
|
53
|
-
return { fee, extraFee }
|
|
51
|
+
return spendableUtxos.value
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
|
|
54
|
+
#getUtxosData = ({ asset, accountState, txSet, feeData, amount, customFee, isSendAll }) => {
|
|
57
55
|
assert(asset, 'asset must be provided')
|
|
58
56
|
assert(feeData, 'feeData must be provided')
|
|
59
57
|
assert(accountState, 'accountState must be provided')
|
|
60
58
|
assert(txSet, 'txSet must be provided')
|
|
61
59
|
|
|
62
60
|
const utxos = getUtxos({ accountState, asset })
|
|
61
|
+
|
|
63
62
|
const spendableUtxos = getSpendableUtxos({
|
|
64
63
|
asset,
|
|
65
64
|
utxos,
|
|
@@ -72,12 +71,16 @@ export class GetFeeResolver {
|
|
|
72
71
|
const receiveAddress = ['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(asset.name)
|
|
73
72
|
? 'P2WSH'
|
|
74
73
|
: 'P2PKH'
|
|
75
|
-
|
|
74
|
+
|
|
75
|
+
const feePerKB = customFee || feeData.feePerKB
|
|
76
|
+
return getUtxosData({
|
|
76
77
|
asset,
|
|
77
|
-
|
|
78
|
+
spendableUtxos,
|
|
78
79
|
replaceableTxs,
|
|
79
|
-
|
|
80
|
+
amount,
|
|
81
|
+
feeRate: feePerKB,
|
|
80
82
|
receiveAddress,
|
|
83
|
+
isSendAll: isSendAll,
|
|
81
84
|
getFeeEstimator: this.#getFeeEstimator,
|
|
82
85
|
})
|
|
83
86
|
}
|
package/src/fee/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export * from './get-fee-resolver'
|
|
2
|
-
export { default as
|
|
2
|
+
export { default as getFeeEstimatorFactory } from './fee-estimator'
|
package/src/fee/utxo-selector.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { UtxoCollection } from '@exodus/models'
|
|
2
2
|
import NumberUnit from '@exodus/currency'
|
|
3
|
+
import assert from 'minimalistic-assert'
|
|
4
|
+
import { getExtraFee } from './fee-utils'
|
|
3
5
|
|
|
4
6
|
const MIN_RELAY_FEE = 1000
|
|
5
|
-
const AVG_TX_SIZE = 192
|
|
6
7
|
|
|
7
8
|
export const selectUtxos = ({
|
|
8
9
|
asset,
|
|
9
|
-
|
|
10
|
+
spendableUtxos,
|
|
10
11
|
replaceableTxs,
|
|
11
12
|
amount,
|
|
12
13
|
feeRate,
|
|
@@ -16,6 +17,14 @@ export const selectUtxos = ({
|
|
|
16
17
|
disableReplacement = false,
|
|
17
18
|
mustSpendUtxos,
|
|
18
19
|
}) => {
|
|
20
|
+
assert(asset, 'asset is required')
|
|
21
|
+
assert(spendableUtxos, 'spendableUtxos is required')
|
|
22
|
+
assert(getFeeEstimator, 'getFeeEstimator is required')
|
|
23
|
+
|
|
24
|
+
const changeAddressType = ['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(asset.name)
|
|
25
|
+
? 'P2WPKH'
|
|
26
|
+
: 'P2PKH'
|
|
27
|
+
|
|
19
28
|
const feeEstimator = getFeeEstimator(asset, { feePerKB: feeRate })
|
|
20
29
|
const { currency } = asset
|
|
21
30
|
if (!amount) amount = currency.ZERO
|
|
@@ -49,7 +58,7 @@ export const selectUtxos = ({
|
|
|
49
58
|
? tx.data.sent.map(({ address }) => address)
|
|
50
59
|
: [
|
|
51
60
|
...tx.data.sent.map(({ address }) => address),
|
|
52
|
-
tx.data.changeAddress
|
|
61
|
+
tx.data.changeAddress?.address || changeAddressType,
|
|
53
62
|
]
|
|
54
63
|
if (receiveAddress) {
|
|
55
64
|
outputs.push(receiveAddress)
|
|
@@ -86,9 +95,7 @@ export const selectUtxos = ({
|
|
|
86
95
|
}
|
|
87
96
|
}
|
|
88
97
|
if (isSendAll || replaceTxAmount.add(additionalUtxos.value).gte(amount.add(fee))) {
|
|
89
|
-
const chainOutputs = isSendAll
|
|
90
|
-
? [receiveAddress]
|
|
91
|
-
: [receiveAddress, asset.name === 'bitcoin' ? 'P2WPKH' : 'P2PKH']
|
|
98
|
+
const chainOutputs = isSendAll ? [receiveAddress] : [receiveAddress, changeAddressType]
|
|
92
99
|
const chainFee = feeEstimator({
|
|
93
100
|
inputs: changeUtxos.union(additionalUtxos),
|
|
94
101
|
outputs: chainOutputs,
|
|
@@ -98,7 +105,7 @@ export const selectUtxos = ({
|
|
|
98
105
|
if ((!amount.isZero || tx.data.changeAddress) && fee.sub(tx.feeAmount).gte(chainFee)) {
|
|
99
106
|
continue
|
|
100
107
|
}
|
|
101
|
-
return {
|
|
108
|
+
return { selectedUtxos: additionalUtxos, fee, replaceTx: tx }
|
|
102
109
|
}
|
|
103
110
|
}
|
|
104
111
|
}
|
|
@@ -116,17 +123,14 @@ export const selectUtxos = ({
|
|
|
116
123
|
}
|
|
117
124
|
|
|
118
125
|
const utxosArray = spendableUtxos.union(ourRbfUtxos).toPriorityOrderedArray()
|
|
119
|
-
if (utxosArray.length === 0) return {}
|
|
120
|
-
|
|
121
|
-
const outputs =
|
|
122
|
-
isSendAll || amount.isZero
|
|
123
|
-
? [receiveAddress]
|
|
124
|
-
: [receiveAddress, asset.name === 'bitcoin' ? 'P2WPKH' : 'P2PKH']
|
|
125
126
|
|
|
126
127
|
if (isSendAll) {
|
|
127
128
|
const selectedUtxos = UtxoCollection.fromArray(utxosArray, { currency })
|
|
128
|
-
const fee = feeEstimator({ inputs: selectedUtxos, outputs })
|
|
129
|
-
|
|
129
|
+
const fee = feeEstimator({ inputs: selectedUtxos, outputs: [receiveAddress] })
|
|
130
|
+
if (selectedUtxos.value.lt(amount.add(fee))) {
|
|
131
|
+
return { fee }
|
|
132
|
+
}
|
|
133
|
+
return { selectedUtxos, fee }
|
|
130
134
|
}
|
|
131
135
|
|
|
132
136
|
// quickly add utxos to get to amount before starting to figure out fees, the minimum place to start is as much as the amount
|
|
@@ -149,6 +153,8 @@ export const selectUtxos = ({
|
|
|
149
153
|
let selectedUtxos = UtxoCollection.fromArray(selectedUtxosArray, { currency })
|
|
150
154
|
|
|
151
155
|
// start figuring out fees
|
|
156
|
+
const outputs = amount.isZero ? [changeAddressType] : [receiveAddress, changeAddressType]
|
|
157
|
+
|
|
152
158
|
let fee = feeEstimator({ inputs: selectedUtxos, outputs })
|
|
153
159
|
|
|
154
160
|
while (selectedUtxos.value.lt(amount.add(fee))) {
|
|
@@ -156,9 +162,6 @@ export const selectUtxos = ({
|
|
|
156
162
|
if (remainingUtxosArray.length === 0) {
|
|
157
163
|
// Try fee with no change
|
|
158
164
|
fee = feeEstimator({ inputs: selectedUtxos, outputs: [receiveAddress] })
|
|
159
|
-
if (selectedUtxos.value.lt(amount.add(fee))) {
|
|
160
|
-
return {}
|
|
161
|
-
}
|
|
162
165
|
break
|
|
163
166
|
}
|
|
164
167
|
|
|
@@ -166,43 +169,15 @@ export const selectUtxos = ({
|
|
|
166
169
|
selectedUtxos = selectedUtxos.addUtxo(remainingUtxosArray.shift())
|
|
167
170
|
fee = feeEstimator({ inputs: selectedUtxos, outputs })
|
|
168
171
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export const getAvailableBalance = ({
|
|
174
|
-
asset,
|
|
175
|
-
utxos,
|
|
176
|
-
replaceableTxs,
|
|
177
|
-
feeRate,
|
|
178
|
-
receiveAddress,
|
|
179
|
-
getFeeEstimator,
|
|
180
|
-
disableReplacement,
|
|
181
|
-
}) => {
|
|
182
|
-
const { utxos: selectedUtxos, replaceTx, fee } = selectUtxos({
|
|
183
|
-
asset,
|
|
184
|
-
utxos,
|
|
185
|
-
replaceableTxs,
|
|
186
|
-
feeRate,
|
|
187
|
-
receiveAddress,
|
|
188
|
-
isSendAll: true,
|
|
189
|
-
getFeeEstimator,
|
|
190
|
-
disableReplacement,
|
|
191
|
-
})
|
|
192
|
-
if (!selectedUtxos) return asset.currency.ZERO
|
|
193
|
-
if (replaceTx) {
|
|
194
|
-
return utxos
|
|
195
|
-
.getTxIdUtxos(replaceTx.txId)
|
|
196
|
-
.union(selectedUtxos)
|
|
197
|
-
.value.sub(fee)
|
|
198
|
-
.add(replaceTx.feeAmount)
|
|
172
|
+
if (selectedUtxos.value.lt(amount.add(fee))) {
|
|
173
|
+
return { fee }
|
|
199
174
|
}
|
|
200
|
-
return selectedUtxos
|
|
175
|
+
return { selectedUtxos, fee }
|
|
201
176
|
}
|
|
202
177
|
|
|
203
|
-
export const
|
|
178
|
+
export const getUtxosData = ({
|
|
204
179
|
asset,
|
|
205
|
-
|
|
180
|
+
spendableUtxos,
|
|
206
181
|
replaceableTxs,
|
|
207
182
|
amount,
|
|
208
183
|
feeRate,
|
|
@@ -210,10 +185,11 @@ export const getFee = ({
|
|
|
210
185
|
isSendAll,
|
|
211
186
|
getFeeEstimator,
|
|
212
187
|
disableReplacement,
|
|
188
|
+
mustSpendUtxos,
|
|
213
189
|
}) => {
|
|
214
|
-
const { replaceTx, fee } = selectUtxos({
|
|
190
|
+
const { selectedUtxos, replaceTx, fee } = selectUtxos({
|
|
215
191
|
asset,
|
|
216
|
-
|
|
192
|
+
spendableUtxos,
|
|
217
193
|
replaceableTxs,
|
|
218
194
|
amount,
|
|
219
195
|
feeRate,
|
|
@@ -221,8 +197,25 @@ export const getFee = ({
|
|
|
221
197
|
isSendAll,
|
|
222
198
|
getFeeEstimator,
|
|
223
199
|
disableReplacement,
|
|
200
|
+
mustSpendUtxos,
|
|
224
201
|
})
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
202
|
+
|
|
203
|
+
const resolvedFee = replaceTx ? fee.sub(replaceTx.feeAmount) : fee
|
|
204
|
+
|
|
205
|
+
const spendableBalance = spendableUtxos.value
|
|
206
|
+
|
|
207
|
+
const extraFee = selectedUtxos
|
|
208
|
+
? asset.currency.baseUnit(getExtraFee({ asset, inputs: selectedUtxos, feePerKB: feeRate }))
|
|
209
|
+
: asset.currency.ZERO
|
|
210
|
+
|
|
211
|
+
const availableBalance = spendableBalance.sub(resolvedFee).clampLowerZero()
|
|
212
|
+
return {
|
|
213
|
+
spendableBalance,
|
|
214
|
+
availableBalance,
|
|
215
|
+
selectedUtxos,
|
|
216
|
+
fee,
|
|
217
|
+
resolvedFee,
|
|
218
|
+
replaceTx,
|
|
219
|
+
extraFee,
|
|
220
|
+
}
|
|
228
221
|
}
|
package/src/tx-send/index.js
CHANGED
|
@@ -120,7 +120,7 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
120
120
|
const insightClient = asset.baseAsset.insightClient
|
|
121
121
|
const currency = asset.currency
|
|
122
122
|
const feeData = await assetClientInterface.getFeeConfig({ assetName })
|
|
123
|
-
const
|
|
123
|
+
const spendableUtxos = getSpendableUtxos({
|
|
124
124
|
asset,
|
|
125
125
|
utxos: getUtxos({ accountState, asset }),
|
|
126
126
|
feeData,
|
|
@@ -145,7 +145,7 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
145
145
|
if (bumpTxId) {
|
|
146
146
|
const bumpTx = replaceableTxs.find(({ txId }) => txId === bumpTxId)
|
|
147
147
|
if (!bumpTx) {
|
|
148
|
-
utxosToBump =
|
|
148
|
+
utxosToBump = spendableUtxos.getTxIdUtxos(bumpTxId)
|
|
149
149
|
if (utxosToBump.size === 0) {
|
|
150
150
|
throw new Error(`Cannot bump transaction ${bumpTxId}`)
|
|
151
151
|
}
|
|
@@ -159,9 +159,9 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
159
159
|
let receiveAddress = bumpTxId ? (replaceableTxs.length > 0 ? null : 'P2WPKH') : address
|
|
160
160
|
const feeRate = feeData.feePerKB
|
|
161
161
|
|
|
162
|
-
let {
|
|
162
|
+
let { selectedUtxos, fee, replaceTx } = selectUtxos({
|
|
163
163
|
asset,
|
|
164
|
-
|
|
164
|
+
spendableUtxos,
|
|
165
165
|
replaceableTxs,
|
|
166
166
|
amount: sendAmount,
|
|
167
167
|
feeRate: customFee || feeRate,
|
|
@@ -171,14 +171,14 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
171
171
|
mustSpendUtxos: utxosToBump,
|
|
172
172
|
})
|
|
173
173
|
|
|
174
|
-
if (!
|
|
174
|
+
if (!selectedUtxos && !replaceTx) throw new Error('Not enough funds.')
|
|
175
175
|
|
|
176
176
|
// When bumping a tx, we can either replace the tx with RBF or spend its selected change.
|
|
177
177
|
// If there is no selected UTXO or the tx to replace is not the tx we want to bump,
|
|
178
178
|
// then something is wrong because we can't actually bump the tx.
|
|
179
179
|
// This shouldn't happen but might due to either the tx confirming before accelerate was
|
|
180
180
|
// pressed, or if the change was already spent from another wallet.
|
|
181
|
-
if (bumpTxId && (!
|
|
181
|
+
if (bumpTxId && (!selectedUtxos || (replaceTx && replaceTx.txId !== bumpTxId))) {
|
|
182
182
|
throw new Error(`Unable to bump ${bumpTxId}`)
|
|
183
183
|
}
|
|
184
184
|
|
|
@@ -188,13 +188,13 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
188
188
|
replaceTx.data.sent = replaceTx.data.sent.map((to) => {
|
|
189
189
|
return { ...to, amount: parseCurrency(to.amount, asset.currency) }
|
|
190
190
|
})
|
|
191
|
-
|
|
191
|
+
selectedUtxos = selectedUtxos.union(
|
|
192
192
|
UtxoCollection.fromJSON(replaceTx.data.inputs, { currency: asset.currency })
|
|
193
193
|
)
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
// transform UTXO object to raw
|
|
197
|
-
const inputs = shuffle(createInputs(assetName,
|
|
197
|
+
const inputs = shuffle(createInputs(assetName, selectedUtxos.toArray(), rbfEnabled))
|
|
198
198
|
|
|
199
199
|
let outputs
|
|
200
200
|
if (replaceTx) {
|
|
@@ -212,7 +212,7 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
212
212
|
? replaceTx.data.sent.reduce((total, { amount }) => total.add(amount), sendAmount)
|
|
213
213
|
: sendAmount
|
|
214
214
|
|
|
215
|
-
const change =
|
|
215
|
+
const change = selectedUtxos.value.sub(totalAmount).sub(fee)
|
|
216
216
|
const dust = getDustValue(asset)
|
|
217
217
|
let ourAddress = replaceTx?.data?.changeAddress || changeAddress
|
|
218
218
|
if (['bcash'].includes(assetName)) {
|
|
@@ -244,12 +244,12 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
244
244
|
outputs,
|
|
245
245
|
},
|
|
246
246
|
txMeta: {
|
|
247
|
-
addressPathsMap:
|
|
247
|
+
addressPathsMap: selectedUtxos.getAddressPathsMap(),
|
|
248
248
|
blockHeight,
|
|
249
249
|
},
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
const nonWitnessTxs = await getNonWitnessTxs(asset,
|
|
252
|
+
const nonWitnessTxs = await getNonWitnessTxs(asset, selectedUtxos, insightClient)
|
|
253
253
|
Object.assign(unsignedTx.txMeta, { rawTxs: nonWitnessTxs })
|
|
254
254
|
const { rawTx, txId, tx } = await assetClientInterface.signTransaction({
|
|
255
255
|
assetName,
|
|
@@ -287,7 +287,7 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
287
287
|
err.txInfo = JSON.stringify({
|
|
288
288
|
amount: sendAmount.toDefaultString({ unit: true }),
|
|
289
289
|
fee: ((fee && fee.toDefaultString({ unit: true })) || 0).toString(), // todo why does 0 not have a unit? Is default unit ok here?
|
|
290
|
-
allUtxos:
|
|
290
|
+
allUtxos: spendableUtxos.toJSON(),
|
|
291
291
|
})
|
|
292
292
|
throw err
|
|
293
293
|
} else {
|
|
@@ -306,7 +306,7 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
306
306
|
}
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
-
let remainingUtxos =
|
|
309
|
+
let remainingUtxos = spendableUtxos.difference(selectedUtxos)
|
|
310
310
|
if (changeUtxoIndex !== -1) {
|
|
311
311
|
const address = Address.create(ourAddress.address, ourAddress.meta)
|
|
312
312
|
const changeUtxo = {
|
|
@@ -368,7 +368,7 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
368
368
|
changeAddress: changeOutput ? ourAddress : undefined,
|
|
369
369
|
blockHeight,
|
|
370
370
|
blocksSeen: 0,
|
|
371
|
-
inputs:
|
|
371
|
+
inputs: selectedUtxos.toJSON(),
|
|
372
372
|
replacedTxId: replaceTx ? replaceTx.txId : undefined,
|
|
373
373
|
},
|
|
374
374
|
},
|
|
@@ -3,7 +3,7 @@ import lodash from 'lodash'
|
|
|
3
3
|
import ECPairFactory from 'ecpair'
|
|
4
4
|
import { Psbt } from '@exodus/bitcoinjs-lib'
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { toAsyncSigner, tweakSigner } from './taproot'
|
|
7
7
|
|
|
8
8
|
let ECPair
|
|
9
9
|
|
|
@@ -12,12 +12,13 @@ const _MAXIMUM_FEE_RATES = {
|
|
|
12
12
|
ravencoin: 1000000,
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo }) => {
|
|
15
|
+
export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, network, ecc }) => {
|
|
16
16
|
assert(assetName, 'assetName is required')
|
|
17
17
|
assert(resolvePurpose, 'resolvePurpose is required')
|
|
18
18
|
assert(keys, 'keys is required')
|
|
19
19
|
assert(coinInfo, 'coinInfo is required')
|
|
20
|
-
|
|
20
|
+
assert(ecc, 'ecc is required')
|
|
21
|
+
return async ({ unsignedTx, hdkeys }): Object => {
|
|
21
22
|
assert(unsignedTx, 'unsignedTx is required')
|
|
22
23
|
const { privateKeysAddressMap, addressPathsMap, rawTxs } = unsignedTx.txMeta
|
|
23
24
|
const { inputs, outputs } = unsignedTx.txData
|
|
@@ -33,7 +34,7 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo }) =>
|
|
|
33
34
|
|
|
34
35
|
ECPair = ECPair || ECPairFactory(ecc)
|
|
35
36
|
|
|
36
|
-
const
|
|
37
|
+
const getKeyAndPurpose = lodash.memoize((address) => {
|
|
37
38
|
// TODO: Consider using privateKeysAddressMap for other assets
|
|
38
39
|
if (privateKeysAddressMap) return ECPair.fromWIF(privateKeysAddressMap[address], networkInfo)
|
|
39
40
|
const path = addressPathsMap[address]
|
|
@@ -44,12 +45,12 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo }) =>
|
|
|
44
45
|
assert(hdkey, `hdkey for purpose for ${purpose} and address ${address} could not be resolved`)
|
|
45
46
|
const derivedhdkey = hdkey.derive(path)
|
|
46
47
|
const privateEncoded = keys.encodePrivate(derivedhdkey.privateKey)
|
|
47
|
-
return ECPair.fromWIF(privateEncoded, networkInfo)
|
|
48
|
+
return { key: ECPair.fromWIF(privateEncoded, networkInfo), purpose }
|
|
48
49
|
})
|
|
49
50
|
|
|
50
51
|
// Fill tx
|
|
51
52
|
for (const { txId, vout, address, value, script, sequence } of inputs) {
|
|
52
|
-
const purpose =
|
|
53
|
+
const { purpose } = getKeyAndPurpose(address)
|
|
53
54
|
const isSegwitAddress = purpose === 84
|
|
54
55
|
const isTaprootAddress = purpose === 86
|
|
55
56
|
const txIn = { hash: txId, index: vout, sequence }
|
|
@@ -69,11 +70,23 @@ export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo }) =>
|
|
|
69
70
|
psbt.addOutput({ value: amount, address })
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
// The Taproot SIGHASH flag includes all previous outputs,
|
|
74
|
+
// so signing is only done AFTER all inputs have been updated
|
|
75
|
+
for (let index = 0; index < inputs.length; index++) {
|
|
76
|
+
const { address } = inputs[index]
|
|
77
|
+
const { key, purpose } = getKeyAndPurpose(address)
|
|
78
|
+
if (ecc.signSchnorrAsync) {
|
|
79
|
+
// desktop / BE signing
|
|
80
|
+
const isTaprootAddress = purpose === 86
|
|
81
|
+
const signingKey = isTaprootAddress
|
|
82
|
+
? tweakSigner({ signer: key, ECPair, ecc, network })
|
|
83
|
+
: key
|
|
84
|
+
await psbt.signInputAsync(index, toAsyncSigner({ keyPair: signingKey, ecc }))
|
|
85
|
+
} else {
|
|
86
|
+
// mobile signing
|
|
87
|
+
psbt.signInput(index, key)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
77
90
|
|
|
78
91
|
// Serialize tx
|
|
79
92
|
psbt.finalizeAllInputs()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { crypto } from '@exodus/bitcoinjs-lib'
|
|
2
|
+
import assert from 'minimalistic-assert'
|
|
3
|
+
|
|
4
|
+
export function tweakSigner({ signer, ECPair, ecc, tweakHash, network }) {
|
|
5
|
+
assert(signer, 'signer is required')
|
|
6
|
+
assert(ECPair, 'ECPair is required')
|
|
7
|
+
assert(ecc, 'ecc is required')
|
|
8
|
+
let privateKey: Uint8Array | undefined = signer.privateKey
|
|
9
|
+
if (!privateKey) {
|
|
10
|
+
throw new Error('Private key is required for tweaking signer!')
|
|
11
|
+
}
|
|
12
|
+
if (signer.publicKey[0] === 3) {
|
|
13
|
+
privateKey = ecc.privateNegate(privateKey)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const tweakedPrivateKey = ecc.privateAdd(
|
|
17
|
+
privateKey,
|
|
18
|
+
tapTweakHash(signer.publicKey.slice(1, 33), tweakHash)
|
|
19
|
+
)
|
|
20
|
+
if (!tweakedPrivateKey) {
|
|
21
|
+
throw new Error('Invalid tweaked private key!')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return ECPair.fromPrivateKey(Buffer.from(tweakedPrivateKey), {
|
|
25
|
+
network,
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function tapTweakHash(pubKey: Buffer, h: Buffer | undefined): Buffer {
|
|
30
|
+
return crypto.taggedHash('TapTweak', Buffer.concat(h ? [pubKey, h] : [pubKey]))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Take a sync signer and make it async.
|
|
35
|
+
*/
|
|
36
|
+
export function toAsyncSigner({ keyPair, ecc }) {
|
|
37
|
+
assert(keyPair, 'keyPair is required')
|
|
38
|
+
assert(ecc, 'ecc is required')
|
|
39
|
+
keyPair.sign = async (h) => {
|
|
40
|
+
const sig = await ecc.signAsync(h, keyPair.privateKey)
|
|
41
|
+
return Buffer.from(sig)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
keyPair.signSchnorr = async (h) => {
|
|
45
|
+
const sig = await ecc.signSchnorrAsync(h, keyPair.privateKey)
|
|
46
|
+
return Buffer.from(sig)
|
|
47
|
+
}
|
|
48
|
+
return keyPair
|
|
49
|
+
}
|
package/src/utxos-utils.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { UtxoCollection } from '@exodus/models'
|
|
3
3
|
import { findLargeUnconfirmedTxs } from './tx-utils'
|
|
4
4
|
import assert from 'minimalistic-assert'
|
|
5
|
+
import { mapValues } from '@exodus/basic-utils'
|
|
5
6
|
|
|
6
7
|
export function getUtxos({ accountState, asset }) {
|
|
7
8
|
return (
|
|
@@ -12,9 +13,23 @@ export function getUtxos({ accountState, asset }) {
|
|
|
12
13
|
)
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
return
|
|
16
|
+
export const getBalancesFactory = ({ taprootEnabled, feeData }) => {
|
|
17
|
+
assert(feeData, 'feeData is required')
|
|
18
|
+
return ({ asset, accountState, txLog }) => {
|
|
19
|
+
assert(asset, 'asset is required')
|
|
20
|
+
assert(accountState, 'accountState is required')
|
|
21
|
+
assert(txLog, 'txLog is required')
|
|
22
|
+
const utxos = getUtxos({ asset, accountState })
|
|
23
|
+
const balance = utxos.value
|
|
24
|
+
const spendableBalance = getSpendableUtxos({
|
|
25
|
+
asset,
|
|
26
|
+
utxos,
|
|
27
|
+
txSet: txLog,
|
|
28
|
+
feeData,
|
|
29
|
+
taprootEnabled,
|
|
30
|
+
}).value
|
|
31
|
+
return mapValues({ balance, spendableBalance }, (balance) => (balance.isZero ? null : balance))
|
|
32
|
+
}
|
|
18
33
|
}
|
|
19
34
|
|
|
20
35
|
const isTaprootUtxo = (asset, utxo) =>
|