@exodus/bitcoin-api 1.0.0-alpha.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 +36 -0
- package/src/account-state.js +11 -0
- package/src/bitcoinjs-lib/ecc/index.js +81 -0
- package/src/bitcoinjs-lib/index.js +4 -0
- package/src/bitcoinjs-lib/script-classify/index.js +49 -0
- package/src/btc-address.js +7 -0
- package/src/btc-like-address.js +110 -0
- package/src/btc-like-keys.js +142 -0
- package/src/constants/bip44.js +27 -0
- package/src/index.js +12 -0
- package/src/insight-api-client/index.js +209 -0
- package/src/insight-api-client/util.js +110 -0
- package/src/insight-api-client/ws.js +54 -0
- package/src/key-identifier.js +19 -0
- package/src/tx-log/bitcoin-monitor-scanner.js +475 -0
- package/src/tx-log/bitcoin-monitor.js +231 -0
- package/src/tx-log/index.js +1 -0
- package/src/unconfirmed-ancestor-data.js +51 -0
- package/src/utxos-utils.js +15 -0
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@exodus/bitcoin-api",
|
|
3
|
+
"version": "1.0.0-alpha.0",
|
|
4
|
+
"description": "Exodus bitcoin-api",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"src",
|
|
8
|
+
"!src/**/__tests__"
|
|
9
|
+
],
|
|
10
|
+
"author": "Exodus",
|
|
11
|
+
"license": "ISC",
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "restricted"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "babel --root-mode upward --delete-dir-on-start --ignore '**/__tests__/**' src --out-dir lib",
|
|
17
|
+
"test": "jest",
|
|
18
|
+
"lint": "eslint ./src",
|
|
19
|
+
"lint:fix": "yarn lint --fix"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@exodus/asset-lib": "^3.7.1",
|
|
23
|
+
"@exodus/bitcoinjs-lib": "6.0.2-beta.4",
|
|
24
|
+
"@exodus/keychain": "^2.0.0",
|
|
25
|
+
"@exodus/models": "^8.10.4",
|
|
26
|
+
"@exodus/ravencoin-lib": "^1.0.1",
|
|
27
|
+
"@exodus/secp256k1": "4.0.2-exodus.0",
|
|
28
|
+
"bip44-constants": "55.0.0",
|
|
29
|
+
"coininfo": "5.1.0",
|
|
30
|
+
"delay": "4.0.1",
|
|
31
|
+
"querystring": "0.2.0",
|
|
32
|
+
"socket.io-client": "2.1.1",
|
|
33
|
+
"url-join": "4.0.0"
|
|
34
|
+
},
|
|
35
|
+
"gitHead": "96693adf64f85200d0fb18c7779e23cd93a72e53"
|
|
36
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AccountState, UtxoCollection } from '@exodus/models'
|
|
2
|
+
|
|
3
|
+
export function createAccountState({ asset }) {
|
|
4
|
+
return class BitcoinAccountState extends AccountState {
|
|
5
|
+
static defaults = {
|
|
6
|
+
utxos: UtxoCollection.createEmpty({
|
|
7
|
+
currency: asset.currency,
|
|
8
|
+
}),
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { TinySecp256k1Interface } from '@exodus/bitcoinjs-lib'
|
|
2
|
+
// TODO: temp import until '@noble/secp256k1' can be used
|
|
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
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { payments } from '@exodus/bitcoinjs-lib'
|
|
2
|
+
import { ecc } from '../ecc'
|
|
3
|
+
|
|
4
|
+
function isPaymentFactory(payment: any): (script: Buffer, eccLib?: any) => boolean {
|
|
5
|
+
return (script: Buffer, eccLib?: any): boolean => {
|
|
6
|
+
try {
|
|
7
|
+
payment({ output: script }, { eccLib })
|
|
8
|
+
return true
|
|
9
|
+
} catch (err) {
|
|
10
|
+
return false
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const isP2WPKH = isPaymentFactory(payments.p2wpkh)
|
|
16
|
+
const isP2TR = isPaymentFactory(payments.p2tr)
|
|
17
|
+
const isP2PKH = isPaymentFactory(payments.p2pkh)
|
|
18
|
+
const isP2MS = isPaymentFactory(payments.p2ms)
|
|
19
|
+
const isP2PK = isPaymentFactory(payments.p2pk)
|
|
20
|
+
const isP2WSHScript = isPaymentFactory(payments.p2wsh)
|
|
21
|
+
const isP2SHScript = isPaymentFactory(payments.p2sh)
|
|
22
|
+
|
|
23
|
+
const types = {
|
|
24
|
+
P2WPKH: 'witnesspubkeyhash',
|
|
25
|
+
P2PKH: 'pubkeyhash',
|
|
26
|
+
P2MS: 'multisig',
|
|
27
|
+
P2PK: 'pubkey',
|
|
28
|
+
P2WSH: 'witnessscripthash',
|
|
29
|
+
P2SH: 'scripthash',
|
|
30
|
+
P2TR: 'taproot',
|
|
31
|
+
NONSTANDARD: 'nonstandard',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const clasifyOutput = (script: Buffer) => {
|
|
35
|
+
if (isP2WPKH(script)) return types.P2WPKH
|
|
36
|
+
if (isP2TR(script, ecc)) return types.P2TR
|
|
37
|
+
if (isP2PKH(script)) return types.P2PKH
|
|
38
|
+
if (isP2MS(script)) return types.P2MS
|
|
39
|
+
if (isP2PK(script)) return types.P2PK
|
|
40
|
+
if (isP2WSHScript(script)) return types.P2WSH
|
|
41
|
+
if (isP2SHScript(script)) return types.P2SH
|
|
42
|
+
|
|
43
|
+
return types.NONSTANDARD
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const scriptClassify = {
|
|
47
|
+
types,
|
|
48
|
+
output: clasifyOutput,
|
|
49
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import bs58check from 'bs58check'
|
|
2
|
+
import bech32 from 'bech32'
|
|
3
|
+
import assert from 'minimalistic-assert'
|
|
4
|
+
import { identity, pickBy } from 'lodash'
|
|
5
|
+
import { ecc as defaultEcc } from './bitcoinjs-lib'
|
|
6
|
+
|
|
7
|
+
export const createBtcLikeAddress = ({
|
|
8
|
+
versions,
|
|
9
|
+
coinInfo,
|
|
10
|
+
bitcoinjsLib,
|
|
11
|
+
ecc = defaultEcc,
|
|
12
|
+
useBip86 = false,
|
|
13
|
+
validateFunctions = {},
|
|
14
|
+
extraFunctions = {},
|
|
15
|
+
}) => {
|
|
16
|
+
assert(versions, 'versions is required')
|
|
17
|
+
assert(coinInfo, 'coinInfo is required')
|
|
18
|
+
|
|
19
|
+
const bs58validateFactory = (version) =>
|
|
20
|
+
version === undefined
|
|
21
|
+
? undefined
|
|
22
|
+
: (string) => {
|
|
23
|
+
const payload = bs58check.decodeUnsafe(string)
|
|
24
|
+
return payload && payload.length === 21 && payload[0] === version
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const bech32ValidateFactory = (version, length) =>
|
|
28
|
+
version === undefined
|
|
29
|
+
? undefined
|
|
30
|
+
: (string) => {
|
|
31
|
+
try {
|
|
32
|
+
const decoded = bech32.decode(string)
|
|
33
|
+
// bech32 lib normalizes prefixes, so not worrying about uppercase
|
|
34
|
+
if (decoded.prefix !== versions.bech32) return false
|
|
35
|
+
// NOTE: We only support version zero witness programs
|
|
36
|
+
if (decoded.words[0] !== 0) return false
|
|
37
|
+
// Ensure correct length
|
|
38
|
+
if (bech32.fromWords(decoded.words.slice(1)).length !== length) return false
|
|
39
|
+
return true
|
|
40
|
+
} catch (e) {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const isP2PKH = validateFunctions.isP2PKH || bs58validateFactory(versions.p2pkh)
|
|
46
|
+
const isP2SH = validateFunctions.isP2SH || bs58validateFactory(versions.p2sh)
|
|
47
|
+
const isP2SH2 = validateFunctions.isP2SH2 || bs58validateFactory(versions.p2sh2)
|
|
48
|
+
const isP2WPKH = validateFunctions.isP2WPKH || bech32ValidateFactory(versions.bech32, 20)
|
|
49
|
+
const isP2WSH = validateFunctions.isP2WSH || bech32ValidateFactory(versions.bech32, 32)
|
|
50
|
+
const isP2TR =
|
|
51
|
+
validateFunctions.isP2TR ||
|
|
52
|
+
(useBip86 &&
|
|
53
|
+
bitcoinjsLib &&
|
|
54
|
+
((addr) => {
|
|
55
|
+
try {
|
|
56
|
+
const network = coinInfo.toBitcoinJS()
|
|
57
|
+
bitcoinjsLib.payments.p2tr({ address: addr, network }, { eccLib: ecc })
|
|
58
|
+
return true
|
|
59
|
+
} catch (e) {
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
})) ||
|
|
63
|
+
undefined
|
|
64
|
+
|
|
65
|
+
const purposeValidators = [
|
|
66
|
+
{ purpose: 44, validator: isP2PKH },
|
|
67
|
+
{ purpose: 49, validator: isP2SH },
|
|
68
|
+
{ purpose: 84, validator: isP2WPKH },
|
|
69
|
+
{ purpose: 86, validator: isP2TR },
|
|
70
|
+
|
|
71
|
+
// What are these 2?
|
|
72
|
+
// { purpose: ?, validator: isP2WSH },
|
|
73
|
+
// { purpose: ?, validator: isP2SH2 },
|
|
74
|
+
].filter(({ validator }) => validator)
|
|
75
|
+
|
|
76
|
+
const resolvedValidateFunctions = pickBy(
|
|
77
|
+
{
|
|
78
|
+
isP2PKH,
|
|
79
|
+
isP2SH,
|
|
80
|
+
isP2SH2,
|
|
81
|
+
isP2WPKH,
|
|
82
|
+
isP2WSH,
|
|
83
|
+
isP2TR,
|
|
84
|
+
},
|
|
85
|
+
identity
|
|
86
|
+
)
|
|
87
|
+
const resolvePurpose = (string) =>
|
|
88
|
+
purposeValidators.find(({ validator }) => validator(string))?.purpose
|
|
89
|
+
const validate = (string) => Object.values(resolvedValidateFunctions).some((fn) => fn(string))
|
|
90
|
+
|
|
91
|
+
const toScriptPubKey =
|
|
92
|
+
coinInfo && bitcoinjsLib?.address?.toOutputScript
|
|
93
|
+
? (string) => {
|
|
94
|
+
const network = coinInfo.toBitcoinJS()
|
|
95
|
+
return bitcoinjsLib.address.toOutputScript(string, network, ecc)
|
|
96
|
+
}
|
|
97
|
+
: undefined
|
|
98
|
+
|
|
99
|
+
return pickBy(
|
|
100
|
+
{
|
|
101
|
+
versions,
|
|
102
|
+
...resolvedValidateFunctions,
|
|
103
|
+
validate,
|
|
104
|
+
resolvePurpose,
|
|
105
|
+
toScriptPubKey,
|
|
106
|
+
...extraFunctions,
|
|
107
|
+
},
|
|
108
|
+
identity
|
|
109
|
+
)
|
|
110
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import bs58check from 'bs58check'
|
|
2
|
+
import secp256k1 from 'tiny-secp256k1'
|
|
3
|
+
import wif from 'wif'
|
|
4
|
+
import createHash from 'create-hash'
|
|
5
|
+
import bech32 from 'bech32'
|
|
6
|
+
import assert from 'minimalistic-assert'
|
|
7
|
+
import { identity, pickBy } from 'lodash'
|
|
8
|
+
import * as defaultBitcoinjsLib from '@exodus/bitcoinjs-lib'
|
|
9
|
+
import { ecc } from './bitcoinjs-lib'
|
|
10
|
+
|
|
11
|
+
export const createBtcLikeKeys = ({
|
|
12
|
+
coinInfo,
|
|
13
|
+
versions,
|
|
14
|
+
useBip86 = false,
|
|
15
|
+
bitcoinjsLib = defaultBitcoinjsLib,
|
|
16
|
+
extraFunctions = {},
|
|
17
|
+
}) => {
|
|
18
|
+
assert(coinInfo, 'coinInfo is required')
|
|
19
|
+
assert(versions, 'versions is required')
|
|
20
|
+
const {
|
|
21
|
+
encodePrivate: encodePrivateCustom,
|
|
22
|
+
encodePublic: encodePublicCustom,
|
|
23
|
+
encodePublicFromWIF: encodePublicFromWIFCustom,
|
|
24
|
+
encodePublicBech32: encodePublicBech32Custom,
|
|
25
|
+
encodePublicBech32FromWIF: encodePublicBech32FromWIFCustom,
|
|
26
|
+
encodeNestedP2WPKH: encodeNestedP2WPKHCustom,
|
|
27
|
+
encodePublicTaproot: encodePublicTaprootCustom,
|
|
28
|
+
...unknownExtraFunctions
|
|
29
|
+
} = extraFunctions
|
|
30
|
+
const encodePrivate =
|
|
31
|
+
encodePrivateCustom ||
|
|
32
|
+
((privateKey, compressed = true) => {
|
|
33
|
+
return wif.encode(coinInfo.versions.private, privateKey, compressed)
|
|
34
|
+
})
|
|
35
|
+
const encodePublicPurpose44 =
|
|
36
|
+
encodePublicCustom ||
|
|
37
|
+
((publicKey) => {
|
|
38
|
+
const sha = createHash('sha256')
|
|
39
|
+
.update(publicKey)
|
|
40
|
+
.digest()
|
|
41
|
+
const pubKeyHash = createHash('rmd160')
|
|
42
|
+
.update(sha)
|
|
43
|
+
.digest()
|
|
44
|
+
const payload = Buffer.concat([Buffer.from([versions.p2pkh]), pubKeyHash])
|
|
45
|
+
return bs58check.encode(payload)
|
|
46
|
+
})
|
|
47
|
+
const encodePublicFromWIF =
|
|
48
|
+
encodePublicFromWIFCustom ||
|
|
49
|
+
((privateKeyWIF) => {
|
|
50
|
+
const { privateKey, compressed } = wif.decode(privateKeyWIF, coinInfo.versions.private)
|
|
51
|
+
const publicKey = secp256k1.pointFromScalar(privateKey, compressed)
|
|
52
|
+
return encodePublicPurpose44(publicKey)
|
|
53
|
+
})
|
|
54
|
+
const encodePublicBech32 =
|
|
55
|
+
encodePublicBech32Custom || versions.bech32 !== undefined
|
|
56
|
+
? (publicKey) => {
|
|
57
|
+
const sha = createHash('sha256')
|
|
58
|
+
.update(publicKey)
|
|
59
|
+
.digest()
|
|
60
|
+
const pubKeyHash = createHash('rmd160')
|
|
61
|
+
.update(sha)
|
|
62
|
+
.digest()
|
|
63
|
+
const witnessVersion = Buffer.from([0])
|
|
64
|
+
const witnessProgram = Buffer.concat([
|
|
65
|
+
witnessVersion,
|
|
66
|
+
Buffer.from(bech32.toWords(pubKeyHash)),
|
|
67
|
+
])
|
|
68
|
+
return bech32.encode(versions.bech32, witnessProgram)
|
|
69
|
+
}
|
|
70
|
+
: undefined
|
|
71
|
+
const encodePublicBech32FromWIF =
|
|
72
|
+
encodePublicBech32FromWIFCustom || encodePublicBech32
|
|
73
|
+
? (privateKeyWIF) => {
|
|
74
|
+
// NOTE: No password support here
|
|
75
|
+
const { versions } = coinInfo
|
|
76
|
+
const { privateKey, compressed } = wif.decode(privateKeyWIF, versions.private)
|
|
77
|
+
const publicKey = secp256k1.pointFromScalar(privateKey, compressed)
|
|
78
|
+
return encodePublicBech32(publicKey)
|
|
79
|
+
}
|
|
80
|
+
: undefined
|
|
81
|
+
|
|
82
|
+
const encodeNestedP2WPKH =
|
|
83
|
+
encodeNestedP2WPKHCustom || bitcoinjsLib
|
|
84
|
+
? (publicKey) => {
|
|
85
|
+
const witnessProgram = bitcoinjsLib.payments.p2wpkh({
|
|
86
|
+
pubkey: publicKey,
|
|
87
|
+
}).output
|
|
88
|
+
|
|
89
|
+
const witnessProgramHash = bitcoinjsLib.crypto.hash160(witnessProgram)
|
|
90
|
+
return bitcoinjsLib.address.toBase58Check(
|
|
91
|
+
witnessProgramHash,
|
|
92
|
+
coinInfo.versions.scripthash
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
: undefined
|
|
96
|
+
|
|
97
|
+
const encodePublicTaproot =
|
|
98
|
+
encodePublicTaprootCustom ||
|
|
99
|
+
(useBip86
|
|
100
|
+
? (publicKey: Buffer): string => {
|
|
101
|
+
const network = coinInfo.toBitcoinJS()
|
|
102
|
+
return bitcoinjsLib.payments.p2tr(
|
|
103
|
+
{ internalPubkey: publicKey.slice(1, 33), network },
|
|
104
|
+
{ eccLib: ecc }
|
|
105
|
+
).address
|
|
106
|
+
}
|
|
107
|
+
: undefined)
|
|
108
|
+
|
|
109
|
+
const encodePublic = (publicKey, meta) => {
|
|
110
|
+
assert(meta?.purpose, 'asset.keys.encodePublic requires meta.purpose')
|
|
111
|
+
const purpose = meta.purpose
|
|
112
|
+
const errorMessage = `asset.keys.encodePublic does not support purpose ${purpose}`
|
|
113
|
+
if (purpose === 44) {
|
|
114
|
+
assert(encodePublicPurpose44, errorMessage)
|
|
115
|
+
return encodePublicPurpose44(publicKey)
|
|
116
|
+
}
|
|
117
|
+
if (purpose === 49) {
|
|
118
|
+
assert(encodeNestedP2WPKH, errorMessage)
|
|
119
|
+
return encodeNestedP2WPKH(publicKey)
|
|
120
|
+
}
|
|
121
|
+
if (purpose === 84) {
|
|
122
|
+
assert(encodePublicBech32, errorMessage)
|
|
123
|
+
return encodePublicBech32(publicKey)
|
|
124
|
+
}
|
|
125
|
+
if (purpose === 86) {
|
|
126
|
+
assert(encodePublicTaproot, errorMessage)
|
|
127
|
+
return encodePublicTaproot(publicKey)
|
|
128
|
+
}
|
|
129
|
+
throw new Error(errorMessage)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return pickBy(
|
|
133
|
+
{
|
|
134
|
+
encodePrivate,
|
|
135
|
+
encodePublic,
|
|
136
|
+
encodePublicFromWIF,
|
|
137
|
+
encodePublicBech32FromWIF,
|
|
138
|
+
...unknownExtraFunctions,
|
|
139
|
+
},
|
|
140
|
+
identity
|
|
141
|
+
)
|
|
142
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import bip44Constants from 'bip44-constants'
|
|
2
|
+
|
|
3
|
+
// MOVE TO SHARED LIB!!!
|
|
4
|
+
|
|
5
|
+
// Translate from
|
|
6
|
+
// array of [bip44, symbol, coin]
|
|
7
|
+
// to
|
|
8
|
+
// map of symbol ---> bip44
|
|
9
|
+
//
|
|
10
|
+
// Careful: for duplicate symbols only the first definition gets stored.
|
|
11
|
+
// Duplicate symbols: LTBC, DST, SAFE, BCO, RYO, XRD
|
|
12
|
+
// Run tests to find all differences between existing and new constants.
|
|
13
|
+
|
|
14
|
+
const _bip44Constants = {}
|
|
15
|
+
for (let [c, t] of bip44Constants) {
|
|
16
|
+
if (!(t in _bip44Constants)) {
|
|
17
|
+
_bip44Constants[t] = c
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// force NANO to 'Bitcoin Nano'
|
|
22
|
+
_bip44Constants['NANO'] = 0x80000100
|
|
23
|
+
_bip44Constants['HBAR'] = _bip44Constants['XHB']
|
|
24
|
+
_bip44Constants['FLR'] = 0x8000022a
|
|
25
|
+
_bip44Constants['EGLD'] = 0x800001fc
|
|
26
|
+
|
|
27
|
+
export default _bip44Constants
|
package/src/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './account-state'
|
|
2
|
+
export * from './btc-address'
|
|
3
|
+
export * from './btc-like-address'
|
|
4
|
+
export * from './btc-like-keys'
|
|
5
|
+
export * from './bitcoinjs-lib'
|
|
6
|
+
export { default as InsightAPIClient } from './insight-api-client'
|
|
7
|
+
export { default as bip44Constants } from './constants/bip44'
|
|
8
|
+
export { default as createGetKeyIdentifier } from './key-identifier'
|
|
9
|
+
|
|
10
|
+
export * from './utxos-utils'
|
|
11
|
+
export * from './tx-log'
|
|
12
|
+
export * from './unconfirmed-ancestor-data'
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/* @flow */
|
|
2
|
+
/* global fetch */
|
|
3
|
+
import urlJoin from 'url-join'
|
|
4
|
+
import qs from 'querystring'
|
|
5
|
+
import delay from 'delay'
|
|
6
|
+
|
|
7
|
+
// TODO: use p-retry
|
|
8
|
+
async function fetchJsonRetry(url, fetchOptions) {
|
|
9
|
+
const waitTimes = [5, 10, 20, 30].map((t) => t * 1000)
|
|
10
|
+
|
|
11
|
+
let i = 0
|
|
12
|
+
while (true) {
|
|
13
|
+
try {
|
|
14
|
+
const data = await fetchJson(url, fetchOptions)
|
|
15
|
+
return data
|
|
16
|
+
} catch (e) {
|
|
17
|
+
if (i < waitTimes.length) {
|
|
18
|
+
await delay(waitTimes[i])
|
|
19
|
+
i++
|
|
20
|
+
} else {
|
|
21
|
+
throw e
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function fetchJson(url, fetchOptions) {
|
|
27
|
+
const response = await fetch(url, fetchOptions)
|
|
28
|
+
if (!response.ok) throw new Error(`${url} returned ${response.status}: ${response.statusText}`)
|
|
29
|
+
return response.json()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default class InsightAPIClient {
|
|
34
|
+
constructor(baseURL) {
|
|
35
|
+
this._baseURL = baseURL
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setBaseUrl(baseURL) {
|
|
39
|
+
this._baseURL = baseURL
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async isNetworkConnected() {
|
|
43
|
+
let url = urlJoin(this._baseURL, '/peer')
|
|
44
|
+
let resp = await fetch(url, { timeout: 10000 })
|
|
45
|
+
let peerStatus = await resp.json()
|
|
46
|
+
|
|
47
|
+
return !!peerStatus.connected
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/*
|
|
51
|
+
async fetchBalance (address) {
|
|
52
|
+
let addrData = await this.fetchAddress(address)
|
|
53
|
+
return (addrData.balanceSat + addrData.unconfirmedBalanceSat) | 0
|
|
54
|
+
}
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
async fetchBlockHeight() {
|
|
58
|
+
let url = urlJoin(this._baseURL, '/status')
|
|
59
|
+
let resp = await fetch(url, { timeout: 10000 })
|
|
60
|
+
let status = await resp.json()
|
|
61
|
+
|
|
62
|
+
return status.info.blocks
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async fetchAddress(address, opts = { includeTxs: false }) {
|
|
66
|
+
let url = urlJoin(
|
|
67
|
+
this._baseURL,
|
|
68
|
+
opts.includeTxs ? `/addr/${address}` : `/addr/${address}?noTxList=1`
|
|
69
|
+
)
|
|
70
|
+
let response = await fetch(url)
|
|
71
|
+
|
|
72
|
+
return response.json()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async fetchUTXOs(addresses, { assetNames = [] } = {}) {
|
|
76
|
+
if (Array.isArray(addresses)) addresses = addresses.join(',')
|
|
77
|
+
let query = 'noCache=1'
|
|
78
|
+
if (assetNames) {
|
|
79
|
+
query = `${query}&assetNames=${assetNames.join(',')}`
|
|
80
|
+
}
|
|
81
|
+
let url = urlJoin(this._baseURL, `/addrs/${addresses}/utxo?${query}`)
|
|
82
|
+
let response = await fetch(url)
|
|
83
|
+
let utxos = await response.json()
|
|
84
|
+
|
|
85
|
+
return utxos.map((utxo) => ({
|
|
86
|
+
address: utxo.address,
|
|
87
|
+
txId: utxo.txid,
|
|
88
|
+
confirmations: utxo.confirmations || 0,
|
|
89
|
+
value: utxo.amount,
|
|
90
|
+
vout: utxo.vout,
|
|
91
|
+
height: utxo.height,
|
|
92
|
+
script: utxo.script,
|
|
93
|
+
asset: utxo.asset,
|
|
94
|
+
}))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async fetchTx(txId: string) {
|
|
98
|
+
let url = urlJoin(this._baseURL, `/tx/${txId}`)
|
|
99
|
+
let response = await fetch(url)
|
|
100
|
+
|
|
101
|
+
// change in https://github.com/jprichardson/exodus-rn/pull/4336 may break Insight compatibility
|
|
102
|
+
// we're probably past the point of just spinning up an Insight server and have it plug n' play for Magnifier
|
|
103
|
+
|
|
104
|
+
if (response.status === 404) return null
|
|
105
|
+
return response.json()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async fetchRawTx(txId: string) {
|
|
109
|
+
const url = urlJoin(this._baseURL, `/rawtx/${txId}`)
|
|
110
|
+
const response = await fetch(url)
|
|
111
|
+
const { rawtx } = await response.json()
|
|
112
|
+
return rawtx
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async fetchTxData(addrs: Array, options = {}) {
|
|
116
|
+
if (!Array.isArray(addrs) || addrs.length === 0) return { items: [], totalItems: 0 }
|
|
117
|
+
|
|
118
|
+
options = { noScriptSig: 1, noAsm: 1, noSpent: 0, from: 0, to: 10, ...options }
|
|
119
|
+
let url = urlJoin(this._baseURL, `/addrs/txs`)
|
|
120
|
+
url = `${url}?${qs.stringify(options)}`
|
|
121
|
+
|
|
122
|
+
const fetchOptions = {
|
|
123
|
+
method: 'post',
|
|
124
|
+
headers: {
|
|
125
|
+
Accept: 'application/json',
|
|
126
|
+
'Content-Type': 'application/json',
|
|
127
|
+
},
|
|
128
|
+
body: JSON.stringify({ addrs: addrs.join(',') }),
|
|
129
|
+
timeout: 10000,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return fetchJsonRetry(url, fetchOptions)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async fetchAllTxData(addrs = [], chunk = 25, httpDelay = 2000, shouldStopFetching = () => {}) {
|
|
136
|
+
const txs = []
|
|
137
|
+
while (true) {
|
|
138
|
+
const data = await this.fetchTxData(addrs, { from: txs.length, to: txs.length + chunk })
|
|
139
|
+
if (!Array.isArray(data.items) || data.items.length === 0) break
|
|
140
|
+
|
|
141
|
+
txs.push(...data.items)
|
|
142
|
+
if ((data.totalItems && data.totalItems <= txs.length) || data.items.length < chunk) break
|
|
143
|
+
|
|
144
|
+
if (shouldStopFetching && (await shouldStopFetching(data.items))) break
|
|
145
|
+
|
|
146
|
+
await delay(httpDelay)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return txs
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async fetchUnconfirmedAncestorData(txId: string) {
|
|
153
|
+
const url = urlJoin(this._baseURL, `/unconfirmed_ancestor/${txId}`)
|
|
154
|
+
const response = await fetch(url)
|
|
155
|
+
return response.json()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async fetchFeeRate() {
|
|
159
|
+
const url = urlJoin(this._baseURL, '/v2/fees')
|
|
160
|
+
const response = await fetch(url)
|
|
161
|
+
return response.json()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
broadcastTx = async (rawTx) => {
|
|
165
|
+
console.log('gonna broadcastTx')
|
|
166
|
+
let url = urlJoin(this._baseURL, '/tx/send')
|
|
167
|
+
let fetchOptions = {
|
|
168
|
+
method: 'post',
|
|
169
|
+
headers: {
|
|
170
|
+
Accept: 'application/json',
|
|
171
|
+
'Content-Type': 'application/json',
|
|
172
|
+
},
|
|
173
|
+
body: JSON.stringify({ rawtx: rawTx }),
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let response = await fetch(url, fetchOptions)
|
|
177
|
+
let data = await response.text()
|
|
178
|
+
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
console.warn('Insight Broadcast HTTP Error:')
|
|
181
|
+
console.warn(response.statusText)
|
|
182
|
+
console.warn(data)
|
|
183
|
+
throw new Error(`Insight Broadcast HTTP Error: ${data}`)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
data = JSON.parse(data)
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.warn('Insight Broadcast JSON Parse Error:', err.message, data)
|
|
190
|
+
throw new Error(`data: ${data}`, { cause: err })
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!data.txid) throw new Error('transaction id was not returned')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async getClaimable(address) {
|
|
197
|
+
const url = urlJoin(this._baseURL, `/addr/${address}/claimable`)
|
|
198
|
+
const response = await fetch(url)
|
|
199
|
+
if (!response.ok) throw new Error(`${url} returned ${response.status}: ${response.statusText}`)
|
|
200
|
+
return response.json()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async getUnclaimed(address) {
|
|
204
|
+
const url = urlJoin(this._baseURL, `/addr/${address}/unclaimed`)
|
|
205
|
+
const response = await fetch(url)
|
|
206
|
+
if (!response.ok) throw new Error(`${url} returned ${response.status}: ${response.statusText}`)
|
|
207
|
+
return response.json()
|
|
208
|
+
}
|
|
209
|
+
}
|