Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Geo blocking with 451 #382

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/app/api/getHealthCheck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import axios, { isAxiosError } from "axios";

import {
API_ERROR_MESSAGE,
GEO_BLOCK_MESSAGE,
HealthCheckResult,
HealthCheckStatus,
} from "../types/healthCheck";

interface HealthCheckResponse {
data: string;
}

export const getHealthCheck = async (): Promise<HealthCheckResult> => {
try {
const response = await axios.get(
`${process.env.NEXT_PUBLIC_API_URL}/healthcheck`,
);
const healthCheckAPIResponse: HealthCheckResponse = response.data;
// If the response has a data field, it's a normal response
if (healthCheckAPIResponse.data) {
return {
status: HealthCheckStatus.Normal,
message: healthCheckAPIResponse.data,
};
} else {
// Something went wrong
throw new Error(API_ERROR_MESSAGE);
}
} catch (error: Error | any) {
// Geo-blocking is a custom status code
if (
isAxiosError(error) &&
(error.response?.status === 451 || error.request.status === 451)
) {
return {
status: HealthCheckStatus.GeoBlocked,
message: error.request?.response?.message || GEO_BLOCK_MESSAGE,
};
} else {
return {
status: HealthCheckStatus.Error,
message:
error.request?.response?.message ||
error?.message ||
API_ERROR_MESSAGE,
};
}
}
};
54 changes: 46 additions & 8 deletions src/app/components/Connect/ConnectSmall.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { useRef, useState } from "react";
import { AiOutlineInfoCircle } from "react-icons/ai";
import { FaBitcoin } from "react-icons/fa";
import { IoMdClose } from "react-icons/io";
import { PiWalletBold } from "react-icons/pi";
import { Tooltip } from "react-tooltip";
import { useOnClickOutside } from "usehooks-ts";

import {
API_ERROR_MESSAGE,
GEO_BLOCK_MESSAGE,
HealthCheckResult,
HealthCheckStatus,
} from "@/app/types/healthCheck";
import { getNetworkConfig } from "@/config/network.config";
import { satoshiToBtc } from "@/utils/btcConversions";
import { maxDecimals } from "@/utils/maxDecimals";
Expand All @@ -17,13 +25,15 @@ interface ConnectSmallProps {
address: string;
btcWalletBalanceSat?: number;
onDisconnect: () => void;
apiAvailable?: HealthCheckResult;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this ??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is undefined before we fetch and get into one of the normal - geoblocking - error state

}

