Skip to content

Instantly share code, notes, and snippets.

@kamescg
Last active August 12, 2024 19:40
Show Gist options
  • Save kamescg/50580ab7047f7c82b5103a92080b9dbd to your computer and use it in GitHub Desktop.
Save kamescg/50580ab7047f7c82b5103a92080b9dbd to your computer and use it in GitHub Desktop.
"use client"
import { useEffect } from "react"
import { atom, useAtom } from "jotai"
import { useMutation } from "@tanstack/react-query"
import { useAccount, useSignMessage } from "wagmi"
import { Address } from "viem"
import { createSiweMessage } from "viem/siwe"
import { SignMessageData, SignMessageVariables } from "wagmi/query"
import { BASE_URL, siteConfig } from "@/config/site"
// We use an atom to store the nonce in case multiple components are mounted at the same time.
// If we don't use an atom, the session.cookie gets out of sync and is invalid during verification.
// This is because multiple /api/siwe/nonce requests would be made and the nonce would be different.
const nonceAtom = atom()
export function useSiweSignIn() {
const { signMessageAsync } = useSignMessage()
const { address } = useAccount()
const { chain } = useAccount()
const [nonce, setNonce] = useAtom(nonceAtom)
// This is a required work-around for Coinbase Smart Wallet on IOS Safari.
// We optimistically fetch the nonce from the server to avoid blocking the pop-up.
useEffect(() => {
if (!nonce) {
;(async () => {
const nonceRes = await fetch(`/api/siwe/nonce`)
setNonce(await nonceRes.text())
})().catch(console.error)
}
}, [nonce])
return useMutation({
mutationKey: ["sign-in-user"],
mutationFn: async () => {
if (!address || !chain?.id) return
const { message, signature } = await siweMessage({
address,
chainId: chain?.id,
signMessageAsync,
nonce: nonce as string,
})
const response = await fetch("/api/siwe/verify", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ message, signature }),
})
if (!response.ok) {
const data = await response.json()
throw new Error(data.message)
}
const data = await response.json()
return data
},
onSuccess: () => {
},
onError: (error) => {
},
})
}
interface SiweMessageOptions {
address: Address
chainId: number
nonce?: string
/* eslint-disable no-unused-vars */
signMessageAsync: (args: SignMessageVariables) => Promise<SignMessageData>
}
/**
* Utility function to create and sign a SIWE message
* @param address - Ethereum address
* @param chainId - Ethereum chain ID
* @param signMessageAsync - Wallet sign message function
* @returns SIWE message, message to sign and signature
*/
export const siweMessage = async ({
address,
chainId,
nonce,
signMessageAsync,
}: SiweMessageOptions) => {
// If nonce is not provided, fetch it from the API. This is last resort.
if (!nonce) {
// 1. Get random nonce from API
const nonceRes = await fetch("/api/siwe/nonce")
nonce = await nonceRes.text()
}
// 2. Create SIWE message with pre-fetched nonce and sign with wallet
const host = window.location.host
.replace("https://", "")
.replace("http://", "")
const DOMAIN = host.startsWith("localhost")
? BASE_URL.replace("http://", "")
: host
const message = createSiweMessage({
// localhost is not a valid RFC 3986 authority, hence not a valid domain: https://www.rfc-editor.org/rfc/rfc3986
domain: DOMAIN,
address,
statement: `Sign in with Ethereum to ${siteConfig.name}`,
uri: window.location.origin,
version: "1",
chainId: chainId,
nonce: nonce,
})
// 4. Sign message
const signature = await signMessageAsync({
message,
})
return {
message,
signature,
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment