@exodus/bitcoin-api 1.0.0-alpha.4 → 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 +5 -3
- 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/index.js +3 -1
- package/src/tx-log/bitcoin-monitor.js +33 -3
- package/src/tx-send/index.js +25 -31
- package/src/tx-sign/default-create-tx.js +74 -53
- package/src/tx-sign/index.js +1 -1
- 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": [
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@exodus/asset-lib": "^3.7.1",
|
|
23
23
|
"@exodus/bitcoinjs-lib": "6.0.2-beta.4",
|
|
24
|
-
"@exodus/keychain": "^
|
|
24
|
+
"@exodus/keychain": "^3.0.0",
|
|
25
25
|
"@exodus/models": "^8.10.4",
|
|
26
26
|
"@exodus/secp256k1": "4.0.2-exodus.0",
|
|
27
27
|
"@exodus/simple-retry": "0.0.6",
|
|
@@ -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/index.js
CHANGED
|
@@ -7,9 +7,11 @@ export { default as InsightAPIClient } from './insight-api-client'
|
|
|
7
7
|
export { default as InsightWSClient } from './insight-api-client/ws'
|
|
8
8
|
export { default as bip44Constants } from './constants/bip44'
|
|
9
9
|
export { default as createGetKeyIdentifier } from './key-identifier'
|
|
10
|
-
|
|
10
|
+
export * from './tx-send'
|
|
11
|
+
export * from './tx-sign'
|
|
11
12
|
export * from './fee'
|
|
12
13
|
export * from './utxos-utils'
|
|
13
14
|
export * from './tx-log'
|
|
14
15
|
export * from './unconfirmed-ancestor-data'
|
|
16
|
+
export * from './parse-unsigned-tx'
|
|
15
17
|
export * from './insight-api-client/util'
|
|
@@ -7,6 +7,7 @@ import { updateUnconfirmedAncestorData } from '../unconfirmed-ancestor-data'
|
|
|
7
7
|
import { BitcoinMonitorScanner } from './bitcoin-monitor-scanner'
|
|
8
8
|
import { normalizeInsightConfig, toWSUrl } from '../insight-api-client/util'
|
|
9
9
|
import ms from 'ms'
|
|
10
|
+
import delay from 'delay'
|
|
10
11
|
|
|
11
12
|
// NOTE: this is a frankenstein mashup of Exodus desktop
|
|
12
13
|
// assets-refresh/insight action + Neo monitor
|
|
@@ -58,6 +59,9 @@ export class Monitor extends BaseMonitor {
|
|
|
58
59
|
})
|
|
59
60
|
|
|
60
61
|
this.addHook('after-tick-multiple-wallet-accounts', () => this.#subscribeToNewAddresses())
|
|
62
|
+
this.addHook('after-stop', async () =>
|
|
63
|
+
Promise.all(Object.keys(this.#runningByWalletAccount).map(this.#waitForWalletToFinish))
|
|
64
|
+
)
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
setServer(assetConfig = {}) {
|
|
@@ -180,10 +184,22 @@ export class Monitor extends BaseMonitor {
|
|
|
180
184
|
|
|
181
185
|
tick = async ({ walletAccount, refresh }) => {
|
|
182
186
|
assert(walletAccount, 'walletAccount is expected')
|
|
183
|
-
|
|
184
|
-
|
|
187
|
+
// 1) if no tick is running, any tick runs
|
|
188
|
+
// 2) if a regular tick is running, a refresh tick waits and runs
|
|
189
|
+
// 3) if a regular tick is running, a regular tick does not wait and does not run
|
|
190
|
+
// 4) if a refresh tick is running, a refresh tick does not wait and does not run
|
|
191
|
+
// 5) if a refresh tick is running, a regular tick does not wait and does not run
|
|
192
|
+
if (refresh && this.#runningByWalletAccount[walletAccount]?.refresh !== refresh) {
|
|
193
|
+
await this.#waitForWalletToFinish(walletAccount)
|
|
194
|
+
}
|
|
195
|
+
if (this.#runningByWalletAccount[walletAccount]) {
|
|
196
|
+
this.logger.debug(`Skipping ${walletAccount} tick as previous tick is still running`)
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
const promise = this.#syncWalletAccount({ walletAccount, refresh })
|
|
200
|
+
this.#runningByWalletAccount[walletAccount] = { refresh, promise }
|
|
185
201
|
try {
|
|
186
|
-
await
|
|
202
|
+
await promise
|
|
187
203
|
} finally {
|
|
188
204
|
delete this.#runningByWalletAccount[walletAccount]
|
|
189
205
|
}
|
|
@@ -242,6 +258,20 @@ export class Monitor extends BaseMonitor {
|
|
|
242
258
|
#logWsStatus = (message, ...args) => {
|
|
243
259
|
// console.debug('btc-like monitor', this.asset.name, message, ...args)
|
|
244
260
|
}
|
|
261
|
+
|
|
262
|
+
#waitForWalletToFinish = async (walletAccount) => {
|
|
263
|
+
const tickState = this.#runningByWalletAccount[walletAccount]
|
|
264
|
+
if (!tickState) return
|
|
265
|
+
|
|
266
|
+
const finished = await Promise.race([
|
|
267
|
+
tickState.promise.then(() => true),
|
|
268
|
+
delay(ms('10s')).then(() => false),
|
|
269
|
+
])
|
|
270
|
+
|
|
271
|
+
if (!finished) {
|
|
272
|
+
this.logger.warn(`Tick for ${walletAccount} did not finish on time`)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
245
275
|
}
|
|
246
276
|
|
|
247
277
|
export const createBitcoinMonitor = (args) => new Monitor(args)
|
package/src/tx-send/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from 'minimalistic-assert'
|
|
2
|
-
|
|
2
|
+
// Using this notation so it can be mocked by jest
|
|
3
|
+
import shuffle from 'lodash/shuffle'
|
|
3
4
|
|
|
4
5
|
import { UtxoCollection, Address } from '@exodus/models'
|
|
5
6
|
import { retry } from '@exodus/simple-retry'
|
|
@@ -12,11 +13,13 @@ import {
|
|
|
12
13
|
createInputs as dogecoinCreateInputs,
|
|
13
14
|
createOutput as dogecoinCreateOutput,
|
|
14
15
|
} from './dogecoin'
|
|
15
|
-
import {
|
|
16
|
+
import { findUnconfirmedSentRbfTxs } from '../tx-utils'
|
|
16
17
|
import { getSpendableUtxos, getUtxos } from '../utxos-utils'
|
|
17
18
|
|
|
18
19
|
const ASSETS_SUPPORTED_BIP_174 = [
|
|
19
20
|
'bitcoin',
|
|
21
|
+
'bitcoinregtest',
|
|
22
|
+
'bitcointestnet',
|
|
20
23
|
'litecoin',
|
|
21
24
|
'dash',
|
|
22
25
|
'ravencoin',
|
|
@@ -86,16 +89,12 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
86
89
|
const {
|
|
87
90
|
multipleAddressesEnabled,
|
|
88
91
|
feePerKB,
|
|
89
|
-
hdkey44,
|
|
90
|
-
hdkey84,
|
|
91
|
-
hdkey86,
|
|
92
92
|
customFee,
|
|
93
93
|
isSendAll,
|
|
94
94
|
isExchange,
|
|
95
95
|
isBip70,
|
|
96
96
|
bumpTxId,
|
|
97
97
|
isRbfAllowed = true,
|
|
98
|
-
preBroadcastHook,
|
|
99
98
|
} = options
|
|
100
99
|
|
|
101
100
|
assert(assetClientInterface, `assetClientInterface must be supplied in sendTx for ${asset.name}`)
|
|
@@ -121,7 +120,7 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
121
120
|
const insightClient = asset.baseAsset.insightClient
|
|
122
121
|
const currency = asset.currency
|
|
123
122
|
const feeData = await assetClientInterface.getFeeConfig({ assetName })
|
|
124
|
-
const
|
|
123
|
+
const spendableUtxos = getSpendableUtxos({
|
|
125
124
|
asset,
|
|
126
125
|
utxos: getUtxos({ accountState, asset }),
|
|
127
126
|
feeData,
|
|
@@ -129,7 +128,7 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
129
128
|
taprootEnabled,
|
|
130
129
|
})
|
|
131
130
|
|
|
132
|
-
let replaceableTxs =
|
|
131
|
+
let replaceableTxs = findUnconfirmedSentRbfTxs(txSet)
|
|
133
132
|
|
|
134
133
|
if (assetName === 'bcash') {
|
|
135
134
|
address = asset.address.toLegacyAddress(address)
|
|
@@ -146,7 +145,7 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
146
145
|
if (bumpTxId) {
|
|
147
146
|
const bumpTx = replaceableTxs.find(({ txId }) => txId === bumpTxId)
|
|
148
147
|
if (!bumpTx) {
|
|
149
|
-
utxosToBump =
|
|
148
|
+
utxosToBump = spendableUtxos.getTxIdUtxos(bumpTxId)
|
|
150
149
|
if (utxosToBump.size === 0) {
|
|
151
150
|
throw new Error(`Cannot bump transaction ${bumpTxId}`)
|
|
152
151
|
}
|
|
@@ -160,9 +159,9 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
160
159
|
let receiveAddress = bumpTxId ? (replaceableTxs.length > 0 ? null : 'P2WPKH') : address
|
|
161
160
|
const feeRate = feeData.feePerKB
|
|
162
161
|
|
|
163
|
-
let {
|
|
162
|
+
let { selectedUtxos, fee, replaceTx } = selectUtxos({
|
|
164
163
|
asset,
|
|
165
|
-
|
|
164
|
+
spendableUtxos,
|
|
166
165
|
replaceableTxs,
|
|
167
166
|
amount: sendAmount,
|
|
168
167
|
feeRate: customFee || feeRate,
|
|
@@ -172,14 +171,14 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
172
171
|
mustSpendUtxos: utxosToBump,
|
|
173
172
|
})
|
|
174
173
|
|
|
175
|
-
if (!
|
|
174
|
+
if (!selectedUtxos && !replaceTx) throw new Error('Not enough funds.')
|
|
176
175
|
|
|
177
176
|
// When bumping a tx, we can either replace the tx with RBF or spend its selected change.
|
|
178
177
|
// If there is no selected UTXO or the tx to replace is not the tx we want to bump,
|
|
179
178
|
// then something is wrong because we can't actually bump the tx.
|
|
180
179
|
// This shouldn't happen but might due to either the tx confirming before accelerate was
|
|
181
180
|
// pressed, or if the change was already spent from another wallet.
|
|
182
|
-
if (bumpTxId && (!
|
|
181
|
+
if (bumpTxId && (!selectedUtxos || (replaceTx && replaceTx.txId !== bumpTxId))) {
|
|
183
182
|
throw new Error(`Unable to bump ${bumpTxId}`)
|
|
184
183
|
}
|
|
185
184
|
|
|
@@ -189,13 +188,13 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
189
188
|
replaceTx.data.sent = replaceTx.data.sent.map((to) => {
|
|
190
189
|
return { ...to, amount: parseCurrency(to.amount, asset.currency) }
|
|
191
190
|
})
|
|
192
|
-
|
|
191
|
+
selectedUtxos = selectedUtxos.union(
|
|
193
192
|
UtxoCollection.fromJSON(replaceTx.data.inputs, { currency: asset.currency })
|
|
194
193
|
)
|
|
195
194
|
}
|
|
196
195
|
|
|
197
196
|
// transform UTXO object to raw
|
|
198
|
-
const inputs =
|
|
197
|
+
const inputs = shuffle(createInputs(assetName, selectedUtxos.toArray(), rbfEnabled))
|
|
199
198
|
|
|
200
199
|
let outputs
|
|
201
200
|
if (replaceTx) {
|
|
@@ -213,7 +212,7 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
213
212
|
? replaceTx.data.sent.reduce((total, { amount }) => total.add(amount), sendAmount)
|
|
214
213
|
: sendAmount
|
|
215
214
|
|
|
216
|
-
const change =
|
|
215
|
+
const change = selectedUtxos.value.sub(totalAmount).sub(fee)
|
|
217
216
|
const dust = getDustValue(asset)
|
|
218
217
|
let ourAddress = replaceTx?.data?.changeAddress || changeAddress
|
|
219
218
|
if (['bcash'].includes(assetName)) {
|
|
@@ -233,7 +232,7 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
233
232
|
fee = fee.add(change)
|
|
234
233
|
}
|
|
235
234
|
|
|
236
|
-
outputs = replaceTx ? outputs :
|
|
235
|
+
outputs = replaceTx ? outputs : shuffle(outputs)
|
|
237
236
|
const blockHeight = ['zcash', 'bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)
|
|
238
237
|
? await insightClient.fetchBlockHeight()
|
|
239
238
|
: 0
|
|
@@ -245,24 +244,19 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
245
244
|
outputs,
|
|
246
245
|
},
|
|
247
246
|
txMeta: {
|
|
248
|
-
addressPathsMap:
|
|
247
|
+
addressPathsMap: selectedUtxos.getAddressPathsMap(),
|
|
249
248
|
blockHeight,
|
|
250
249
|
},
|
|
251
250
|
}
|
|
252
251
|
|
|
253
|
-
const nonWitnessTxs = await getNonWitnessTxs(asset,
|
|
252
|
+
const nonWitnessTxs = await getNonWitnessTxs(asset, selectedUtxos, insightClient)
|
|
254
253
|
Object.assign(unsignedTx.txMeta, { rawTxs: nonWitnessTxs })
|
|
255
|
-
const { rawTx, txId, tx } = await
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
hdkey86,
|
|
254
|
+
const { rawTx, txId, tx } = await assetClientInterface.signTransaction({
|
|
255
|
+
assetName,
|
|
256
|
+
unsignedTx,
|
|
257
|
+
walletAccount,
|
|
260
258
|
})
|
|
261
259
|
|
|
262
|
-
if (preBroadcastHook) {
|
|
263
|
-
await preBroadcastHook({ txId, unsignedTx })
|
|
264
|
-
}
|
|
265
|
-
|
|
266
260
|
const broadcastTxWithRetry = retry(
|
|
267
261
|
async (rawTx) => {
|
|
268
262
|
try {
|
|
@@ -293,7 +287,7 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
293
287
|
err.txInfo = JSON.stringify({
|
|
294
288
|
amount: sendAmount.toDefaultString({ unit: true }),
|
|
295
289
|
fee: ((fee && fee.toDefaultString({ unit: true })) || 0).toString(), // todo why does 0 not have a unit? Is default unit ok here?
|
|
296
|
-
allUtxos:
|
|
290
|
+
allUtxos: spendableUtxos.toJSON(),
|
|
297
291
|
})
|
|
298
292
|
throw err
|
|
299
293
|
} else {
|
|
@@ -312,7 +306,7 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
312
306
|
}
|
|
313
307
|
}
|
|
314
308
|
|
|
315
|
-
let remainingUtxos =
|
|
309
|
+
let remainingUtxos = spendableUtxos.difference(selectedUtxos)
|
|
316
310
|
if (changeUtxoIndex !== -1) {
|
|
317
311
|
const address = Address.create(ourAddress.address, ourAddress.meta)
|
|
318
312
|
const changeUtxo = {
|
|
@@ -374,7 +368,7 @@ export const createAndBroadcastTXFactory = ({ getFeeEstimator, taprootEnabled })
|
|
|
374
368
|
changeAddress: changeOutput ? ourAddress : undefined,
|
|
375
369
|
blockHeight,
|
|
376
370
|
blocksSeen: 0,
|
|
377
|
-
inputs:
|
|
371
|
+
inputs: selectedUtxos.toJSON(),
|
|
378
372
|
replacedTxId: replaceTx ? replaceTx.txId : undefined,
|
|
379
373
|
},
|
|
380
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,67 +12,88 @@ const _MAXIMUM_FEE_RATES = {
|
|
|
12
12
|
ravencoin: 1000000,
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export
|
|
16
|
-
assert(
|
|
17
|
-
assert(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
export const signTxFactory = ({ assetName, resolvePurpose, keys, coinInfo, network, ecc }) => {
|
|
16
|
+
assert(assetName, 'assetName is required')
|
|
17
|
+
assert(resolvePurpose, 'resolvePurpose is required')
|
|
18
|
+
assert(keys, 'keys is required')
|
|
19
|
+
assert(coinInfo, 'coinInfo is required')
|
|
20
|
+
assert(ecc, 'ecc is required')
|
|
21
|
+
return async ({ unsignedTx, hdkeys }): Object => {
|
|
22
|
+
assert(unsignedTx, 'unsignedTx is required')
|
|
23
|
+
const { privateKeysAddressMap, addressPathsMap, rawTxs } = unsignedTx.txMeta
|
|
24
|
+
const { inputs, outputs } = unsignedTx.txData
|
|
25
|
+
const networkInfo = { ...coinInfo.toBitcoinJS(), messagePrefix: '' }
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
// use harcoded max fee rates for specific assets
|
|
28
|
+
// if undefined, will be set to default value by PSBT (2500)
|
|
29
|
+
const maximumFeeRate = _MAXIMUM_FEE_RATES[assetName]
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
const psbt = new Psbt({ maximumFeeRate, eccLib: ecc, network: networkInfo })
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
if (!['bitcoin', 'bitcoinregtest', 'bitcointestnet'].includes(assetName)) psbt.setVersion(1)
|
|
30
34
|
|
|
31
|
-
|
|
35
|
+
ECPair = ECPair || ECPairFactory(ecc)
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
const getKeyAndPurpose = lodash.memoize((address) => {
|
|
38
|
+
// TODO: Consider using privateKeysAddressMap for other assets
|
|
39
|
+
if (privateKeysAddressMap) return ECPair.fromWIF(privateKeysAddressMap[address], networkInfo)
|
|
40
|
+
const path = addressPathsMap[address]
|
|
41
|
+
const purpose = resolvePurpose(address)
|
|
42
|
+
assert(hdkeys, 'hdkeys must be provided')
|
|
43
|
+
assert(purpose, `purpose for address ${address} could not be resolved`)
|
|
44
|
+
const hdkey = hdkeys[purpose]
|
|
45
|
+
assert(hdkey, `hdkey for purpose for ${purpose} and address ${address} could not be resolved`)
|
|
46
|
+
const derivedhdkey = hdkey.derive(path)
|
|
47
|
+
const privateEncoded = keys.encodePrivate(derivedhdkey.privateKey)
|
|
48
|
+
return { key: ECPair.fromWIF(privateEncoded, networkInfo), purpose }
|
|
49
|
+
})
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
// Fill tx
|
|
52
|
+
for (const { txId, vout, address, value, script, sequence } of inputs) {
|
|
53
|
+
const { purpose } = getKeyAndPurpose(address)
|
|
54
|
+
const isSegwitAddress = purpose === 84
|
|
55
|
+
const isTaprootAddress = purpose === 86
|
|
56
|
+
const txIn = { hash: txId, index: vout, sequence }
|
|
57
|
+
if (isSegwitAddress || isTaprootAddress) {
|
|
58
|
+
// witness outputs only require the value and the script, not the full transaction
|
|
59
|
+
txIn.witnessUtxo = { value, script: Buffer.from(script, 'hex') }
|
|
60
|
+
} else {
|
|
61
|
+
// non-witness outptus require the full transaction
|
|
62
|
+
const rawTx = (rawTxs || []).find((t) => t.txId === txId)
|
|
63
|
+
assert(!!rawTx, 'Non-witness outputs require the full previous transaction.')
|
|
64
|
+
txIn.nonWitnessUtxo = Buffer.from(rawTx.rawData, 'hex')
|
|
65
|
+
}
|
|
66
|
+
psbt.addInput(txIn)
|
|
57
67
|
}
|
|
58
|
-
psbt.addInput(txIn)
|
|
59
|
-
}
|
|
60
68
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
for (const [address, amount] of outputs) {
|
|
70
|
+
psbt.addOutput({ value: amount, address })
|
|
71
|
+
}
|
|
64
72
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
+
}
|
|
70
90
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
91
|
+
// Serialize tx
|
|
92
|
+
psbt.finalizeAllInputs()
|
|
93
|
+
const tx = psbt.extractTransaction()
|
|
94
|
+
const rawTx = tx.toBuffer()
|
|
95
|
+
const txId = tx.getId()
|
|
76
96
|
|
|
77
|
-
|
|
97
|
+
return { rawTx, txId, tx }
|
|
98
|
+
}
|
|
78
99
|
}
|
package/src/tx-sign/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { signTxFactory } from './default-create-tx'
|
|
@@ -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) =>
|