Skip to content

Commit

Permalink
feat: validate address and pk
Browse files Browse the repository at this point in the history
  • Loading branch information
0xDazzer committed Feb 10, 2025
1 parent 8a4e4f2 commit 2997b7c
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 33 deletions.
25 changes: 25 additions & 0 deletions src/components/Error/container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useWidgetState } from "@/hooks/useWidgetState";

import { Error } from "./index";

interface ErrorContainer {
className?: string;
}

export function ErrorContainer({ className }: ErrorContainer) {
const { screen } = useWidgetState();
const { icon, title, description, cancelButton, submitButton, onCancel, onSubmit } = screen.params ?? {};

return (
<Error
className={className}
icon={icon}
title={title}
description={description}
cancelButton={cancelButton}
submitButton={submitButton}
onCancel={onCancel}
onSubmit={onSubmit}
/>
);
}
33 changes: 33 additions & 0 deletions src/components/Error/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Meta, StoryObj } from "@storybook/react";

import { Error } from "./index";

const meta: Meta<typeof Error> = {
component: Error,
tags: ["autodocs"],
};

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
className: "",
icon: (
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52" fill="none">
<path
d="M2.16675 46.5837H49.8334L26.0001 5.41699L2.16675 46.5837ZM28.1667 40.0837H23.8334V35.7503H28.1667V40.0837ZM28.1667 31.417H23.8334V22.7503H28.1667V31.417Z"
fill="#387085"
/>
</svg>
),
title: "Public Key Mismatch",
description:
"The Bitcoin address and Public Key for this wallet do not match. Please contact your wallet provider for support.",
cancelButton: "Cancel",
submitButton: "Continue Anyway",
onCancel: () => console.log("cancel"),
onSubmit: () => console.log("submit"),
},
};
52 changes: 52 additions & 0 deletions src/components/Error/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Button, DialogBody, DialogFooter, Heading, Text } from "@babylonlabs-io/bbn-core-ui";
import {} from "react";

interface ErrorProps {
className?: string;
icon: JSX.Element;
title: string | JSX.Element;
description: string | JSX.Element;
cancelButton?: string | JSX.Element;
submitButton?: string | JSX.Element;
onCancel?: () => void;
onSubmit?: () => void;
}

export function Error({
className,
icon,
title,
description,
cancelButton = "Cancel",
submitButton = "Submit",
onCancel,
onSubmit,
}: ErrorProps) {
return (
<div className={className}>
<DialogBody className="py-16 text-center">
<div className="mb-6 inline-flex h-20 w-20 items-center justify-center bg-primary-contrast text-primary-light">
{icon}
</div>

<Heading variant="h4" className="mb-4 text-accent-primary">
{title}
</Heading>

<Text as="div" className="text-accent-secondary">
{description}
</Text>
</DialogBody>

<DialogFooter className="flex gap-4">
<Button variant="outlined" fluid onClick={onCancel}>
{cancelButton}
</Button>

<Button fluid onClick={onSubmit}>
{submitButton}
</Button>
</DialogFooter>
</div>
);
}
2 changes: 2 additions & 0 deletions src/components/WalletProvider/components/Screen.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type JSX } from "react";

import { ChainsContainer as Chains } from "@/components/Chains/container";
import { ErrorContainer as Error } from "@/components/Error/container";
import { InscriptionsContainer as Inscriptions } from "@/components/Inscriptions/container";
import { LoaderScreen } from "@/components/Loader";
import { TermsOfServiceContainer as TermsOfService } from "@/components/TermsOfService/container";
Expand Down Expand Up @@ -37,6 +38,7 @@ const SCREENS = {
LOADER: ({ className, current }: ScreenProps) => (
<LoaderScreen className={className} title={current?.params?.message as string} />
),
ERROR: () => <Error />,
EMPTY: ({ className }: ScreenProps) => <div className={className} />,
} as const;

Expand Down
18 changes: 16 additions & 2 deletions src/context/State.context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ import type { IChain, IWallet } from "@/core/types";

export type Screen<T extends string = string> = {
type: T;
params?: Record<string, string | number>;
params?: Record<string, any>;
};

export type Screens =
| Screen<"LOADER">
| Screen<"TERMS_OF_SERVICE">
| Screen<"CHAINS">
| Screen<"WALLETS">
| Screen<"INSCRIPTIONS">;
| Screen<"INSCRIPTIONS">
| Screen<"ERROR">;

