diff --git a/components/dialogs/sign-in-dialog.tsx b/components/dialogs/sign-in-dialog.tsx index 28f2379c..1403f04d 100644 --- a/components/dialogs/sign-in-dialog.tsx +++ b/components/dialogs/sign-in-dialog.tsx @@ -1,18 +1,24 @@ -import React, { useId } from "react" +import React from "react" import { useConfig } from "@renegade-fi/react" import { createWallet, getWalletFromRelayer, - getWalletId, lookupWallet, } from "@renegade-fi/react/actions" import { ROOT_KEY_MESSAGE_PREFIX } from "@renegade-fi/react/constants" -import { Loader2 } from "lucide-react" +import { MutationStatus, useMutation } from "@tanstack/react-query" +import { useModal } from "connectkit" +import { Check, Loader2, X } from "lucide-react" import { toast } from "sonner" import { useLocalStorage } from "usehooks-ts" import { BaseError } from "viem" -import { useSignMessage } from "wagmi" +import { + useChainId, + useDisconnect, + useSignMessage, + useSwitchChain, +} from "wagmi" import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" @@ -24,19 +30,40 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog" +import { Label } from "@/components/ui/label" import { useMediaQuery } from "@/hooks/use-media-query" import { STORAGE_REMEMBER_ME } from "@/lib/constants/storage" import { - CREATE_WALLET_ERROR, CREATE_WALLET_START, CREATE_WALLET_SUCCESS, - LOOKUP_WALLET_ERROR, LOOKUP_WALLET_START, LOOKUP_WALLET_SUCCESS, } from "@/lib/constants/toast" import { chain } from "@/lib/viem" +// Define a Step type +type Step = { + label: string + description: string + status: MutationStatus + error?: string +} + +const NonDeterministicWalletError = new Error( + "Nondeterministic wallets are not supported on Renegade", +) + +function isNonDeterministicWalletError(error?: string) { + return error === NonDeterministicWalletError.message +} + +enum ConnectSuccess { + CREATE_WALLET, + LOOKUP_WALLET, + ALREADY_INDEXED, +} + export function SignInDialog({ open, onOpenChange, @@ -44,140 +71,221 @@ export function SignInDialog({ open: boolean onOpenChange: (open: boolean) => void }) { - const { - signMessage, - status: signStatus, - isSuccess: signSuccess, - } = useSignMessage() + const chainId = useChainId() + const { switchChain } = useSwitchChain() + const { disconnectAsync } = useDisconnect() + const isDesktop = useMediaQuery("(min-width: 1024px)") const config = useConfig() - const [isConnecting, setIsConnecting] = React.useState(false) - const toastId = useId() + const [currentStep, setCurrentStep] = React.useState(0) + const [connectDescription, setConnectDescription] = React.useState( + "Connect your wallet to Renegade.", + ) + const { setOpen } = useModal() + + const { + data: signMessage1Data, + signMessage: signMessage1, + status: signStatus1, + reset: reset1, + error: signMessage1Error, + } = useSignMessage({ + mutation: { + onMutate() { + setCurrentStep(1) + }, + onSuccess() { + signMessage2({ + message: `${ROOT_KEY_MESSAGE_PREFIX} ${chain.id}`, + }) + }, + }, + }) - const handleClick = () => - signMessage( + const { + signMessage: signMessage2, + status: signStatus2, + reset: reset2, + error: signMessage2Error, + } = useSignMessage({ + mutation: { + onMutate() { + setCurrentStep(2) + }, + onSuccess(data) { + if (!data || !signMessage1Data) throw new Error("Missing signature") + if (data !== signMessage1Data) throw NonDeterministicWalletError + connectWallet({ + signature: signMessage1Data, + }) + }, + }, + }) + + const { + mutate: connectWallet, + status: connectStatus, + reset: resetConnect, + error: connectError, + } = useMutation({ + mutationFn: async (variables: { signature: `0x${string}` }) => { + const seed = variables.signature + config.setState((x) => ({ ...x, seed })) + + try { + // GET wallet from relayer + const wallet = await getWalletFromRelayer(config) + // If success, return + if (wallet) { + config.setState((x) => ({ ...x, status: "in relayer" })) + return ConnectSuccess.ALREADY_INDEXED + } + } catch (error) {} + + // GET # logs + const blinderShare = config.utils.derive_blinder_share(seed) + const res = await fetch(`/api/get-logs?blinderShare=${blinderShare}`) + if (!res.ok) throw new Error("Failed to query chain") + const { logs } = await res.json() + // Iff logs === 0, create wallet + if (logs === 0) { + setConnectDescription(CREATE_WALLET_START) + await createWallet(config) + return ConnectSuccess.CREATE_WALLET + } else if (logs > 0) { + setConnectDescription(LOOKUP_WALLET_START) + await lookupWallet(config) + return ConnectSuccess.LOOKUP_WALLET + } + throw new Error("Failed to create or lookup wallet") + }, + onMutate() { + setCurrentStep(3) + }, + onSuccess(data) { + let message = "" + if (data === ConnectSuccess.CREATE_WALLET) { + message = CREATE_WALLET_SUCCESS + } else if (data === ConnectSuccess.LOOKUP_WALLET) { + message = LOOKUP_WALLET_SUCCESS + } else if (data === ConnectSuccess.ALREADY_INDEXED) { + message = "Successfully signed in" + } + toast.success(message) + reset() + onOpenChange(false) + }, + }) + + const reset = () => { + reset1() + reset2() + resetConnect() + setConnectDescription("Connect your wallet to Renegade.") + setCurrentStep(0) + } + + const onSubmit = async () => { + if (chainId !== chain.id) { + await switchChain({ chainId }) + } + if (steps.some((step) => step.status === "error")) { + const error = steps.find((step) => step.status === "error") + if (isNonDeterministicWalletError(error?.error)) { + reset() + await disconnectAsync().then(() => { + onOpenChange(false) + // setOpen(true) + }) + return + } + } + reset() + signMessage1({ + message: `${ROOT_KEY_MESSAGE_PREFIX} ${chain.id}`, + }) + } + + const steps = React.useMemo(() => { + return [ { - message: `${ROOT_KEY_MESSAGE_PREFIX} ${chain.id}`, + label: "Generate your Renegade wallet", + description: "Verify that you own this wallet.", + status: signStatus1, + error: (signMessage1Error as BaseError)?.shortMessage, }, { - async onSuccess(data) { - toast.success("Processing request...", { - id: toastId, - icon: , - }) - config.setState((x) => ({ ...x, seed: data })) - const id = getWalletId(config) - config.setState((x) => ({ ...x, id })) - setIsConnecting(true) - try { - // GET wallet from relayer - const wallet = await getWalletFromRelayer(config) - // If success, return - if (wallet) { - config.setState((x) => ({ ...x, status: "in relayer" })) - toast.success("Successfully signed in", { - id: toastId, - icon: undefined, - }) - onOpenChange(false) - setIsConnecting(false) - return - } - } catch (error) {} - - // GET # logs - const blinderShare = config.utils.derive_blinder_share(data) - const res = await fetch(`/api/get-logs?blinderShare=${blinderShare}`) - - if (!res.ok) { - toast.error("Failed to query chain, please try again.") - setIsConnecting(false) - } - const { logs } = await res.json() - if (logs === 0) { - // Iff logs === 0, create wallet - toast.promise( - createWallet(config) - .then(() => { - onOpenChange(false) - }) - .finally(() => { - setIsConnecting(false) - }), - { - id: toastId, - loading: CREATE_WALLET_START, - success: CREATE_WALLET_SUCCESS, - error: (error) => { - console.error(error) - return CREATE_WALLET_ERROR - }, - icon: undefined, - }, - ) - } else if (logs > 0) { - // Iff logs > 0, lookup wallet - toast.promise( - lookupWallet(config) - .then(() => { - onOpenChange(false) - }) - .finally(() => { - setIsConnecting(false) - }), - { - id: toastId, - loading: LOOKUP_WALLET_START, - success: LOOKUP_WALLET_SUCCESS, - error: LOOKUP_WALLET_ERROR, - icon: undefined, - }, - ) - } - }, - onError(error) { - console.error(error) - toast.error( - `Error signing message: ${(error as BaseError).shortMessage || error.message}`, - { - id: toastId, - icon: undefined, - }, - ) - }, + label: "Verify wallet compatibility", + description: "Ensure your wallet is supported.", + status: signStatus2, + error: + (signMessage2Error as BaseError)?.shortMessage ?? + signMessage2Error?.message, }, - ) - const isDesktop = useMediaQuery("(min-width: 1024px)") + { + label: "Connect wallet", + description: connectDescription, + status: connectStatus, + error: connectError?.message, + }, + ] + }, [ + connectDescription, + connectError?.message, + connectStatus, + signMessage1Error, + signMessage2Error, + signStatus1, + signStatus2, + ]) + + const isDisabled = steps.some((step) => step.status === "pending") + + const buttonText = React.useMemo(() => { + if (isDisabled) return "Confirm in wallet" + if (steps.some((step) => step.status === "error")) { + const error = steps.find((step) => step.status === "error") + if (isNonDeterministicWalletError(error?.error)) + return "Connect a new wallet" + return "Try again" + } + return "Sign in to Renegade" + }, [chainId, isDisabled, steps]) if (isDesktop) { return ( { + reset() + onOpenChange(open) + }} > - + - Unlock your Wallet + Sign in to Renegade - To trade on Renegade, we require a one-time signature to create or - find your wallet on-chain. + Verify wallet ownership and compatibility before signing in. + Signatures are free and do not send a transaction. -
- +
+ +
@@ -188,29 +296,34 @@ export function SignInDialog({ return ( { + reset() + onOpenChange(open) + }} > - Unlock your Wallet + Sign in to Renegade - To trade on Renegade, we require a one-time signature to create or - find your wallet on-chain. + Verify wallet ownership and compatibility before signing in. + Signatures are free and do not send a transaction. + -
- +
+
@@ -218,7 +331,25 @@ export function SignInDialog({ ) } -function SignInContent() { +function ErrorWarning({ steps }: { steps: Step[] }) { + if (!steps.some((step) => step.status === "error")) return null + return ( +
+ +
+ {steps.find((step) => step.status === "error")?.error} +
+
+ ) +} + +function SignInContent({ + steps, + currentStep, +}: { + steps: Step[] + currentStep: number +}) { const [rememberMe, setRememberMe] = useLocalStorage( STORAGE_REMEMBER_ME, false, @@ -226,8 +357,37 @@ function SignInContent() { initializeWithValue: false, }, ) + + const getIcon = (status: MutationStatus, step: number) => { + if (step > currentStep) return null + switch (status) { + case "pending": + return + case "success": + return + case "error": + return + default: + return null + } + } + return ( <> + {steps.map((step, index) => ( +
+
+ +
+ {step.description} +
+
+ {getIcon(step.status, index)} +
+ ))}
- +
) diff --git a/providers/wagmi-provider/wagmi-provider.tsx b/providers/wagmi-provider/wagmi-provider.tsx index 5bf8ef3e..c7b4c6cb 100644 --- a/providers/wagmi-provider/wagmi-provider.tsx +++ b/providers/wagmi-provider/wagmi-provider.tsx @@ -109,11 +109,12 @@ export function WagmiProvider({ > {children} + {/* TODO: Any issues with this? */} + - )