diff --git a/src/components/Error/container.tsx b/src/components/Error/container.tsx
new file mode 100644
index 0000000..ee4b3df
--- /dev/null
+++ b/src/components/Error/container.tsx
@@ -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 (
+
+ );
+}
diff --git a/src/components/Error/index.stories.tsx b/src/components/Error/index.stories.tsx
new file mode 100644
index 0000000..6b863fc
--- /dev/null
+++ b/src/components/Error/index.stories.tsx
@@ -0,0 +1,33 @@
+import type { Meta, StoryObj } from "@storybook/react";
+
+import { Error } from "./index";
+
+const meta: Meta = {
+ component: Error,
+ tags: ["autodocs"],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ className: "",
+ icon: (
+
+ ),
+ 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"),
+ },
+};
diff --git a/src/components/Error/index.tsx b/src/components/Error/index.tsx
new file mode 100644
index 0000000..a35a2a8
--- /dev/null
+++ b/src/components/Error/index.tsx
@@ -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 (
+
+
+
+ {icon}
+
+
+
+ {title}
+
+
+
+ {description}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/WalletProvider/components/Screen.tsx b/src/components/WalletProvider/components/Screen.tsx
index 3f21fa6..73ba4dd 100644
--- a/src/components/WalletProvider/components/Screen.tsx
+++ b/src/components/WalletProvider/components/Screen.tsx
@@ -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";
@@ -37,6 +38,7 @@ const SCREENS = {
LOADER: ({ className, current }: ScreenProps) => (
),
+ ERROR: () => ,
EMPTY: ({ className }: ScreenProps) => ,
} as const;
diff --git a/src/context/State.context.tsx b/src/context/State.context.tsx
index 819369a..ec4ebe5 100644
--- a/src/context/State.context.tsx
+++ b/src/context/State.context.tsx
@@ -4,7 +4,7 @@ import type { IChain, IWallet } from "@/core/types";
export type Screen = {
type: T;
- params?: Record;
+ params?: Record;
};
export type Screens =
@@ -12,7 +12,8 @@ export type Screens =
| Screen<"TERMS_OF_SERVICE">
| Screen<"CHAINS">
| Screen<"WALLETS">
- | Screen<"INSCRIPTIONS">;
+ | Screen<"INSCRIPTIONS">
+ | Screen<"ERROR">;
export interface State {
confirmed: boolean;
@@ -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;
@@ -91,6 +101,10 @@ export function StateProvider({ children, chains }: PropsWithChildren ({ ...state, screen: { type: "INSCRIPTIONS" } }));
},
+ displayError: (params) => {
+ setState((state) => ({ ...state, screen: { type: "ERROR", params } }));
+ },
+
selectWallet: (chain: string, wallet: IWallet) => {
setState((state) => ({
...state,
diff --git a/src/core/utils/wallet.ts b/src/core/utils/wallet.ts
index 7e9f208..dddfb0e 100644
--- a/src/core/utils/wallet.ts
+++ b/src/core/utils/wallet.ts
@@ -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;
diff --git a/src/hooks/useWalletConnectors.tsx b/src/hooks/useWalletConnectors.tsx
index c7f0089..553885f 100644
--- a/src/hooks/useWalletConnectors.tsx
+++ b/src/hooks/useWalletConnectors.tsx
@@ -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
@@ -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 (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: (
+
+ ),
+ 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(() => {