export interface State {
confirmed: boolean;
Expand All @@ -30,6 +31,15 @@ export interface Actions {
displayWallets?: (chain: string) => void;
displayInscriptions?: () => void;
displayTermsOfService?: () => void;
displayError?: (params: {
icon: JSX.Element;
title: string;
description: string;
cancelButton?: string;
submitButton?: string;
onCancel?: () => void;
onSubmit?: () => void;
}) => void;
selectWallet?: (chain: string, wallet: IWallet) => void;
removeWallet?: (chain: string) => void;
confirm?: () => void;
Expand Down Expand Up @@ -91,6 +101,10 @@ export function StateProvider({ children, chains }: PropsWithChildren<StateProvi
setState((state) => ({ ...state, screen: { type: "INSCRIPTIONS" } }));
},

displayError: (params) => {
setState((state) => ({ ...state, screen: { type: "ERROR", params } }));
},

selectWallet: (chain: string, wallet: IWallet) => {
setState((state) => ({
...state,
Expand Down
119 changes: 98 additions & 21 deletions src/core/utils/wallet.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,109 @@
import { networks } from "bitcoinjs-lib";
import * as ecc from "@bitcoin-js/tiny-secp256k1-asmjs";
import { initEccLib, networks, payments } from "bitcoinjs-lib";
import { toXOnly } from "bitcoinjs-lib/src/psbt/bip371";

import { Network } from "@/core/types";
export const COMPRESSED_PUBLIC_KEY_HEX_LENGTH = 66;

export function validateAddress(network: Network, address: string): void {
if (network === Network.MAINNET && !address.startsWith("bc1")) {
throw new Error("Incorrect address prefix for Mainnet. Expected address to start with 'bc1'.");
initEccLib(ecc);

const NETWORKS = {
[Network.MAINNET]: {
name: "Mainnet",
config: networks.bitcoin,
prefix: {
common: "bc1",
nativeSegWit: "bc1q",
taproot: "bc1p",
},
},
[Network.CANARY]: {
name: "Canary",
config: networks.bitcoin,
prefix: {
common: "bc1",
nativeSegWit: "bc1q",
taproot: "bc1p",
},
},
[Network.TESTNET]: {
name: "Testnet",
config: networks.testnet,
prefix: {
common: "tb1",
nativeSegWit: "tb1q",
taproot: "tb1p",
},
},
[Network.SIGNET]: {
name: "Signet",
config: networks.testnet,
prefix: {
common: "tb1",
nativeSegWit: "tb1q",
taproot: "tb1p",
},
},
};

export const getTaprootAddress = (publicKey: string, network: Network) => {
if (publicKey.length == COMPRESSED_PUBLIC_KEY_HEX_LENGTH) {
publicKey = publicKey.slice(2);
}
if (network === Network.CANARY && !address.startsWith("bc1")) {
throw new Error("Incorrect address prefix for Canary. Expected address to start with 'bc1'.");

const internalPubkey = Buffer.from(publicKey, "hex");
const { address, output: scriptPubKey } = payments.p2tr({
internalPubkey: toXOnly(internalPubkey),
network: NETWORKS[network].config,
});

if (!address || !scriptPubKey) {
throw new Error("Failed to generate taproot address or script from public key");
}
if ((network === Network.TESTNET || network === Network.SIGNET) && !address.startsWith("tb1")) {
throw new Error("Incorrect address prefix for Testnet/Signet. Expected address to start with 'tb1'.");

return address;
};

export const getNativeSegwitAddress = (publicKey: string, network: Network) => {
if (publicKey.length !== COMPRESSED_PUBLIC_KEY_HEX_LENGTH) {
throw new Error("Invalid public key length for generating native segwit address");
}

if (![Network.MAINNET, Network.SIGNET, Network.TESTNET, Network.CANARY].includes(network)) {
throw new Error(`Unsupported network: ${network}. Please provide a valid network.`);
const internalPubkey = Buffer.from(publicKey, "hex");
const { address, output: scriptPubKey } = payments.p2wpkh({
pubkey: internalPubkey,
network: NETWORKS[network].config,
});

if (!address || !scriptPubKey) {
throw new Error("Failed to generate native segwit address or script from public key");
}

return address;
};

export function validateAddressWithPK(address: string, publicKey: string, network: Network) {
if (address.startsWith(NETWORKS[network].prefix.taproot)) {
return address === getTaprootAddress(publicKey, network);
}

if (address.startsWith(NETWORKS[network].prefix.nativeSegWit)) {
return address === getNativeSegwitAddress(publicKey, network);
}

return false;
}

export const toNetwork = (network: Network): networks.Network => {
switch (network) {
case Network.MAINNET:
case Network.CANARY:
return networks.bitcoin;
case Network.TESTNET:
case Network.SIGNET:
return networks.testnet;
default:
throw new Error("Unsupported network");
export function validateAddress(network: Network, address: string): void {
const { prefix, name } = NETWORKS[network];

if (!(network in NETWORKS)) {
throw new Error(`Unsupported network: ${network}. Please provide a valid network.`);
}
};

if (!address.startsWith(prefix.common)) {
throw new Error(`Incorrect address prefix for ${name}. Expected address to start with '${prefix}'.`);
}
}

export const toNetwork = (network: Network): networks.Network => NETWORKS[network].config;
60 changes: 50 additions & 10 deletions src/hooks/useWalletConnectors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { useCallback, useEffect } from "react";
import { useChainProviders } from "@/context/Chain.context";
import { useInscriptionProvider } from "@/context/Inscriptions.context";
import { IChain, IWallet } from "@/core/types";
import { validateAddressWithPK } from "@/core/utils/wallet";

import { useWidgetState } from "./useWidgetState";

export function useWalletConnectors(onError?: (e: Error) => void) {
const connectors = useChainProviders();
const { selectWallet, removeWallet, displayLoader, displayChains, displayInscriptions } = useWidgetState();
const { selectWallet, removeWallet, displayLoader, displayChains, displayInscriptions, displayError } =
useWidgetState();
const { showAgain } = useInscriptionProvider();

// Connecting event
Expand All @@ -28,22 +30,60 @@ export function useWalletConnectors(onError?: (e: Error) => void) {
useEffect(() => {
const connectorArr = Object.values(connectors);

const unsubscribeArr = connectorArr.filter(Boolean).map((connector) =>
connector.on("connect", (connectedWallet: IWallet) => {
const handlers: Record<string, (connector: any) => (connectedWallet: IWallet) => void> = {
BTC: (connector) => (connectedWallet) => {
if (connectedWallet) {
selectWallet?.(connector.id, connectedWallet);
selectWallet?.("BTC", connectedWallet);
}

if (showAgain && connector.id === "BTC") {
displayInscriptions?.();
const goToNextScreen = () => void (showAgain ? displayInscriptions?.() : displayChains?.());

if (
validateAddressWithPK(
connectedWallet.account?.address ?? "",
connectedWallet.account?.publicKeyHex ?? "",
connector.config.network,
)
) {
goToNextScreen();
} else {
displayChains?.();
displayError?.({
icon: (
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52" fill="none">
<path
d="M2.16675 46.5837H49.8334L26.0001 5.41699L2.16675 46.5837ZM28.1667 40.0837H23.8334V35.7503H28.1667V40.0837ZM28.1667 31.417H23.8334V22.7503H28.1667V31.417Z"
fill="#387085"
/>
</svg>
),
title: "Public Key Mismatch",
description:
"The Bitcoin address and Public Key for this wallet do not match. Please contact your wallet provider for support.",
cancelButton: "Cancel",
submitButton: "Continue Anyway",
onSubmit: goToNextScreen,
onCancel: () => {
removeWallet?.(connector.id);
displayChains?.();
},
});
}
}),
);
},
BBN: (connector) => (connectedWallet) => {
if (connectedWallet) {
selectWallet?.(connector.id, connectedWallet);
}

displayChains?.();
},
};

const unsubscribeArr = connectorArr
.filter(Boolean)
.map((connector) => connector.on("connect", handlers[connector.id]?.(connector)));

return () => unsubscribeArr.forEach((unsubscribe) => unsubscribe());
}, [selectWallet, displayInscriptions, displayChains, connectors, showAgain]);
}, [selectWallet, removeWallet, displayInscriptions, displayChains, connectors, showAgain]);

// Disconnect Event
useEffect(() => {
Expand Down

0 comments on commit 2997b7c

Please sign in to comment.