Skip to content

Commit

Permalink
feat: add blockstream and hiro api rate limiters, closes #4926
Browse files Browse the repository at this point in the history
  • Loading branch information
alter-eggo committed Mar 23, 2024
1 parent 5f08a9c commit 6ab31f4
Show file tree
Hide file tree
Showing 33 changed files with 356 additions and 157 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -206,13 +206,13 @@
"jotai-redux": "0.2.1",
"jsontokens": "4.0.1",
"ledger-bitcoin": "0.2.3",
"limiter": "2.1.0",
"lodash.get": "4.4.2",
"lodash.isequal": "4.5.0",
"lodash.uniqby": "4.7.0",
"micro-packed": "0.3.2",
"object-hash": "3.0.0",
"observable-hooks": "4.2.3",
"p-queue": "8.0.1",
"pino": "8.19.0",
"postcss-preset-env": "9.4.0",
"prism-react-renderer": "2.3.1",
Expand Down
36 changes: 23 additions & 13 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions src/app/common/hooks/account/use-account-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ export function useCurrentAccountDisplayName() {
}

export function useAccountDisplayName({ address, index }: { index: number; address: string }) {
const { data: names = [] } = useGetAccountNamesByAddressQuery(address);
const { data: names = [], isLoading } = useGetAccountNamesByAddressQuery(address);
return useMemo(() => {
if (names[0]) return parseIfValidPunycode(names[0]);
return getAutogeneratedAccountDisplayName(index);
}, [names, index]);
const name = names[0] || getAutogeneratedAccountDisplayName(index);
return {
name,
isLoading,
};
}, [names, index, isLoading]);
}
15 changes: 11 additions & 4 deletions src/app/components/account-total-balance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,25 @@ import { styled } from 'leather-styles/jsx';

import { useTotalBalance } from '@app/common/hooks/balance/use-total-balance';

import { shimmerStyles } from '../../../theme/global/shimmer-styles';

interface AccountTotalBalanceProps {
btcAddress: string;
stxAddress: string;
}
export const AccountTotalBalance = memo(({ btcAddress, stxAddress }: AccountTotalBalanceProps) => {
const totalBalance = useTotalBalance({ btcAddress, stxAddress });
const { totalUsdBalance, isLoading } = useTotalBalance({ btcAddress, stxAddress });

if (!totalBalance) return null;
if (!totalUsdBalance) return null;

return (
<styled.span fontWeight={500} textStyle="label.02">
{totalBalance.totalUsdBalance}
<styled.span
fontWeight={500}
textStyle="label.02"
data-state={isLoading ? 'loading' : undefined}
className={shimmerStyles}
>
{totalUsdBalance}
</styled.span>
);
});
14 changes: 12 additions & 2 deletions src/app/components/account/account-name.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,21 @@ import { memo } from 'react';

import { styled } from 'leather-styles/jsx';

import { shimmerStyles } from '../../../../theme/global/shimmer-styles';

