Skip to content

Commit

Permalink
docs: add siwe guide (wevm#137)
Browse files Browse the repository at this point in the history
* docs: add siwe guide

* chore: changeset
  • Loading branch information
tmm authored Feb 5, 2022
1 parent 9ad1357 commit dceeb43
Show file tree
Hide file tree
Showing 37 changed files with 775 additions and 42 deletions.
6 changes: 6 additions & 0 deletions .changeset/serious-emus-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'wagmi-private': patch
'wagmi': patch
---

add siwe guide
1 change: 1 addition & 0 deletions docs/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
NEXT_IRON_PASSWORD=
NEXT_PUBLIC_ALCHEMY_ID=
NEXT_PUBLIC_ETHERSCAN_API_KEY=
NEXT_PUBLIC_INFURA_ID=
File renamed without changes.
1 change: 1 addition & 0 deletions docs/components/core/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { PreviewWrapper } from './PreviewWrapper'
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import * as React from 'react'
import { useAccount } from 'wagmi'

import { PreviewWrapper } from './PreviewWrapper'
import { Account } from './Account'
import { WalletSelector } from './WalletSelector'
import { PreviewWrapper } from '../core'
import { Account, WalletSelector } from '../web3'

export const ConnectWallet = () => {
const [{ data }] = useAccount()
Expand Down
80 changes: 80 additions & 0 deletions docs/components/guides/SignInWithEthereum.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import * as React from 'react'
import { Box, Button, Skeleton, Stack } from 'degen'
import { chain, useAccount, useNetwork } from 'wagmi'

import { PreviewWrapper } from '../core'
import { Account, SignInWithEthereumButton, WalletSelector } from '../web3'
import { formatAddress } from '../../lib/address'

export const SignInWithEthereum = () => {
const [state, setState] = React.useState<{
address?: string
loading?: boolean
}>({})
const [{ data: accountData }] = useAccount()
const [{ data: networkData }] = useNetwork()

React.useEffect(() => {
const handler = async () => {
try {
const res = await fetch('/api/me')
const json = await res.json()
setState((x) => ({ ...x, address: json.address }))
} finally {
setState((x) => ({ ...x, loading: false }))
}
}
;(async () => await handler())()

window.addEventListener('focus', handler)
return () => window.removeEventListener('focus', handler)
}, [])

const signedInContent = state.address ? (
<Stack direction="horizontal" align="center" justify="center">
<Box fontSize="large">Signed in as {formatAddress(state.address)}</Box>
<Button
size="small"
variant="tertiary"
onClick={async () => {
await fetch('/api/logout')
setState({})
}}
>
Sign Out
</Button>
</Stack>
) : null

if (accountData)
return (
<PreviewWrapper>
<Stack space="6">
<Account />

{state.address ? (
signedInContent
) : (
<Skeleton loading={state.loading} width="full" radius="2xLarge">
<SignInWithEthereumButton
address={accountData.address}
chainId={networkData.chain?.id ?? chain.mainnet.id}
onSuccess={({ address }) =>
setState((x) => ({ ...x, address }))
}
/>
</Skeleton>
)}
</Stack>
</PreviewWrapper>
)

return (
<PreviewWrapper>
<Stack space="6">
<WalletSelector />
{signedInContent}
</Stack>
</PreviewWrapper>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import { Box, Button, Text, Textarea } from 'degen'
import { verifyMessage } from 'ethers/lib/utils'
import { useAccount, useSignMessage } from 'wagmi'

import { PreviewWrapper } from './PreviewWrapper'
import { Account } from './Account'
import { WalletSelector } from './WalletSelector'
import { PreviewWrapper } from '../core'
import { Account, WalletSelector } from '../web3'

export const SignMessage = () => {
const previousMessage = React.useRef<string>()
Expand Down
3 changes: 1 addition & 2 deletions docs/components/index.ts → docs/components/guides/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export { Account } from './Account'
export { ConnectWallet } from './ConnectWallet'
export { PreviewWrapper } from './PreviewWrapper'
export { SignInWithEthereum } from './SignInWithEthereum'
export { SignMessage } from './SignMessage'
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@ import * as React from 'react'
import { Avatar, Box, Button, Stack } from 'degen'
import { useAccount } from 'wagmi'

import { formatAddress } from '../../lib/address'

export const Account = () => {
const [{ data: accountData }, disconnect] = useAccount({
fetchEns: true,
})

if (!accountData) return null

const formattedAddress = `${accountData.address.slice(
0,
6,
)}${accountData.address.slice(38, 42)}`
const formattedAddress = formatAddress(accountData.address)
return (
<Stack
align="center"
Expand Down
75 changes: 75 additions & 0 deletions docs/components/web3/SignInWithEthereumButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import * as React from 'react'
import { Button, IconEth, Stack, Text } from 'degen'
import { SiweMessage } from 'siwe'
import { useSignMessage } from 'wagmi'

type Props = {
address: string
chainId: number
onSuccess?(data: { address: string }): void
}

export const SignInWithEthereumButton = ({
address,
chainId,
onSuccess,
}: Props) => {
const [state, setState] = React.useState<{
error?: Error
loading?: boolean
}>({})
const [, signMessage] = useSignMessage()

const handleSignIn = React.useCallback(async () => {
try {
setState((x) => ({ ...x, error: undefined, loading: true }))
const nonceRes = await fetch('/api/nonce')
const message = new SiweMessage({
domain: window.location.host,
address,
statement: 'Sign in with Ethereum to the app.',
uri: window.location.origin,
version: '1',
chainId,
nonce: await nonceRes.text(),
})

const signRes = await signMessage({ message: message.prepareMessage() })
if (signRes.error) throw signRes.error

const signature = signRes.data
const verifyRes = await fetch('/api/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message, signature }),
})
if (!verifyRes.ok) throw new Error('Error verifying message')

setState((x) => ({ ...x, loading: false }))
onSuccess && onSuccess({ address })
} catch (error) {
setState((x) => ({ ...x, error: error as Error, loading: false }))
}
}, [address, chainId, signMessage, onSuccess])

return (
<Stack space="4">
<Button
prefix={!state.loading && <IconEth />}
width="full"
loading={state.loading}
disabled={state.loading}
center
onClick={handleSignIn}
>
{state.loading ? 'Check Wallet' : 'Sign-In with Ethereum'}
</Button>

{state.error && (
<Text color="red">{state.error?.message ?? 'Failed to sign in'}</Text>
)}
</Stack>
)
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import * as React from 'react'
import { Button, Stack, Text } from 'degen'
import { useConnect } from 'wagmi'
import { Connector, ConnectorData, useConnect } from 'wagmi'

import { useIsMounted } from '../hooks'
import { useIsMounted } from '../../hooks'

export const WalletSelector = () => {
type Props = {
onError?(error: Error): void
onSuccess?(data: ConnectorData): void
}

export const WalletSelector = ({ onError, onSuccess }: Props) => {
const isMounted = useIsMounted()
const [
{
Expand All @@ -15,6 +20,15 @@ export const WalletSelector = () => {
connect,
] = useConnect()

const handleConnect = React.useCallback(
async (connector: Connector) => {
const { data, error } = await connect(connector)
if (error) onError?.(error)
if (data) onSuccess?.(data)
},
[connect, onError, onSuccess],
)

return (
<Stack space="4">
{connectors.map((x) => (
Expand All @@ -25,7 +39,7 @@ export const WalletSelector = () => {
loading={loading && x.name === connector?.name}
disabled={isMounted ? !x.ready : false}
key={x.id}
onClick={() => connect(x)}
onClick={() => handleConnect(x)}
>
{isMounted ? x.name : x.id === 'injected' ? x.id : x.name}
{isMounted ? !x.ready && ' (unsupported)' : ''}
Expand Down
3 changes: 3 additions & 0 deletions docs/components/web3/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { Account } from './Account'
export { WalletSelector } from './WalletSelector'
export { SignInWithEthereumButton } from './SignInWithEthereumButton'
2 changes: 2 additions & 0 deletions docs/lib/address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const formatAddress = (address: string) =>
`${address.slice(0, 6)}${address.slice(38, 42)}`
9 changes: 9 additions & 0 deletions docs/lib/iron.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { IronSessionOptions } from 'iron-session'

export const ironOptions: IronSessionOptions = {
cookieName: 'siwe',
password: process.env.NEXT_IRON_PASSWORD as string,
cookieOptions: {
secure: process.env.NODE_ENV === 'production',
},
}
2 changes: 2 additions & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
"@reach/skip-nav": "^0.16.0",
"degen": "^0.0.49",
"ethers": "^5.5.1",
"iron-session": "^6.0.5",
"next": "12.0.7",
"next-themes": "0.0.8",
"nextra": "v2.0.0-beta.5",
"nextra-theme-docs": "2.0.0-beta.5",
"react": "17.0.2",
"react-dom": "17.0.2",
"siwe": "^1.1.0",
"wagmi": "0.2.8"
},
"devDependencies": {
Expand Down
19 changes: 19 additions & 0 deletions docs/pages/api/logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { withIronSessionApiRoute } from 'iron-session/next'
import { NextApiRequest, NextApiResponse } from 'next'

import { ironOptions } from '../../lib/iron'

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { method } = req
switch (method) {
case 'GET':
req.session.destroy()
res.send({ ok: true })
break
default:
res.setHeader('Allow', ['GET'])
res.status(405).end(`Method ${method} Not Allowed`)
}
}

export default withIronSessionApiRoute(handler, ironOptions)
18 changes: 18 additions & 0 deletions docs/pages/api/me.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { withIronSessionApiRoute } from 'iron-session/next'
import { NextApiRequest, NextApiResponse } from 'next'

import { ironOptions } from '../../lib/iron'

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { method } = req
switch (method) {
case 'GET':
res.send({ address: req.session.siwe?.address })
break
default:
res.setHeader('Allow', ['GET'])
res.status(405).end(`Method ${method} Not Allowed`)
}
}

export default withIronSessionApiRoute(handler, ironOptions)
17 changes: 17 additions & 0 deletions docs/pages/api/nonce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { generateNonce } from 'siwe'

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { method } = req
switch (method) {
case 'GET':
res.setHeader('Content-Type', 'text/plain')
res.send(generateNonce())
break
default:
res.setHeader('Allow', ['GET'])
res.status(405).end(`Method ${method} Not Allowed`)
}
}

export default handler
29 changes: 29 additions & 0 deletions docs/pages/api/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { withIronSessionApiRoute } from 'iron-session/next'
import { NextApiRequest, NextApiResponse } from 'next'
import { SiweMessage } from 'siwe'

import { ironOptions } from '../../lib/iron'

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { method } = req
switch (method) {
case 'POST':
try {
const { message, signature } = req.body
const siweMessage = new SiweMessage(message)
const fields = await siweMessage.validate(signature)
req.session.siwe = fields
await req.session.save()

res.json({ ok: true })
} catch (_error) {
res.json({ ok: false })
}
break
default:
res.setHeader('Allow', ['POST'])
res.status(405).end(`Method ${method} Not Allowed`)
}
}

export default withIronSessionApiRoute(handler, ironOptions)
4 changes: 2 additions & 2 deletions docs/pages/guides/connect-wallet.mdx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { ConnectWallet } from '../../components'
import { ConnectWallet } from '../../components/guides'

# Connect Wallet

Connecting wallets to your app is extremely simple when you use wagmi. It takes less than five minutes to get up and running with MetaMask, WalletConnect, and Coinbase Wallet!

The example below uses [`useConnect`](/docs/hooks/useConnect) and [`useAccount`](/docs/hooks/useAccount) to allow you to connect a wallet and view ENS information for the connected account. Test it out before moving on.
The example below uses [`useConnect`](/docs/hooks/useConnect) and [`useAccount`](/docs/hooks/useAccount) to allow you to connect a wallet and view ENS information for the connected account. Try it out before moving on.

<ConnectWallet />

Expand Down
Loading

0 comments on commit dceeb43

Please sign in to comment.