diff --git a/demo/redirect-flow-example/src/App.tsx b/demo/redirect-flow-example/src/App.tsx index 2e7cf36..cbfdf6e 100644 --- a/demo/redirect-flow-example/src/App.tsx +++ b/demo/redirect-flow-example/src/App.tsx @@ -1,6 +1,5 @@ import { useEffect, useState } from "react"; import { - Web3AuthMPCCoreKit, WEB3AUTH_NETWORK, COREKIT_STATUS, makeEthereumSigner, @@ -11,9 +10,6 @@ import Web3 from "web3"; import { CHAIN_NAMESPACES, CustomChainConfig, IProvider } from "@web3auth/base"; import { EthereumSigningProvider } from "@web3auth/ethereum-mpc-provider"; import { KeyType } from "@tkey/common-types"; -import { tssLib as tssLibDkls } from "@toruslabs/tss-dkls-lib"; -import { tssLib as tssLibFrost } from "@toruslabs/tss-frost-lib"; -import { tssLib as tssLibFrostBip340 } from "@toruslabs/tss-frost-lib-bip340"; import bowser from "bowser"; @@ -35,7 +31,6 @@ export const generateSecretKey = (): string => { return base32.encode(randomBytes).toString().replace(/=/g, ""); }; -type TssLib = typeof tssLibDkls | typeof tssLibFrost | typeof tssLibFrostBip340; const PASSKEYS_ALLOWED_MAP = [bowser.OS_MAP.iOS, bowser.OS_MAP.MacOS, bowser.OS_MAP.Android, bowser.OS_MAP.Windows]; const getWindowsVersion = (osVersion: string) => { @@ -102,25 +97,23 @@ const uiConsole = (...args: any[]): void => { console.log(...args); }; -const selectedNetwork = WEB3AUTH_NETWORK.MAINNET; export const DEFAULT_CHAIN_CONFIG: CustomChainConfig = { chainNamespace: CHAIN_NAMESPACES.EIP155, - chainId: "0xaa36a7", - rpcTarget: "https://rpc.ankr.com/eth_sepolia", - displayName: "Ethereum Sepolia Testnet", - blockExplorerUrl: "https://sepolia.etherscan.io", + chainId: "0x66eee", // Arbitrum Sepolia chain ID + rpcTarget: "https://arbitrum-sepolia.blockpi.network/v1/rpc/private", + displayName: "Arbitrum Sepolia Testnet", + blockExplorerUrl: "https://sepolia.arbiscan.io", // Arbitrum Sepolia block explorer URL ticker: "ETH", tickerName: "Ethereum", decimals: 18, }; function App() { - const { coreKitInstance, passkeyPlugin, setCoreKitInstance, coreKitStatus, setCoreKitStatus, provider, setProvider, setWeb3, setUserInfo, globalLoading, getShareDescriptions } = useCoreKit(); + const { coreKitInstance, setCoreKitInstance, coreKitStatus, setCoreKitStatus, setProvider, setUserInfo, globalLoading, getShareDescriptions, provider, setWeb3 } = useCoreKit(); const [isLoading, setIsLoading] = useState(true); const navigate = useNavigate(); - const [selectedTssLib, setSelectedTssLib] = useState(tssLibDkls); async function setupProvider(chainConfig?: CustomChainConfig) { if (coreKitInstance.keyType !== KeyType.secp256k1) { @@ -139,10 +132,9 @@ function App() { if (coreKitInstance.status === COREKIT_STATUS.NOT_INITIALIZED) { await coreKitInstance.init({ rehydrate: true, handleRedirectResult: false }); setCoreKitInstance(coreKitInstance); - await passkeyPlugin.initWithMpcCoreKit(coreKitInstance); setIsLoading(false); } - setupProviderPostLogin(); + await setupProviderPostLogin(); if (coreKitInstance.status === COREKIT_STATUS.REQUIRED_SHARE) { navigate("/recovery"); @@ -176,24 +168,10 @@ function App() { }, [coreKitStatus]) useEffect(() => { - setIsLoading(globalLoading || false); - }, [globalLoading]); - - useEffect(() => { - const instance = new Web3AuthMPCCoreKit({ - web3AuthClientId: "BPi5PB_UiIZ-cPz1GtV5i1I2iOSOHuimiXBI0e-Oe_u6X3oVAbCiAZOTEBtTXw4tsluTITPqA8zMsfxIKMjiqNQ", - web3AuthNetwork: selectedNetwork, - uxMode: "popup", - manualSync: false, - storage: window.localStorage, - tssLib: selectedTssLib, - useDKG: false, - }); - setCoreKitInstance(instance); - if (instance.status === COREKIT_STATUS.NOT_INITIALIZED) { + if (coreKitInstance.status === COREKIT_STATUS.NOT_INITIALIZED) { init(); } - }, [selectedTssLib]); + }, [coreKitInstance]); useEffect(() => { if (provider) { @@ -286,7 +264,8 @@ function App() { } return (
- {isLoading ? ( + {/* {JSON.stringify(isLoading)} {coreKitInstance.status} */} + {isLoading || globalLoading ? ( <>
diff --git a/demo/redirect-flow-example/src/components/DropDown.tsx b/demo/redirect-flow-example/src/components/DropDown.tsx new file mode 100644 index 0000000..bbf75df --- /dev/null +++ b/demo/redirect-flow-example/src/components/DropDown.tsx @@ -0,0 +1,321 @@ +import React, { useState, useEffect, useRef } from "react"; +import { FaChevronDown, FaChevronUp } from "react-icons/fa"; +import { HiCheckCircle } from "react-icons/hi2"; +import { MdOutlineRadioButtonChecked, MdOutlineRadioButtonUnchecked } from "react-icons/md"; +import { cn } from "./Card"; + +interface Option { + name: string; + value: string; + icon?: React.ReactNode; + endSlot?: React.ReactNode; + startSlot?: React.ReactNode; +} + +export interface DropdownProps + extends Omit< + React.HTMLAttributes, + "options" | "defaultValue" | "onChange" + > { + value?: string | string[]; + options: Option[]; + defaultValue?: string | string[]; + placeholder?: string; + onChange?: (value: string[] | string) => void; + pill?: boolean; + inputSize?: "sm" | "md" | "lg"; + showArrow?: boolean; + EndSlot?: React.ReactNode; + StartSlot?: React.ReactNode; + showSelectHint?: boolean; + multiple?: boolean; + showValue?: boolean; + classes?: { + container?: string; + inputContainer?: string; + placeholder?: string; + inputIcon?: string; + input?: string; + optionContainer?: string; + optionItem?: string; + optionIcon?: string; + optionEndSlot?: string; + optionStartSlot?: string; + startSlot?: string; + endSlot?: string; + arrow?: string; + }; +} + +const Dropdown = React.forwardRef( + ( + { + value, + className, + options, + defaultValue, + placeholder = "Select...", + onChange, + pill = true, + inputSize = "md", + classes, + showArrow = true, + StartSlot, + EndSlot, + showSelectHint = true, + multiple = false, + showValue = true, + ...props + }, + ref, + ) => { + const initialSelectedOptions = Array.isArray(defaultValue) + ? options.filter((option) => defaultValue.includes(option.value)) + : options.filter((option) => option.value === defaultValue); + + const [isOpen, setIsOpen] = useState(false); + const [selectedOption, setSelectedOptions] = useState( + initialSelectedOptions, + ); + const [highlightedIndex, setHighlightedIndex] = useState(-1); + const [isFocused, setIsFocused] = useState(false); + + const dropdownRef = useRef(null); + + const handleOutsideClick = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + + useEffect(() => { + document.addEventListener("mousedown", handleOutsideClick); + return () => { + document.removeEventListener("mousedown", handleOutsideClick); + }; + }, []); + + const handleOptionClick = (option: Option) => { + if (multiple) { + setSelectedOptions((prev) => { + if (prev.find((item) => item.value === option.value)) { + return prev.filter((item) => item.value !== option.value); + } + if (onChange) + onChange([...prev, option].map((option) => option.value)); + return [...prev, option]; + }); + } else { + setSelectedOptions([option]); + if (onChange) onChange(option.value); + setIsOpen(false); + } + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (!isFocused) return; + + if (isOpen) { + if (event.key === "ArrowDown") { + setHighlightedIndex((prevIndex) => (prevIndex + 1) % options.length); + } else if (event.key === "ArrowUp") { + setHighlightedIndex( + (prevIndex) => (prevIndex - 1 + options.length) % options.length, + ); + } else if (event.key === "Enter") { + handleOptionClick(options[highlightedIndex]); + } + } else if (event.key === "Tab" || event.key === "Enter") { + setIsOpen(true); + } + }; + + const renderSelectedOptions = () => { + if (multiple) { + if (selectedOption.length > 2) { + return ( +

+ {selectedOption[0].name}, + {selectedOption[1].name} + +{selectedOption.length - 2} +

+ ); + } else { + return selectedOption.map((option) => ( + + {option.name} + + )); + } + } else { + return selectedOption[0]?.name; + } + }; + + const isSelected = (option: Option) => { + return multiple + ? selectedOption.find((item) => item.value === option.value) + : selectedOption?.[0]?.value === option.value; + }; + + useEffect(() => { + if (!value) return; + + const selectedValues: Option[] = []; + if (Array.isArray(value)) { + selectedValues.push(...options.filter((o) => value.includes(o.value))); + } else { + selectedValues.push(...options.filter((o) => o.value === value)); + } + setSelectedOptions(selectedValues); + }, [options, value]); + + return ( +
setIsFocused(true)} + onBlur={() => setIsFocused(false)} + {...props} + > +
setIsOpen(!isOpen)} + onKeyDown={handleKeyDown} + > + {selectedOption ? ( +
+ {!multiple && selectedOption?.[0]?.icon && ( + + {selectedOption?.[0]?.icon} + + )} + {StartSlot && ( +
{StartSlot}
+ )} + {showValue && renderSelectedOptions()} +
+ ) : ( + + {placeholder} + + )} +
+ {EndSlot && ( +
{EndSlot}
+ )} + {showArrow && ( + + {isOpen ? ( + + ) : ( + + )} + + )} +
+
+ {isOpen && ( +
    + {options.map((option, index) => ( +
  • handleOptionClick(option)} + onMouseEnter={() => setHighlightedIndex(index)} + > +
    + {option.startSlot && ( +
    + {option.startSlot} +
    + )} + {option.icon && ( +
    + {option.icon} +
    + )} +

    {option.name}

    +
    +
    + {option.endSlot && ( +
    + {option.endSlot} +
    + )} + {showSelectHint && ( +
    + {isSelected(option) ? ( + multiple ? ( + + ) : ( + + ) + ) : ( + + )} +
    + )} +
    +
  • + ))} +
+ )} +
+ ); + }, +); + +Dropdown.displayName = "Dropdown"; + +export { Dropdown }; \ No newline at end of file diff --git a/demo/redirect-flow-example/src/components/LoginCard.tsx b/demo/redirect-flow-example/src/components/LoginCard.tsx index 228c5bf..fe278a9 100644 --- a/demo/redirect-flow-example/src/components/LoginCard.tsx +++ b/demo/redirect-flow-example/src/components/LoginCard.tsx @@ -6,20 +6,38 @@ import { Button } from "./Button"; import { LoginForm } from "./LoginForm"; import { useSocialLogins } from "./useSocialLogin"; import { SocialLoginObj } from "./types"; +import { Dropdown } from "./DropDown"; +import { KeyType } from "@tkey/common-types"; +import { useCoreKit } from "../composibles/useCoreKit"; interface LoginCardProps { handleEmailPasswordLess: () => void; handleSocialLogin: (item: SocialLoginObj) => void; } -const LoginCard: React.FC = ({handleEmailPasswordLess, handleSocialLogin}) => { +const LoginCard: React.FC = ({ handleEmailPasswordLess, handleSocialLogin }) => { + const { setKeyType } = useCoreKit(); const [loginHint, setLoginHint] = React.useState(""); + const [defaultValue, setDefaultValue] = React.useState(localStorage.getItem("keyType") || KeyType.secp256k1); + const keyOptions = [ + { + name: "Ethereum (secp256k1)", + value: KeyType.secp256k1, + icon: {"Ethereum"}, + }, + { + name: "Solana (ed25519)", + value: KeyType.ed25519, + icon: ( + {"Solana"} + ), + }, + ]; const socialLogins = useSocialLogins(); const handlePasswordlessLogin = (e: React.FormEvent) => { e.preventDefault(); - console.log("loginHint", loginHint); // Handle passwordless login handleEmailPasswordLess(); }; @@ -29,6 +47,12 @@ const LoginCard: React.FC = ({handleEmailPasswordLess, handleSoc handleSocialLogin(item); }; + const handleSwitchKey = (val: string | string[]) => { + const keyType = val as KeyType; + if (!keyType || !setKeyType) throw new Error("key type is undefined"); + setKeyType(keyType); + }; + return (
= ({handleEmailPasswordLess, handleSoc
-

Welcome to Web3Auth

-

Login to continue

+

Welcome to Web3Auth

+
+ +

Your

+ +
+
+
+

Wallet in one Click

+

Login to continue

+
= ({handleEmailPasswordLess, handleSoc expandLabel="View more" collapseLabel="View less" primaryBtn="input" - onSocialLoginClick={ handleSocial } + onSocialLoginClick={handleSocial} >
{ - const { setAddShareType, coreKitInstance, setCoreKitStatus, existingModules } = useCoreKit(); + const { setAddShareType, coreKitInstance, setCoreKitStatus, existingModules, setKeyType } = useCoreKit(); const navigate = useNavigate(); const addMfa = (addShareType: AddShareType) => { @@ -28,6 +29,7 @@ const MfaCard: React.FC = () => { input: { message: "KEY_NOT_FOUND" }, }); await coreKitInstance.logout(); + setKeyType(KeyType.secp256k1); setCoreKitStatus(COREKIT_STATUS.NOT_INITIALIZED) navigate("/"); window.location.reload(); diff --git a/demo/redirect-flow-example/src/components/NavBar.tsx b/demo/redirect-flow-example/src/components/NavBar.tsx index 3112d71..29813f8 100644 --- a/demo/redirect-flow-example/src/components/NavBar.tsx +++ b/demo/redirect-flow-example/src/components/NavBar.tsx @@ -3,9 +3,10 @@ import { Button } from "./Button"; import { useCoreKit } from "../composibles/useCoreKit"; import { useNavigate } from "react-router-dom"; import { COREKIT_STATUS } from "@web3auth/mpc-core-kit"; +import { KeyType } from "@tkey/common-types"; const Header: React.FC = () => { - const { coreKitInstance, setCoreKitStatus, userInfo, setUserInfo, setGlobalLoading } = useCoreKit(); + const { coreKitInstance, setCoreKitStatus, userInfo, setUserInfo, setGlobalLoading, setKeyType } = useCoreKit(); const [isLogin, setIsLogin] = React.useState(true); React.useEffect(() => { try { @@ -26,6 +27,7 @@ const Header: React.FC = () => { setGlobalLoading(true); try { await coreKitInstance.logout(); + setKeyType(KeyType.secp256k1); setCoreKitStatus(COREKIT_STATUS.NOT_INITIALIZED); setUserInfo(undefined); navigate("/"); diff --git a/demo/redirect-flow-example/src/components/UserCard.tsx b/demo/redirect-flow-example/src/components/UserCard.tsx index 17c01f8..63fb6f1 100644 --- a/demo/redirect-flow-example/src/components/UserCard.tsx +++ b/demo/redirect-flow-example/src/components/UserCard.tsx @@ -4,18 +4,17 @@ import { Button } from "./Button"; import { Card } from "./Card"; import { Drawer } from "./Drawer"; import { useCoreKit } from "../composibles/useCoreKit"; -import { COREKIT_STATUS, factorKeyCurve } from "@web3auth/mpc-core-kit"; import { HiOutlineDuplicate } from "react-icons/hi"; import { HiOutlineCheckCircle } from "react-icons/hi"; import { Link } from "./Link"; +import useUnifiedRPC from "../composibles/useRpc"; const UserCard: React.FC = () => { - const { web3, drawerHeading, setDrawerHeading, drawerInfo, setDrawerInfo, userInfo } = useCoreKit(); - + const { drawerHeading, setDrawerHeading, drawerInfo, setDrawerInfo, userInfo, coreKitInstance } = useCoreKit(); + const { getAccount, account } = useUnifiedRPC(); const [openConsole, setOpenConsole] = React.useState(false); const [isCopied, setIsCopied] = React.useState(false); - const [account, setAccount] = React.useState(""); const [imageError, setImageError] = React.useState(false); const [currentDrawerHeading, setCurrentDrawerHeading] = React.useState(""); const [currentDrawerInfo, setCurrentDrawerInfo] = React.useState(null); @@ -36,18 +35,15 @@ const UserCard: React.FC = () => { } }, [drawerInfo]); - const getAccounts = async () => { - if (!web3) { - return; - } - const address = (await web3.eth.getAccounts())[0]; - setAccount(address); - return address; - }; React.useEffect(() => { - getAccounts(); - }, [userInfo, web3]); + const getAccountRPC = async () => { + if (coreKitInstance.state.userInfo) { + await getAccount(); + } + }; + getAccountRPC(); + }, [coreKitInstance]); const handleConsoleBtn = () => { setDrawerHeading("User Info Console"); diff --git a/demo/redirect-flow-example/src/components/mfa-cards/authenticator/AuthenticatorScan.tsx b/demo/redirect-flow-example/src/components/mfa-cards/authenticator/AuthenticatorScan.tsx index 9b8544e..f5f81c8 100644 --- a/demo/redirect-flow-example/src/components/mfa-cards/authenticator/AuthenticatorScan.tsx +++ b/demo/redirect-flow-example/src/components/mfa-cards/authenticator/AuthenticatorScan.tsx @@ -164,6 +164,8 @@ const AuthenticatorQRCodeCard: React.FC = () => { setDrawerInfo("Authenticator has been set successfully"); } catch (error) { console.error(error); + setDrawerHeading(`Authenticator`); + setDrawerInfo(`Error Setting Authenticator: ${(error as Error).message || "Failed"}`); } finally { setIsLoading(false); } diff --git a/demo/redirect-flow-example/src/components/mfa-cards/mnemonic/CreateMnemonic.tsx b/demo/redirect-flow-example/src/components/mfa-cards/mnemonic/CreateMnemonic.tsx index 294f31e..61f04b8 100644 --- a/demo/redirect-flow-example/src/components/mfa-cards/mnemonic/CreateMnemonic.tsx +++ b/demo/redirect-flow-example/src/components/mfa-cards/mnemonic/CreateMnemonic.tsx @@ -51,6 +51,8 @@ const CreateMnemonicPhraseCard: React.FC = () => { setAddShareType(""); } catch (error) { console.error(error); + setDrawerHeading(`Seed Phrase`); + setDrawerInfo(`Error Setting Seed phrase: ${(error as Error).message || "Failed"}`); } finally { setIsLoading(false); } diff --git a/demo/redirect-flow-example/src/components/mfa-cards/password/CreatePassword.tsx b/demo/redirect-flow-example/src/components/mfa-cards/password/CreatePassword.tsx index a00928b..28058bc 100644 --- a/demo/redirect-flow-example/src/components/mfa-cards/password/CreatePassword.tsx +++ b/demo/redirect-flow-example/src/components/mfa-cards/password/CreatePassword.tsx @@ -68,6 +68,8 @@ const GetPasswordCard: React.FC = () => { setDrawerInfo("Security question has been set successfully"); } catch (error) { console.error(error); + setDrawerHeading("Security Question"); + setDrawerInfo(`Error Setting Security Question: ${(error as Error).message || "Failed"}`); } finally { setIsLoading(false); } diff --git a/demo/redirect-flow-example/src/components/mfa-cards/recovery/RecoveryOptionCard.tsx b/demo/redirect-flow-example/src/components/mfa-cards/recovery/RecoveryOptionCard.tsx index aa9f000..09abc0c 100644 --- a/demo/redirect-flow-example/src/components/mfa-cards/recovery/RecoveryOptionCard.tsx +++ b/demo/redirect-flow-example/src/components/mfa-cards/recovery/RecoveryOptionCard.tsx @@ -5,10 +5,11 @@ import { Card } from "../../Card"; import { useCoreKit } from "../../../composibles/useCoreKit"; import { BN } from "bn.js"; import { COREKIT_STATUS } from "@web3auth/mpc-core-kit"; +import { KeyType } from "@tkey/common-types"; const RecoveryOptionsCard: React.FC = () => { const navigate = useNavigate(); - const { coreKitInstance, setCoreKitStatus, existingModules } = useCoreKit(); + const { coreKitInstance, setCoreKitStatus, existingModules, setKeyType } = useCoreKit(); const handleRecoveryOption = (option: string) => { navigate(`/verify-${option}`); @@ -27,6 +28,7 @@ const RecoveryOptionsCard: React.FC = () => { input: { message: "KEY_NOT_FOUND" }, }); await coreKitInstance.logout(); + setKeyType(KeyType.secp256k1); setCoreKitStatus(COREKIT_STATUS.NOT_INITIALIZED) navigate("/"); } diff --git a/demo/redirect-flow-example/src/components/transactions/SignMessage.tsx b/demo/redirect-flow-example/src/components/transactions/SignMessage.tsx index 0e18c2d..e033e3c 100644 --- a/demo/redirect-flow-example/src/components/transactions/SignMessage.tsx +++ b/demo/redirect-flow-example/src/components/transactions/SignMessage.tsx @@ -3,11 +3,13 @@ import { useCoreKit } from "../../composibles/useCoreKit"; import { Button } from "../Button"; import { Card } from "../Card"; import { SIG_TYPE } from "@toruslabs/constants"; +import useUnifiedRPC from "../../composibles/useRpc"; const SignPersonalMessageCard: React.FC = () => { const [message, setMessage] = React.useState("Hello World!"); - const { coreKitInstance, setDrawerHeading, setDrawerInfo, web3 } = useCoreKit(); + const { coreKitInstance, setDrawerHeading, setDrawerInfo } = useCoreKit(); const [isLoading, setIsLoading] = React.useState(false); + const { signMessage: signMessageFunction } = useUnifiedRPC(); const signMessage = async () => { setIsLoading(true); @@ -17,25 +19,13 @@ const SignPersonalMessageCard: React.FC = () => { } try { - if (coreKitInstance.sigType === SIG_TYPE.ECDSA_SECP256K1) { - if (!web3) { - console.log("web3 not initialized yet"); - return; - } - const fromAddress = (await web3.eth.getAccounts())[0]; - const message = "hello"; - const signedMessage = await web3.eth.personal.sign(message, fromAddress, ""); - setDrawerHeading("Sign Personal Message"); - setDrawerInfo(`Message has been signed successfully, ${signedMessage.toString()}`); - } else if (coreKitInstance.sigType === SIG_TYPE.ED25519 || coreKitInstance.sigType === SIG_TYPE.BIP340) { - const msg = Buffer.from("hello signer!"); - const sig = await coreKitInstance.sign(msg); - console.log(sig.toString("hex")); - setDrawerHeading("Sign Personal Message"); - setDrawerInfo(`Message has been signed successfully, ${sig.toString("hex")}`); - } + const signedMessage = await signMessageFunction(message); + setDrawerHeading(`Sign Personal Message ${coreKitInstance.sigType}`); + setDrawerInfo(`Message has been signed successfully, ${signedMessage.toString()}`); } catch (error) { console.error("Error signing message:", error); + setDrawerHeading(`Sign Personal Message ${coreKitInstance.sigType}`); + setDrawerInfo(`Error signing message: ${(error as Error).message || "Message signing failed"}`); } finally { setIsLoading(false); } diff --git a/demo/redirect-flow-example/src/components/transactions/TransactionCard.tsx b/demo/redirect-flow-example/src/components/transactions/TransactionCard.tsx index 5ea2458..6438b41 100644 --- a/demo/redirect-flow-example/src/components/transactions/TransactionCard.tsx +++ b/demo/redirect-flow-example/src/components/transactions/TransactionCard.tsx @@ -3,40 +3,31 @@ import { useCoreKit } from "../../composibles/useCoreKit"; import { Button } from "../Button"; import { Card } from "../Card"; import { TextField } from "../TextField"; +import useUnifiedRPC from "../../composibles/useRpc"; +import { KeyType } from "@tkey/common-types"; const TransactionCard: React.FC = () => { const [amount, setAmount] = React.useState("0.0001"); - const { web3, setDrawerHeading, setDrawerInfo } = useCoreKit(); + const { setDrawerHeading, setDrawerInfo, coreKitInstance } = useCoreKit(); const [isLoading, setIsLoading] = React.useState(false); + const { sendTransaction, account } = useUnifiedRPC(); - const sendTransaction = async () => { - if (!web3) { - console.error("web3 not initialized yet"); - return; - } + const sendWeb3AuthTx = async () => { setIsLoading(true); - const fromAddress = (await web3.eth.getAccounts())[0]; - - const destination = "0x2E464670992574A613f10F7682D5057fB507Cc21"; - const value = web3.utils.toWei(amount, "ether"); // Convert amount to wei - // Submit transaction to the blockchain and wait for it to be mined - console.log("Sending transaction..."); try { - const receipt = await web3.eth.sendTransaction({ - from: fromAddress, - to: destination, - value: value, - }); - console.log(receipt); + const toAddress = account; + if (!toAddress) { + console.error("No account found"); + return; + } + const receipt = await sendTransaction(toAddress, amount); setDrawerHeading("Send Transaction Result"); - const temp = JSON.stringify( - receipt, - (key, value) => (typeof value === "bigint" ? value.toString() : value) // return everything else unchanged - ); - setDrawerInfo(`${temp}`); + setDrawerInfo(`${receipt}`); } catch (error) { console.error("Error sending transaction:", error); + setDrawerHeading(`Send Transaction Result`); + setDrawerInfo(`Error Sending Transaction: ${(error as Error).message || "Sending Transaction failed"}`); } finally { setIsLoading(false); } @@ -49,14 +40,14 @@ const TransactionCard: React.FC = () => { setAmount(e.target.value)} - label="Amount (ETH)" - placeholder="Enter amount in ETH" + label={`Amount ${coreKitInstance?.keyType === KeyType.ed25519 ? "(SOL)" : "(ETH)"}`} + placeholder={`Enter amount in ${coreKitInstance?.keyType === KeyType.ed25519 ? "SOL" : "ETH"}`} className="mb-4 rounded-md" classes={{ container: "flex flex-col justify-center items-center", }} /> -
diff --git a/demo/redirect-flow-example/src/composibles/useCoreKit.tsx b/demo/redirect-flow-example/src/composibles/useCoreKit.tsx index 0bd9c6b..22a93fa 100644 --- a/demo/redirect-flow-example/src/composibles/useCoreKit.tsx +++ b/demo/redirect-flow-example/src/composibles/useCoreKit.tsx @@ -1,7 +1,8 @@ -import { COREKIT_STATUS, makeEthereumSigner, UserInfo, WEB3AUTH_NETWORK, Web3AuthMPCCoreKit } from "@web3auth/mpc-core-kit"; +import { COREKIT_STATUS, CoreKitMode, makeEthereumSigner, UserInfo, WEB3AUTH_NETWORK, Web3AuthMPCCoreKit } from "@web3auth/mpc-core-kit"; import { PasskeysPlugin } from "@web3auth/mpc-passkey-plugin"; import React, { createContext, useContext, useState, useEffect } from "react"; import { tssLib as tssLibDkls } from "@toruslabs/tss-dkls-lib"; +import { tssLib as tssLibFrost } from "@toruslabs/tss-frost-lib"; import Web3 from "web3"; import { CustomChainConfig, IProvider } from "@web3auth/base"; import { BN } from "bn.js"; @@ -10,6 +11,17 @@ import { EthereumSigningProvider } from "@web3auth/ethereum-mpc-provider"; import { DEFAULT_CHAIN_CONFIG } from "../App"; import { ShareDescription } from "../components/types"; +const selectedNetwork = WEB3AUTH_NETWORK.MAINNET; +const initialWeb3AuthConfig = { + web3AuthClientId: "BPi5PB_UiIZ-cPz1GtV5i1I2iOSOHuimiXBI0e-Oe_u6X3oVAbCiAZOTEBtTXw4tsluTITPqA8zMsfxIKMjiqNQ", + web3AuthNetwork: selectedNetwork, + uxMode: "popup" as CoreKitMode, + manualSync: false, + storage: window.localStorage, + tssLib: localStorage.getItem("keyType") === KeyType.ed25519 ? tssLibFrost : tssLibDkls, + useDKG: false, +}; + export type AddShareType = "phrase" | "authenticator" | "password" | ""; // Define the types for your context values @@ -38,19 +50,13 @@ interface CoreKitContextType { getShareDescriptions: () => void; shareDescriptions: ShareDescription[] | null; existingModules: string[]; + setKeyType: React.Dispatch>; + keyType: KeyType; } // Create the context with default values const CoreKitContext = createContext({ - coreKitInstance: new Web3AuthMPCCoreKit({ - web3AuthClientId: "BPi5PB_UiIZ-cPz1GtV5i1I2iOSOHuimiXBI0e-Oe_u6X3oVAbCiAZOTEBtTXw4tsluTITPqA8zMsfxIKMjiqNQ", - web3AuthNetwork: WEB3AUTH_NETWORK.MAINNET, - uxMode: "popup", - manualSync: false, - storage: window.localStorage, - tssLib: tssLibDkls, - useDKG: false, - }), + coreKitInstance: new Web3AuthMPCCoreKit(initialWeb3AuthConfig), passkeyPlugin: new PasskeysPlugin({ baseURL: "https://testing-mpc-passkeys.web3auth.io/api/v1", }), @@ -75,6 +81,8 @@ const CoreKitContext = createContext({ inputBackupFactorKey: async (backupFactorKey: string) => Promise.resolve(), getShareDescriptions: () => { }, shareDescriptions: null, + setKeyType: () => { }, + keyType: localStorage.getItem("keyType") as KeyType || KeyType.secp256k1, existingModules: [], }); @@ -82,21 +90,11 @@ const CoreKitContext = createContext({ interface CoreKitProviderProps { children: React.ReactNode; } - export const CoreKitProvider: React.FC = ({ children }) => { - const selectedNetwork = WEB3AUTH_NETWORK.MAINNET; const possibleModules = ["seedPhrase", "tssSecurityQuestions", "Authenticator"]; const [coreKitInstance, setCoreKitInstance] = useState( - new Web3AuthMPCCoreKit({ - web3AuthClientId: "BPi5PB_UiIZ-cPz1GtV5i1I2iOSOHuimiXBI0e-Oe_u6X3oVAbCiAZOTEBtTXw4tsluTITPqA8zMsfxIKMjiqNQ", - web3AuthNetwork: selectedNetwork, - uxMode: "popup", - manualSync: false, - storage: window.localStorage, - tssLib: tssLibDkls, - useDKG: false, - }) + new Web3AuthMPCCoreKit(initialWeb3AuthConfig) ); const [passkeyPlugin, setPasskeyPlugin] = useState( new PasskeysPlugin({ @@ -114,7 +112,17 @@ export const CoreKitProvider: React.FC = ({ children }) => const [shareDescriptions, setShareDescriptions] = React.useState(null); const [globalLoading, setGlobalLoading] = useState(false); const [existingModules, setExistingModules] = React.useState([]); + const [keyType, setKeyType] = React.useState(localStorage.getItem("keyType") as KeyType || KeyType.secp256k1); + useEffect(() => { + if (coreKitInstance.keyType === keyType) return; + localStorage.setItem("keyType", keyType); + setCoreKitInstance(new Web3AuthMPCCoreKit({ + ...initialWeb3AuthConfig, + tssLib: keyType === KeyType.secp256k1 ? tssLibDkls : tssLibFrost, + })); + }, [keyType]); + async function setupProvider(chainConfig?: CustomChainConfig) { if (coreKitInstance.keyType !== KeyType.secp256k1) { console.warn(`Ethereum requires keytype ${KeyType.secp256k1}, skipping provider setup`); @@ -191,6 +199,7 @@ export const CoreKitProvider: React.FC = ({ children }) => globalLoading, setGlobalLoading, getShareDescriptions, shareDescriptions, existingModules, + keyType, setKeyType, }}>{children} ); }; diff --git a/demo/redirect-flow-example/src/composibles/useEthereum.tsx b/demo/redirect-flow-example/src/composibles/useEthereum.tsx new file mode 100644 index 0000000..e6e4154 --- /dev/null +++ b/demo/redirect-flow-example/src/composibles/useEthereum.tsx @@ -0,0 +1,102 @@ +import { useState, useEffect } from "react"; +import Web3 from "web3"; +import { IProvider } from "@web3auth/base"; +import axios from "axios"; +import { useCoreKit } from "./useCoreKit"; + +const useEthereumRPC = () => { + const [ethereumAccount, setEthereumAccount] = useState(""); + const { web3 } = useCoreKit(); + + useEffect(() => { + getEthereumAccount(); + }, [web3]); + + const getEthereumAccount = async () => { + try { + if (!web3) throw new Error("Web3 is not set"); + const accounts = await web3.eth.getAccounts(); + setEthereumAccount(accounts[0]); + return accounts[0]; + } catch (error) { + console.error("Error getting accounts:", error); + return ""; + } + }; + + const getEthereumBalance = async (): Promise => { + try { + if (!web3 || !ethereumAccount) throw new Error("Web3 or account is not set"); + const balance = await web3.eth.getBalance(ethereumAccount); + return web3.utils.fromWei(balance, "ether"); + } catch (error) { + return error as string; + } + }; + + const sendTransactionEthereum = async (toAddress: string, amount: string): Promise => { + try { + await checkBalanceAndMint(); + if (!web3 || !ethereumAccount) throw new Error("Web3 or account is not set"); + const value = web3.utils.toWei(amount, "ether"); + const receipt = await web3.eth.sendTransaction({ + from: ethereumAccount, + to: toAddress, + value, + }); + return JSON.stringify(receipt, (key, value) => (typeof value === "bigint" ? value.toString() : value)); + } catch (error) { + return error as string; + } + }; + + const signMessageEthereum = async (message: string): Promise => { + try { + if (!web3 || !ethereumAccount) throw new Error("Web3 or account is not set"); + const signedMessage = await web3.eth.personal.sign(message, ethereumAccount, ""); + return signedMessage; + } catch (error) { + return error as string; + } + }; + + const checkBalanceAndMint = async () => { + try { + if (!web3) { + throw new Error("Web3 is not set"); + } + const address = (await web3.eth.getAccounts())[0]; + const balance = await checkBalance(); + return +balance > 0.001 ? console.log("Balance is less than 0.1 ETH") : await mintCall(address); + } catch (error) { + console.error("Error minting:", error); + } + }; + + const checkBalance = async () => { + if (!web3) { + throw new Error("Web3 is not set"); + } + const address = (await web3.eth.getAccounts())[0]; + const balance = web3.utils.fromWei(await web3.eth.getBalance(address), "ether"); + return balance; + }; + + const mintCall = async (address: string) => { + const resp = await axios.post(`https://web3pay-staging.web3auth.dev/api/mint`, { + chainId: "421614", + toAddress: address, + }); + return resp; + }; + + return { + getEthereumAccount, + getEthereumBalance, + sendTransactionEthereum, + signMessageEthereum, + ethereumAccount, + }; +}; + +export default useEthereumRPC; \ No newline at end of file diff --git a/demo/redirect-flow-example/src/composibles/useRpc.tsx b/demo/redirect-flow-example/src/composibles/useRpc.tsx new file mode 100644 index 0000000..0a8d9aa --- /dev/null +++ b/demo/redirect-flow-example/src/composibles/useRpc.tsx @@ -0,0 +1,67 @@ +import { useCoreKit } from "./useCoreKit"; +import { KeyType } from "@tkey/common-types"; +import useSolanaRPC from "./useSolana"; +import useEthereumRPC from "./useEthereum"; +import React, { useEffect } from "react"; + +const useUnifiedRPC = () => { + const { coreKitInstance } = useCoreKit(); + const solanaRPC = useSolanaRPC(); + const ethereumRPC = useEthereumRPC(); + const [account, setAccount] = React.useState(""); + const [balance, setBalance] = React.useState(""); + + useEffect(() => { + if (coreKitInstance.keyType === KeyType.secp256k1) { + setAccount(ethereumRPC.ethereumAccount); + } else if (coreKitInstance.keyType === KeyType.ed25519) { + setAccount(solanaRPC.publicKey?.toBase58() || ""); + } + }, [solanaRPC.publicKey, ethereumRPC.ethereumAccount, coreKitInstance.keyType]); + + const getAccount = async () => { + if (coreKitInstance.keyType === KeyType.secp256k1) { + const result = await ethereumRPC.getEthereumAccount(); + return result; + } else if (coreKitInstance.keyType === KeyType.ed25519) { + const result = await solanaRPC.getSolanaAccount(); + return result; + } + }; + + const getBalance = async () => { + if (coreKitInstance.keyType === KeyType.secp256k1) { + return await ethereumRPC.getEthereumBalance(); + } else if (coreKitInstance.keyType === KeyType.ed25519) { + return await solanaRPC.getSolanaBalance(); + } + return "0"; + }; + + const sendTransaction = async (toAddress: string, amount: string | number) => { + if (coreKitInstance.keyType === KeyType.secp256k1) { + return await ethereumRPC.sendTransactionEthereum(toAddress, amount as string); + } else if (coreKitInstance.keyType === KeyType.ed25519) { + return await solanaRPC.sendTransactionSolana(toAddress, amount as number); + } + }; + + const signMessage = async (message: string) => { + if (coreKitInstance.keyType === KeyType.secp256k1) { + return await ethereumRPC.signMessageEthereum(message); + } else if (coreKitInstance.keyType === KeyType.ed25519) { + return await solanaRPC.signMessageSolana(message); + } + return ""; + }; + + return { + getAccount, + getBalance, + sendTransaction, + signMessage, + account, + }; +}; + +export default useUnifiedRPC; diff --git a/demo/redirect-flow-example/src/composibles/useSolana.tsx b/demo/redirect-flow-example/src/composibles/useSolana.tsx new file mode 100644 index 0000000..4b2e9ab --- /dev/null +++ b/demo/redirect-flow-example/src/composibles/useSolana.tsx @@ -0,0 +1,125 @@ +import { base58, base64 } from "@scure/base"; +import { Connection, LAMPORTS_PER_SOL, PublicKey, SystemProgram, Transaction, clusterApiUrl } from "@solana/web3.js"; +import nacl from "tweetnacl"; +import { useState, useEffect } from "react"; +import { useCoreKit } from "./useCoreKit"; +import { KeyType } from "@tkey/common-types"; + +const useSolanaRPC = () => { + const [connection] = useState(new Connection(clusterApiUrl("devnet"), "confirmed")); + const [publicKey, setPublicKey] = useState(null); + const { coreKitInstance, userInfo } = useCoreKit(); + + const getPublicKey = () => { + if (!coreKitInstance) return; + const address = base58.encode(Uint8Array.from(coreKitInstance.getPubKeyEd25519())); + setPublicKey(new PublicKey(address)); + return new PublicKey(address); + } + + useEffect(() => { + if (coreKitInstance && coreKitInstance.state.userInfo && coreKitInstance.keyType === KeyType.ed25519 ) + getPublicKey(); + }, [coreKitInstance, userInfo]); + + const getSolanaAccount = async () => { + try { + const pubKey = getPublicKey(); + return pubKey?.toBase58() || publicKey?.toBase58() || ""; + } catch (error) { + return error as string; + } + }; + + const getSolanaBalance = async (): Promise => { + try { + if (!publicKey) throw new Error("Public key is not set"); + const balanceResponse = await connection.getBalance(publicKey); + return (balanceResponse / LAMPORTS_PER_SOL).toString(); + } catch (error) { + return error as string; + } + }; + + const checkBalanceAndMint = async () => { + try { + const balance = await getSolanaBalance(); + if (Number(balance) < .1) { + await requestFaucet(); + } + } catch (error) { + console.error("Error checking balance and minting:", error); + } + }; + + const sendTransactionSolana = async (toPubkey: string, transferAmount: number): Promise => { + try { + await checkBalanceAndMint(); + const balance = await getSolanaBalance(); + console.log({ balance }); + if (!publicKey) throw new Error("Public key is not set"); + const getRecentBlockhash = await connection.getLatestBlockhash("confirmed"); + const transferTransaction = new Transaction().add( + SystemProgram.transfer({ + fromPubkey: publicKey, + toPubkey: new PublicKey(toPubkey), + lamports: transferAmount * LAMPORTS_PER_SOL, // Convert transferAmount to lamports + }) + ); + + transferTransaction.recentBlockhash = getRecentBlockhash.blockhash; + transferTransaction.feePayer = publicKey; + + const sig = await coreKitInstance.sign(transferTransaction.serializeMessage()); + + transferTransaction.addSignature(publicKey, sig); + const hash = await connection.sendRawTransaction(transferTransaction.serialize()); + return hash; + } catch (error) { + return error as string; + } + }; + + const requestFaucet = async (): Promise => { + try { + if (!publicKey) throw new Error("Public key is not set"); + const hash = await connection.requestAirdrop(publicKey, LAMPORTS_PER_SOL); + const latestBlockHash = await connection.getLatestBlockhash(); + const response = await connection.confirmTransaction({ + blockhash: latestBlockHash.blockhash, + lastValidBlockHeight: latestBlockHash.lastValidBlockHeight, + signature: hash, + }); + console.log({ response }); + return hash; + } catch (error) { + return error as string; + } + }; + + const signMessageSolana = async (message: string): Promise => { + try { + if (!publicKey) throw new Error("Public key is not set"); + const msg = Buffer.from(message); + const sig = await coreKitInstance.sign(msg); + + const result = nacl.sign.detached.verify(msg, sig, publicKey.toBytes()); + + console.log("Signature verification result:", result); + return base64.encode(sig); + } catch (error) { + return error as string; + } + }; + + return { + getSolanaAccount, + getSolanaBalance, + sendTransactionSolana, + requestFaucet, + signMessageSolana, + publicKey, + }; +}; + +export default useSolanaRPC; \ No newline at end of file