export const ConnectSmall: React.FC<ConnectSmallProps> = ({
onConnect,
address,
btcWalletBalanceSat,
onDisconnect,
apiAvailable,
}) => {
const [showMenu, setShowMenu] = useState(false);
const handleClickOutside = () => {
Expand All @@ -35,6 +45,31 @@ export const ConnectSmall: React.FC<ConnectSmallProps> = ({

const { coinName, networkName } = getNetworkConfig();

const renderApiAvailableTooltip = () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const renderApiAvailableTooltip = () => {
const renderApiNotAvailableTooltip = () => {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also have a comment describing what this function's purpose is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

if (!apiAvailable) return null;

const message =
apiAvailable.status === HealthCheckStatus.GeoBlocked
? GEO_BLOCK_MESSAGE
: API_ERROR_MESSAGE;

return (
<>
<span
className="cursor-pointer text-xs"
data-tooltip-id={`tooltip-connect-${apiAvailable.status}`}
data-tooltip-content={message}
data-tooltip-place="bottom"
>
<AiOutlineInfoCircle />
</span>
<Tooltip id={`tooltip-connect-${apiAvailable.status}`} />
</>
);
};

const isApiNormal = apiAvailable?.status === HealthCheckStatus.Normal;

return address ? (
<div className="relative mr-[-10px] text-sm hidden md:flex" ref={ref}>
<button
Expand Down Expand Up @@ -92,13 +127,16 @@ export const ConnectSmall: React.FC<ConnectSmallProps> = ({
)}
</div>
) : (
<button
className="btn-primary btn h-[2.5rem] min-h-[2.5rem] rounded-full px-2 text-white md:rounded-lg"
onClick={onConnect}
disabled={!!address}
>
<PiWalletBold size={20} className="flex md:hidden" />
<span className="hidden md:flex">Connect to {networkName} network</span>
</button>
<div className="flex items-center gap-1">
<button
className="btn-primary btn h-[2.5rem] min-h-[2.5rem] rounded-full px-2 text-white md:rounded-lg"
onClick={onConnect}
disabled={!!address || !isApiNormal}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a comment denoting that we disable the button if the API is not healthy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

>
<PiWalletBold size={20} className="flex md:hidden" />
<span className="hidden md:flex">Connect to {networkName} network</span>
</button>
{!isApiNormal && renderApiAvailableTooltip()}
</div>
);
};
5 changes: 5 additions & 0 deletions src/app/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { HealthCheckResult } from "@/app/types/healthCheck";

import { ConnectSmall } from "../Connect/ConnectSmall";
import { ConnectedSmall } from "../Connect/ConnectedSmall";
import { TestingInfo } from "../TestingInfo/TestingInfo";
Expand All @@ -10,13 +12,15 @@ interface HeaderProps {
address: string;
btcWalletBalanceSat?: number;
onDisconnect: () => void;
apiAvailable?: HealthCheckResult;
}

export const Header: React.FC<HeaderProps> = ({
onConnect,
address,
btcWalletBalanceSat,
onDisconnect,
apiAvailable,
}) => {
return (
<nav>
Expand All @@ -33,6 +37,7 @@ export const Header: React.FC<HeaderProps> = ({
address={address}
btcWalletBalanceSat={btcWalletBalanceSat}
onDisconnect={onDisconnect}
apiAvailable={apiAvailable}
/>
<ThemeToggle />
</div>
Expand Down
7 changes: 7 additions & 0 deletions src/app/components/Staking/Form/States/api-not-available.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/app/components/Staking/Form/States/geo-restricted.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 37 additions & 7 deletions src/app/components/Staking/Staking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import { useStakingStats } from "@/app/context/api/StakingStatsProvider";
import { Delegation } from "@/app/types/delegations";
import { ErrorHandlerParam, ErrorState } from "@/app/types/errors";
import { FinalityProvider as FinalityProviderInterface } from "@/app/types/finalityProviders";
import {
API_ERROR_MESSAGE,
GEO_BLOCK_MESSAGE,
HealthCheckResult,
HealthCheckStatus,
} from "@/app/types/healthCheck";
import { getNetworkConfig } from "@/config/network.config";
import {
createStakingTx,
Expand All @@ -39,6 +45,8 @@ import { StakingFee } from "./Form/StakingFee";
import { StakingTime } from "./Form/StakingTime";
import { Message } from "./Form/States/Message";
import { WalletNotConnected } from "./Form/States/WalletNotConnected";
import apiNotAvailable from "./Form/States/api-not-available.svg";
import geoRestricted from "./Form/States/geo-restricted.svg";
import stakingCapReached from "./Form/States/staking-cap-reached.svg";
import stakingNotStarted from "./Form/States/staking-not-started.svg";
import stakingUpgrading from "./Form/States/staking-upgrading.svg";
Expand All @@ -65,6 +73,7 @@ interface StakingProps {
publicKeyNoCoord: string;
setDelegationsLocalStorage: Dispatch<SetStateAction<Delegation[]>>;
availableUTXOs?: UTXO[] | undefined;
apiAvailable?: HealthCheckResult;
}

export const Staking: React.FC<StakingProps> = ({
Expand All @@ -83,6 +92,7 @@ export const Staking: React.FC<StakingProps> = ({
setDelegationsLocalStorage,
btcWalletBalanceSat,
availableUTXOs,
apiAvailable,
}) => {
// Staking form state
const [stakingAmountSat, setStakingAmountSat] = useState(0);
Expand Down Expand Up @@ -484,15 +494,35 @@ export const Staking: React.FC<StakingProps> = ({

const renderStakingForm = () => {
// States of the staking form:
// 1. Wallet is not connected
if (!isWalletConnected) {
// Health check failed
if (!apiAvailable || apiAvailable.status === HealthCheckStatus.Error) {
return (
<Message
title="Staking is not available"
messages={[API_ERROR_MESSAGE]}
icon={apiNotAvailable}
/>
);
}
// Geo blocked
else if (apiAvailable.status === HealthCheckStatus.GeoBlocked) {
return (
<Message
title="Staking is not available"
messages={[GEO_BLOCK_MESSAGE]}
icon={geoRestricted}
/>
);
}
// Wallet is not connected
else if (!isWalletConnected) {
return <WalletNotConnected onConnect={onConnect} />;
}
// 2. Wallet is connected but we are still loading the staking params
// Wallet is connected but we are still loading the staking params
else if (isLoading) {
return <LoadingView />;
}
// 3. Staking has not started yet
// Staking has not started yet
else if (isBlockHeightUnderActivation) {
return (
<Message
Expand All @@ -504,7 +534,7 @@ export const Staking: React.FC<StakingProps> = ({
/>
);
}
// 4. Staking params upgrading
// Staking params upgrading
else if (isUpgrading) {
return (
<Message
Expand All @@ -516,11 +546,11 @@ export const Staking: React.FC<StakingProps> = ({
/>
);
}
// 5. Staking cap reached
// Staking cap reached
else if (overflow.overTheCapRange) {
return showOverflowWarning(overflow);
}
// 6. Staking form
// Staking form
else {
const {
minStakingAmountSat,
Expand Down
24 changes: 24 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
PaginatedFinalityProviders,
} from "./api/getFinalityProviders";
import { getGlobalParams } from "./api/getGlobalParams";
import { getHealthCheck } from "./api/getHealthCheck";
import { UTXO_KEY } from "./common/constants";
import { signPsbtTransaction } from "./common/utils/psbt";
import { Delegations } from "./components/Delegations/Delegations";
Expand Down Expand Up @@ -55,6 +56,18 @@ const Home: React.FC<HomeProps> = () => {
useError();
const { isTermsOpen, closeTerms } = useTerms();

const {
data: apiAvailable,
error: apiAvailableError,
isError: hasApiAvailableError,
refetch: refetchApiAvailable,
} = useQuery({
queryKey: ["api available"],
queryFn: getHealthCheck,
refetchOnMount: false,
refetchOnWindowFocus: false,
});

const {
data: paramWithContext,
isLoading: isLoadingCurrentParams,
Expand Down Expand Up @@ -224,6 +237,12 @@ const Home: React.FC<HomeProps> = () => {
errorState: ErrorState.SERVER_ERROR,
refetchFunction: refetchAvailableUTXOs,
});
handleError({
error: apiAvailableError,
hasError: hasApiAvailableError,
errorState: ErrorState.SERVER_ERROR,
refetchFunction: refetchApiAvailable,
});
}, [
hasFinalityProvidersError,
hasGlobalParamsVersionError,
Expand All @@ -239,6 +258,9 @@ const Home: React.FC<HomeProps> = () => {
availableUTXOsError,
hasAvailableUTXOsError,
refetchAvailableUTXOs,
apiAvailableError,
hasApiAvailableError,
refetchApiAvailable,
]);

// Initializing btc curve is a required one-time operation
Expand Down Expand Up @@ -393,6 +415,7 @@ const Home: React.FC<HomeProps> = () => {
onDisconnect={handleDisconnectBTC}
address={address}
btcWalletBalanceSat={btcWalletBalanceSat}
apiAvailable={apiAvailable}
/>
<div className="container mx-auto flex justify-center p-6">
<div className="container flex flex-col gap-6">
Expand Down Expand Up @@ -422,6 +445,7 @@ const Home: React.FC<HomeProps> = () => {
publicKeyNoCoord={publicKeyNoCoord}
setDelegationsLocalStorage={setDelegationsLocalStorage}
availableUTXOs={availableUTXOs}
apiAvailable={apiAvailable}
/>
{btcWallet &&
delegations &&
Expand Down
15 changes: 15 additions & 0 deletions src/app/types/healthCheck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export type HealthCheckResult =
| { status: HealthCheckStatus.Normal; message: string }
| { status: HealthCheckStatus.GeoBlocked; message: string }
| { status: HealthCheckStatus.Error; message: string };

export enum HealthCheckStatus {
Normal = "normal",
GeoBlocked = "geoblocked",
Error = "error",
}

export const API_ERROR_MESSAGE =
"Error occurred while fetching API. Please try again later";
export const GEO_BLOCK_MESSAGE =
"The Bitcoin Staking functionality is not available in your region";
1 change: 0 additions & 1 deletion src/utils/mempool_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ export async function pushTx(txHex: string): Promise<string> {
* @returns A promise that resolves to the amount of satoshis that the address
* holds.
*/
// TODO balance should come from reduced UTXOs
export async function getAddressBalance(address: string): Promise<number> {
const response = await fetch(addressInfoUrl(address));
if (!response.ok) {
Expand Down