interface AccountNameLayoutProps {
children: React.ReactNode;
isLoading?: boolean;
}
export const AccountNameLayout = memo(({ children }: AccountNameLayoutProps) => (
<styled.span fontWeight={500} textStyle="label.02">

export const AccountNameLayout = memo(({ children, isLoading }: AccountNameLayoutProps) => (
<styled.span
fontWeight={500}
textStyle="label.02"
aria-busy={isLoading}
data-state={isLoading ? 'loading' : undefined}
className={shimmerStyles}
>
{children}
</styled.span>
));
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function CollectibleImage(props: CollectibleImageProps) {
<img
alt={alt}
onError={() => setIsError(true)}
loading="lazy"
onLoad={event => {
const target = event.target as HTMLImageElement;
setWidth(target.naturalWidth);
Expand All @@ -37,7 +38,8 @@ export function CollectibleImage(props: CollectibleImageProps) {
height: '100%',
aspectRatio: '1 / 1',
objectFit: 'cover',
display: isLoading ? 'none' : 'inherit',
// display: 'none' breaks onLoad event firing
opacity: isLoading ? '0' : '1',
imageRendering: width <= 40 ? 'pixelated' : 'auto',
}}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ export const SwitchAccountListItem = memo(
'SWITCH_ACCOUNTS' + stxAddress || btcAddress
);
const { handleSwitchAccount } = useSwitchAccount(handleClose);
const name = useAccountDisplayName({ address: stxAddress, index });
const { name, isLoading: isLoadingBnsName } = useAccountDisplayName({
address: stxAddress,
index,
});

const handleClick = async () => {
setIsLoading();
Expand All @@ -41,7 +44,7 @@ export const SwitchAccountListItem = memo(
return (
<AccountListItemLayout
accountAddresses={<AcccountAddresses index={index} />}
accountName={<AccountNameLayout>{name}</AccountNameLayout>}
accountName={<AccountNameLayout isLoading={isLoadingBnsName}>{name}</AccountNameLayout>}
avatar={
<AccountAvatarItem
index={index}
Expand Down
2 changes: 1 addition & 1 deletion src/app/pages/choose-account/components/accounts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ interface ChooseAccountItemProps extends FlexProps {
}
const ChooseAccountItem = memo(
({ account, isLoading, onSelectAccount }: ChooseAccountItemProps) => {
const name = useAccountDisplayName(account);
const { name } = useAccountDisplayName(account);
const btcAddress = useNativeSegwitAccountIndexAddressIndexZero(account.index);

const accountSlug = useMemo(() => slugify(`Account ${account?.index + 1}`), [account?.index]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SettingsSelectors } from '@tests/selectors/settings.selectors';
import { Box, Flex, Stack, styled } from 'leather-styles/jsx';
import { Flex, Stack, styled } from 'leather-styles/jsx';

import { NetworkConfiguration } from '@shared/constants';

Expand Down Expand Up @@ -30,22 +30,17 @@ export function NetworkListItemLayout({
const unSelectable = !isOnline || isActive;
return (
<Flex data-testid={SettingsSelectors.NetworkListItem}>
<Box
<Button
width="100%"
variant="ghost"
key={networkId}
_hover={
unSelectable
? undefined
: {
backgroundColor: 'ink.component-background-hover',
}
}
px="space.05"
py="space.04"
onClick={unSelectable ? undefined : onSelectNetwork}
cursor={!isOnline ? 'not-allowed' : isActive ? 'default' : 'pointer'}
opacity={!isOnline ? 0.5 : 1}
data-testid={network.id}
aria-disabled={unSelectable}
>
<Flex width="100%" justifyContent="space-between" alignItems="center">
<Stack alignItems="flex-start" flex={1} gap="space.02">
Expand All @@ -70,7 +65,7 @@ export function NetworkListItemLayout({
<TrashIcon />
</Button>
)}
</Box>
</Button>
</Flex>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const AccountListItem = memo(({ index, stacksAccount, onClose }: AccountL
BitcoinSendFormValues | StacksSendFormValues
>();
const stacksAddress = stacksAccount?.address || '';
const name = useAccountDisplayName({ address: stacksAddress, index });
const { name } = useAccountDisplayName({ address: stacksAddress, index });

const bitcoinSigner = useNativeSegwitSigner(index);
const bitcoinAddress = bitcoinSigner?.(0).address || '';
Expand Down
10 changes: 7 additions & 3 deletions src/app/query/bitcoin/address/transactions-by-address.query.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useQueries, useQuery } from '@tanstack/react-query';
import { type QueryFunctionContext, useQueries, useQuery } from '@tanstack/react-query';

import { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';

Expand All @@ -18,7 +18,9 @@ export function useGetBitcoinTransactionsByAddressQuery<T extends unknown = Bitc
return useQuery({
enabled: !!address,
queryKey: ['btc-txs-by-address', address],
queryFn: () => client.addressApi.getTransactionsByAddress(address),
queryFn: async ({ signal }) => {
return client.addressApi.getTransactionsByAddress(address, signal);
},
...queryOptions,
...options,
});
Expand All @@ -35,7 +37,9 @@ export function useGetBitcoinTransactionsByAddressesQuery<T extends unknown = Bi
return {
enabled: !!address,
queryKey: ['btc-txs-by-address', address],
queryFn: () => client.addressApi.getTransactionsByAddress(address),
queryFn: async ({ signal }: QueryFunctionContext<string[], any>) => {
return client.addressApi.getTransactionsByAddress(address, signal);
},
...queryOptions,
...options,
};
Expand Down
5 changes: 4 additions & 1 deletion src/app/query/bitcoin/address/utxos-by-address.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ export function useGetUtxosByAddressQuery<T extends unknown = UtxoResponseItem[]
options?: AppUseQueryConfig<UtxoResponseItem[], T>
) {
const client = useBitcoinClient();

return useQuery({
enabled: !!address,
queryKey: ['btc-utxos-by-address', address],
queryFn: () => client.addressApi.getUtxosByAddress(address),
queryFn: async ({ signal }) => {
return client.addressApi.getUtxosByAddress(address, signal);
},
...queryOptions,
...options,
});
Expand Down
23 changes: 17 additions & 6 deletions src/app/query/bitcoin/bitcoin-client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import axios from 'axios';
import PQueue from 'p-queue';

import { HIRO_API_BASE_URL_MAINNET } from '@shared/constants';
import { Paginated } from '@shared/models/api-types';
import type { BitcoinTx } from '@shared/models/transactions/bitcoin-transaction.model';

import { getBlockstreamRatelimiter } from './blockstream-rate-limiter';

class Configuration {
constructor(public baseUrl: string) {}
Expand Down Expand Up @@ -162,16 +166,23 @@ class HiroApi {
}

class AddressApi {
constructor(public configuration: Configuration) {}
rateLimiter: PQueue;
constructor(public configuration: Configuration) {
this.rateLimiter = getBlockstreamRatelimiter(this.configuration.baseUrl);
}

async getTransactionsByAddress(address: string) {
const resp = await axios.get(`${this.configuration.baseUrl}/address/${address}/txs`);
async getTransactionsByAddress(address: string, signal?: AbortSignal) {
const resp = await this.rateLimiter.add(
() => axios.get<BitcoinTx[]>(`${this.configuration.baseUrl}/address/${address}/txs`),
{ signal, throwOnTimeout: true }
);
return resp.data;
}

async getUtxosByAddress(address: string): Promise<UtxoResponseItem[]> {
const resp = await axios.get<UtxoResponseItem[]>(
`${this.configuration.baseUrl}/address/${address}/utxo`
async getUtxosByAddress(address: string, signal?: AbortSignal): Promise<UtxoResponseItem[]> {
const resp = await this.rateLimiter.add(
() => axios.get<UtxoResponseItem[]>(`${this.configuration.baseUrl}/address/${address}/utxo`),
{ signal, priority: 1, throwOnTimeout: true }
);
return resp.data.sort((a, b) => a.vout - b.vout);
}
Expand Down
18 changes: 18 additions & 0 deletions src/app/query/bitcoin/blockstream-rate-limiter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import PQueue from 'p-queue';

import { BITCOIN_API_BASE_URL_TESTNET } from '@shared/constants';

const blockstreamMainnetApiLimiter = new PQueue({
interval: 5000,
intervalCap: 20,
});

const blockstreamTestnetApiLimiter = new PQueue({
interval: 5000,
intervalCap: 30,
});

export function getBlockstreamRatelimiter(url: string) {
if (url.includes(BITCOIN_API_BASE_URL_TESTNET)) return blockstreamTestnetApiLimiter;
return blockstreamMainnetApiLimiter;
}
Loading

0 comments on commit 6ab31f4

Please sign in to comment.