@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 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,4 @@
1
+ import { ecc } from './ecc'
2
+ import { scriptClassify } from './script-classify'
3
+
4
+ export { ecc, scriptClassify }
@@ -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,7 @@
1
+ export const getBtcVersions = (coinInfo) => ({
2
+ p2pkh: coinInfo.versions.public,
3
+ p2sh: coinInfo.versions.scripthash,
4
+ bech32: coinInfo.bech32,
5
+ segwit: `${coinInfo.bech32}1q`,
6
+ taproot: `${coinInfo.bech32}1p`,
7
+ })
@@ -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
+ }