From 8c3f3d2535fc1da5292eef04c2b922b9985d6f83 Mon Sep 17 00:00:00 2001 From: kyranjamie Date: Wed, 15 Nov 2023 13:50:41 +0100 Subject: [PATCH] fix: add hiro api key, closes #4518 --- .eslintrc.js | 12 ++++- src/app/common/api/fetch-wrapper.ts | 44 +++++++++++++++++++ src/app/common/api/wrapped-fetch.ts | 31 ------------- src/app/common/utils.ts | 6 --- .../components/bitcoin/ordinals.tsx | 1 - src/app/query/bitcoin/bitcoin-client.ts | 8 +++- .../send-accepted-bitcoin-contract-offer.ts | 3 ++ .../ordinals/brc20/brc20-tokens.query.ts | 12 +++-- .../inscription-text-content.query.ts | 6 +-- .../bitcoin/ordinals/inscription.query.ts | 7 ++- .../ordinals/inscriptions-by-param.query.ts | 7 ++- .../bitcoin/ordinals/inscriptions.query.ts | 3 +- .../bitcoin/stamps/stamps-by-address.query.ts | 6 +-- .../vendors/binance-market-data.query.ts | 10 ++--- .../vendors/coincap-market-data.query.ts | 8 ++-- .../vendors/coingecko-market-data.query.ts | 8 ++-- .../remote-config/remote-config.query.ts | 3 +- src/app/query/stacks/fees/fees.query.ts | 4 +- src/app/query/stacks/network/network.query.ts | 2 +- src/app/query/stacks/utils.ts | 2 +- src/app/query/utils.ts | 4 +- src/shared/constants.ts | 2 + 22 files changed, 106 insertions(+), 83 deletions(-) create mode 100644 src/app/common/api/fetch-wrapper.ts delete mode 100644 src/app/common/api/wrapped-fetch.ts diff --git a/.eslintrc.js b/.eslintrc.js index 14b2024420e..8062907604a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,5 @@ const avoidWindowOpenMsg = 'Use `openInNewTab` helper'; +const avoidFetchMsg = 'Use `axios` instead for consistency with the rest of the project'; module.exports = { parser: '@typescript-eslint/parser', @@ -34,7 +35,11 @@ module.exports = { ignoreReadBeforeAssign: false, }, ], - 'no-restricted-globals': ['error', { name: 'open', message: avoidWindowOpenMsg }], + 'no-restricted-globals': [ + 'error', + { name: 'open', message: avoidWindowOpenMsg }, + { name: 'fetch', message: avoidFetchMsg }, + ], 'no-restricted-properties': [ 'error', { @@ -52,6 +57,11 @@ module.exports = { property: 'close', message: 'Use `closeWindow` utility helper', }, + { + object: 'window', + property: 'fetch', + message: avoidFetchMsg, + }, ], '@typescript-eslint/no-floating-promises': ['warn'], '@typescript-eslint/no-unnecessary-type-assertion': ['warn'], diff --git a/src/app/common/api/fetch-wrapper.ts b/src/app/common/api/fetch-wrapper.ts new file mode 100644 index 00000000000..bedc27697d9 --- /dev/null +++ b/src/app/common/api/fetch-wrapper.ts @@ -0,0 +1,44 @@ +import axios from 'axios'; + +import { HIRO_API_KEY } from '@shared/constants'; + +const leatherHeaders: HeadersInit = { + 'x-leather-version': VERSION, + 'x-hiro-api-key': HIRO_API_KEY, +}; + +/** + * @deprecated Use `axios` directly instead + */ +export function wrappedFetch(input: RequestInfo, init: RequestInit = {}) { + const initHeaders = init.headers || {}; + // eslint-disable-next-line no-restricted-globals + return fetch(input, { + credentials: 'omit', + ...init, + headers: { ...initHeaders, ...leatherHeaders }, + }); +} + +export async function fetchWithTimeout( + input: RequestInfo, + init: RequestInit & { timeout?: number } = {} +) { + const { timeout = 8000, ...options } = init; + + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeout); + + const response = await wrappedFetch(input, { + ...options, + signal: controller.signal, + }); + clearTimeout(id); + + return response; +} + +axios.interceptors.request.use(config => { + if (config.url?.includes('hiro.so')) config.headers['x-hiro-api-key'] = HIRO_API_KEY; + return config; +}); diff --git a/src/app/common/api/wrapped-fetch.ts b/src/app/common/api/wrapped-fetch.ts deleted file mode 100644 index aa8c1695c85..00000000000 --- a/src/app/common/api/wrapped-fetch.ts +++ /dev/null @@ -1,31 +0,0 @@ -const hiroHeaders: HeadersInit = { - 'x-hiro-product': 'stacks-wallet-web', - 'x-hiro-version': VERSION, -}; - -export function fetcher(input: RequestInfo, init: RequestInit = {}) { - const initHeaders = init.headers || {}; - return fetch(input, { - credentials: 'omit', - ...init, - headers: { ...initHeaders, ...hiroHeaders }, - }); -} - -export async function fetchWithTimeout( - input: RequestInfo, - init: RequestInit & { timeout?: number } = {} -) { - const { timeout = 8000, ...options } = init; - - const controller = new AbortController(); - const id = setTimeout(() => controller.abort(), timeout); - - const response = await fetcher(input, { - ...options, - signal: controller.signal, - }); - clearTimeout(id); - - return response; -} diff --git a/src/app/common/utils.ts b/src/app/common/utils.ts index 9c9c9779f09..8c03cf9fb2c 100644 --- a/src/app/common/utils.ts +++ b/src/app/common/utils.ts @@ -8,7 +8,6 @@ import { import { toUnicode } from 'punycode'; import { BitcoinNetworkModes, KEBAB_REGEX } from '@shared/constants'; -import { logger } from '@shared/logger'; import type { Blockchains } from '@shared/models/blockchain.model'; export function createNullArrayOfLength(length: number) { @@ -264,11 +263,6 @@ export function whenStacksChainId(chainId: ChainID) { return (chainIdMap: WhenStacksChainIdMap): T => chainIdMap[chainId]; } -export function logAndThrow(msg: string, args: any[] = []) { - logger.error(msg, ...args); - throw new Error(msg); -} - export const parseIfValidPunycode = (s: string) => { try { return toUnicode(s); diff --git a/src/app/features/collectibles/components/bitcoin/ordinals.tsx b/src/app/features/collectibles/components/bitcoin/ordinals.tsx index af0dc434d16..5bae0628251 100644 --- a/src/app/features/collectibles/components/bitcoin/ordinals.tsx +++ b/src/app/features/collectibles/components/bitcoin/ordinals.tsx @@ -11,7 +11,6 @@ import { Inscription } from './inscription'; interface OrdinalsProps { setIsLoadingMore: (isLoading: boolean) => void; } - export function Ordinals({ setIsLoadingMore }: OrdinalsProps) { const query = useGetInscriptionsInfiniteQuery(); const pages = query.data?.pages; diff --git a/src/app/query/bitcoin/bitcoin-client.ts b/src/app/query/bitcoin/bitcoin-client.ts index fb85849007c..ae6c519326b 100644 --- a/src/app/query/bitcoin/bitcoin-client.ts +++ b/src/app/query/bitcoin/bitcoin-client.ts @@ -1,3 +1,5 @@ +import axios from 'axios'; + import { fetchData } from '../utils'; class Configuration { @@ -112,10 +114,14 @@ class TransactionsApi { constructor(public configuration: Configuration) {} async getBitcoinTransaction(txid: string) { - return fetch(`${this.configuration.baseUrl}/tx/${txid}`).then(res => res.json()); + const resp = await axios.get(`${this.configuration.baseUrl}/tx/${txid}`); + return resp.data; } async broadcastTransaction(tx: string) { + // TODO: refactor to use `axios` + // https://github.com/leather-wallet/extension/issues/4521 + // eslint-disable-next-line no-restricted-globals return fetch(`${this.configuration.baseUrl}/tx`, { method: 'POST', body: tx, diff --git a/src/app/query/bitcoin/contract/send-accepted-bitcoin-contract-offer.ts b/src/app/query/bitcoin/contract/send-accepted-bitcoin-contract-offer.ts index ed61555d9e8..8df410306b7 100644 --- a/src/app/query/bitcoin/contract/send-accepted-bitcoin-contract-offer.ts +++ b/src/app/query/bitcoin/contract/send-accepted-bitcoin-contract-offer.ts @@ -2,6 +2,9 @@ export async function sendAcceptedBitcoinContractOfferToProtocolWallet( acceptedBitcoinContractOffer: string, counterpartyWalletURL: string ) { + // TODO: refactor to use `axios` + // https://github.com/leather-wallet/extension/issues/4521 + // eslint-disable-next-line no-restricted-globals const response = await fetch(`${counterpartyWalletURL}/offer/accept`, { method: 'put', body: JSON.stringify({ diff --git a/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.query.ts b/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.query.ts index 742b583a5de..4096b279438 100644 --- a/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.query.ts +++ b/src/app/query/bitcoin/ordinals/brc20/brc20-tokens.query.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect } from 'react'; import { useInfiniteQuery } from '@tanstack/react-query'; +import axios from 'axios'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; import { createNumArrayOfRange } from '@app/common/utils'; @@ -36,16 +37,13 @@ interface Brc20TokenTicker { } async function fetchTickerData(ticker: string): Promise { - const res = await fetch(`https://brc20api.bestinslot.xyz/v1/get_brc20_ticker/${ticker}`); - if (!res.ok) throw new Error('Failed to fetch BRC-20 token ticker data'); - return res.json(); + const res = await axios.get(`https://brc20api.bestinslot.xyz/v1/get_brc20_ticker/${ticker}`); + return res.data; } async function fetchBrc20TokensByAddress(address: string): Promise { - const res = await fetch(`https://brc20api.bestinslot.xyz/v1/get_brc20_balance/${address}`); - - if (!res.ok) throw new Error('Failed to fetch BRC-20 token balances'); - const tokensData = await res.json(); + const res = await axios.get(`https://brc20api.bestinslot.xyz/v1/get_brc20_balance/${address}`); + const tokensData = res.data; const tickerPromises = tokensData.map((token: Brc20TokenResponse) => { return fetchTickerData(token.tick); diff --git a/src/app/query/bitcoin/ordinals/inscription-text-content.query.ts b/src/app/query/bitcoin/ordinals/inscription-text-content.query.ts index 47cf1e2004b..a233d28aa16 100644 --- a/src/app/query/bitcoin/ordinals/inscription-text-content.query.ts +++ b/src/app/query/bitcoin/ordinals/inscription-text-content.query.ts @@ -1,11 +1,11 @@ import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; import { QueryPrefixes } from '@app/query/query-prefixes'; async function getInscriptionTextContent(src: string) { - const res = await fetch(src); - if (!res.ok) throw new Error('Failed to fetch ordinal text content'); - return res.text(); + const res = await axios.get(src, { responseType: 'text' }); + return res.data; } export function useInscriptionTextContentQuery(contentSrc: string) { diff --git a/src/app/query/bitcoin/ordinals/inscription.query.ts b/src/app/query/bitcoin/ordinals/inscription.query.ts index 721ff17b106..5e77ae41ccc 100644 --- a/src/app/query/bitcoin/ordinals/inscription.query.ts +++ b/src/app/query/bitcoin/ordinals/inscription.query.ts @@ -1,4 +1,5 @@ import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; import { HIRO_INSCRIPTIONS_API_URL } from '@shared/constants'; import { Inscription } from '@shared/models/inscription.model'; @@ -16,10 +17,8 @@ const inscriptionQueryOptions = { */ function fetchInscription() { return async (id: string) => { - const res = await fetch(`${HIRO_INSCRIPTIONS_API_URL}/${id}`); - if (!res.ok) throw new Error('Error retrieving inscription metadata'); - const data = await res.json(); - return data as Inscription; + const res = await axios.get(`${HIRO_INSCRIPTIONS_API_URL}/${id}`); + return res.data as Inscription; }; } diff --git a/src/app/query/bitcoin/ordinals/inscriptions-by-param.query.ts b/src/app/query/bitcoin/ordinals/inscriptions-by-param.query.ts index 8eed26a5b3d..253b080a5b5 100644 --- a/src/app/query/bitcoin/ordinals/inscriptions-by-param.query.ts +++ b/src/app/query/bitcoin/ordinals/inscriptions-by-param.query.ts @@ -1,6 +1,7 @@ import * as btc from '@scure/btc-signer'; import { bytesToHex } from '@stacks/common'; import { useQueries, useQuery } from '@tanstack/react-query'; +import axios from 'axios'; import { HIRO_INSCRIPTIONS_API_URL } from '@shared/constants'; import { Paginated } from '@shared/models/api-types'; @@ -13,10 +14,8 @@ type FetchInscriptionResp = Awaited { - const res = await fetch(`${HIRO_INSCRIPTIONS_API_URL}?${param}`); - if (!res.ok) throw new Error('Error retrieving inscription metadata'); - const data = await res.json(); - return data as Paginated; + const res = await axios.get(`${HIRO_INSCRIPTIONS_API_URL}?${param}`); + return res.data as Paginated; }; } diff --git a/src/app/query/bitcoin/ordinals/inscriptions.query.ts b/src/app/query/bitcoin/ordinals/inscriptions.query.ts index 474a9f8f4a5..6769c5c9cd3 100644 --- a/src/app/query/bitcoin/ordinals/inscriptions.query.ts +++ b/src/app/query/bitcoin/ordinals/inscriptions.query.ts @@ -7,6 +7,7 @@ import { getTaprootAddress } from '@shared/crypto/bitcoin/bitcoin.utils'; import { InscriptionResponseItem } from '@shared/models/inscription.model'; import { ensureArray } from '@shared/utils'; +import { wrappedFetch } from '@app/common/api/fetch-wrapper'; import { createNumArrayOfRange } from '@app/common/utils'; import { QueryPrefixes } from '@app/query/query-prefixes'; import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; @@ -41,7 +42,7 @@ async function fetchInscriptions(addresses: string | string[], offset = 0, limit params.append('limit', limit.toString()); params.append('offset', offset.toString()); - const res = await fetch(`${HIRO_INSCRIPTIONS_API_URL}?${params.toString()}`); + const res = await wrappedFetch(`${HIRO_INSCRIPTIONS_API_URL}?${params.toString()}`); if (!res.ok) throw new Error('Error retrieving inscription metadata'); const data = await res.json(); return data as InscriptionsQueryResponse; diff --git a/src/app/query/bitcoin/stamps/stamps-by-address.query.ts b/src/app/query/bitcoin/stamps/stamps-by-address.query.ts index a30c7cc0db8..efebb81e652 100644 --- a/src/app/query/bitcoin/stamps/stamps-by-address.query.ts +++ b/src/app/query/bitcoin/stamps/stamps-by-address.query.ts @@ -1,4 +1,5 @@ import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; import { AppUseQueryConfig } from '@app/query/query-config'; import { QueryPrefixes } from '@app/query/query-prefixes'; @@ -16,9 +17,8 @@ export interface Stamp { } async function fetchStampsByAddress(address: string): Promise { - return fetch(`https://stampchain.io/api/stamps?wallet_address=${address}`).then(res => - res.json() - ); + const resp = await axios.get(`https://stampchain.io/api/stamps?wallet_address=${address}`); + return resp.data; } type FetchStampsByAddressResp = Awaited>; diff --git a/src/app/query/common/market-data/vendors/binance-market-data.query.ts b/src/app/query/common/market-data/vendors/binance-market-data.query.ts index f8c3968c555..7a0bccb2e67 100644 --- a/src/app/query/common/market-data/vendors/binance-market-data.query.ts +++ b/src/app/query/common/market-data/vendors/binance-market-data.query.ts @@ -1,15 +1,15 @@ import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; import { CryptoCurrencies } from '@shared/models/currencies.model'; -import { logAndThrow } from '@app/common/utils'; - import { marketDataQueryOptions } from '../market-data.query'; async function fetchBinanceMarketData(currency: CryptoCurrencies) { - const resp = await fetch(`https://api1.binance.com/api/v3/ticker/price?symbol=${currency}USDT`); - if (!resp.ok) logAndThrow('Cannot load binance data'); - return resp.json(); + const resp = await axios.get( + `https://api1.binance.com/api/v3/ticker/price?symbol=${currency}USDT` + ); + return resp.data; } export function selectBinanceUsdPrice(resp: any) { diff --git a/src/app/query/common/market-data/vendors/coincap-market-data.query.ts b/src/app/query/common/market-data/vendors/coincap-market-data.query.ts index f0000550895..d1a1c1a3b77 100644 --- a/src/app/query/common/market-data/vendors/coincap-market-data.query.ts +++ b/src/app/query/common/market-data/vendors/coincap-market-data.query.ts @@ -1,9 +1,8 @@ import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; import { CryptoCurrencies } from '@shared/models/currencies.model'; -import { logAndThrow } from '@app/common/utils'; - import { marketDataQueryOptions } from '../market-data.query'; const currencyNameMap: Record = { @@ -12,9 +11,8 @@ const currencyNameMap: Record = { }; async function fetchCoincapMarketData(currency: CryptoCurrencies) { - const resp = await fetch(`https://api.coincap.io/v2/assets/${currencyNameMap[currency]}`); - if (!resp.ok) logAndThrow('Cannot load coincap data'); - return resp.json(); + const resp = await axios.get(`https://api.coincap.io/v2/assets/${currencyNameMap[currency]}`); + return resp.data; } export function selectCoincapUsdPrice(resp: any) { diff --git a/src/app/query/common/market-data/vendors/coingecko-market-data.query.ts b/src/app/query/common/market-data/vendors/coingecko-market-data.query.ts index 5d847fab735..1021db95000 100644 --- a/src/app/query/common/market-data/vendors/coingecko-market-data.query.ts +++ b/src/app/query/common/market-data/vendors/coingecko-market-data.query.ts @@ -1,9 +1,8 @@ import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; import { CryptoCurrencies } from '@shared/models/currencies.model'; -import { logAndThrow } from '@app/common/utils'; - import { marketDataQueryOptions } from '../market-data.query'; const currencyNameMap: Record = { @@ -12,11 +11,10 @@ const currencyNameMap: Record = { }; async function fetchCoingeckoMarketData(currency: CryptoCurrencies) { - const resp = await fetch( + const resp = await axios.get( `https://api.coingecko.com/api/v3/simple/price?ids=${currencyNameMap[currency]}&vs_currencies=usd` ); - if (!resp.ok) logAndThrow('Cannot load coingecko data'); - return resp.json(); + return resp.data; } export function selectCoingeckoUsdPrice(resp: any) { diff --git a/src/app/query/common/remote-config/remote-config.query.ts b/src/app/query/common/remote-config/remote-config.query.ts index 4c22148d04b..12a6df3a7c1 100644 --- a/src/app/query/common/remote-config/remote-config.query.ts +++ b/src/app/query/common/remote-config/remote-config.query.ts @@ -1,4 +1,5 @@ import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; import get from 'lodash.get'; import { GITHUB_ORG, GITHUB_REPO } from '@shared/constants'; @@ -76,7 +77,7 @@ const githubWalletConfigRawUrl = `https://raw.githubusercontent.com/${GITHUB_ORG async function fetchHiroMessages(): Promise { if ((!BRANCH_NAME && WALLET_ENVIRONMENT !== 'production') || IS_TEST_ENV) return localConfig as RemoteConfig; - return fetch(githubWalletConfigRawUrl).then(msg => msg.json()); + return axios.get(githubWalletConfigRawUrl); } function useRemoteConfig() { diff --git a/src/app/query/stacks/fees/fees.query.ts b/src/app/query/stacks/fees/fees.query.ts index 1e1b7eae8a0..e0e0d540308 100644 --- a/src/app/query/stacks/fees/fees.query.ts +++ b/src/app/query/stacks/fees/fees.query.ts @@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import { StacksTxFeeEstimation } from '@shared/models/fees/stacks-fees.model'; -import { fetcher } from '@app/common/api/wrapped-fetch'; +import { wrappedFetch } from '@app/common/api/fetch-wrapper'; import { AppUseQueryConfig } from '@app/query/query-config'; import { useCurrentNetworkState } from '@app/store/networks/networks.hooks'; @@ -11,7 +11,7 @@ import { RateLimiter, useHiroApiRateLimiter } from '../rate-limiter'; function fetchTransactionFeeEstimation(currentNetwork: any, limiter: RateLimiter) { return async (estimatedLen: number | null, transactionPayload: string) => { await limiter.removeTokens(1); - const resp = await fetcher(currentNetwork.chain.stacks.url + '/v2/fees/transaction', { + const resp = await wrappedFetch(currentNetwork.chain.stacks.url + '/v2/fees/transaction', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ diff --git a/src/app/query/stacks/network/network.query.ts b/src/app/query/stacks/network/network.query.ts index 86fe0bedd5b..1e0af93804c 100644 --- a/src/app/query/stacks/network/network.query.ts +++ b/src/app/query/stacks/network/network.query.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; -import { fetchWithTimeout } from '@app/common/api/wrapped-fetch'; +import { fetchWithTimeout } from '@app/common/api/fetch-wrapper'; import { RateLimiter, useHiroApiRateLimiter } from '../rate-limiter'; diff --git a/src/app/query/stacks/utils.ts b/src/app/query/stacks/utils.ts index b9591a9188a..901fda91fe5 100644 --- a/src/app/query/stacks/utils.ts +++ b/src/app/query/stacks/utils.ts @@ -3,7 +3,7 @@ import { Configuration, Middleware, RequestContext } from '@stacks/blockchain-ap import { MICROBLOCKS_ENABLED } from '@shared/constants'; -import { fetcher as fetchApi } from '@app/common/api/wrapped-fetch'; +import { wrappedFetch as fetchApi } from '@app/common/api/fetch-wrapper'; const unanchoredStacksMiddleware: Middleware = { pre: (context: RequestContext) => { diff --git a/src/app/query/utils.ts b/src/app/query/utils.ts index 9fd76eb7a03..02827bf384e 100644 --- a/src/app/query/utils.ts +++ b/src/app/query/utils.ts @@ -1,10 +1,12 @@ +import { wrappedFetch } from '@app/common/api/fetch-wrapper'; + interface FetchDataArgs { errorMsg: string; url: string; } export async function fetchData({ errorMsg, url }: FetchDataArgs) { - const response = await fetch(url); + const response = await wrappedFetch(url); if (!response.ok) { throw new Error(errorMsg); } diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 322db7c608e..9c19c09aa60 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -79,6 +79,8 @@ export interface NetworkConfiguration { }; } +export const HIRO_API_KEY = 'leather_i2LDUpJTyVZcLz51EoY57QjYr8fx8vvX'; + export const HIRO_API_BASE_URL_MAINNET = 'https://api.hiro.so'; export const HIRO_API_BASE_URL_TESTNET = 'https://api.testnet.hiro.so'; export const HIRO_INSCRIPTIONS_API_URL = 'https://api.hiro.so/ordinals/v1/inscriptions';