Skip to content

Commit

Permalink
⛓ Allow to use multichain with SIWE (#953)
Browse files Browse the repository at this point in the history
  • Loading branch information
jakvbs committed Nov 8, 2022
1 parent 90722b6 commit 14b9f68
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 72 deletions.
5 changes: 5 additions & 0 deletions .changeset/fair-fans-promise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@usedapp/siwe": patch
---

⛓ Allow to use multichain
35 changes: 35 additions & 0 deletions packages/docs/playwright/with-metamask.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,41 @@ describe(`Browser: ${browserType.name()} with Metamask`, () => {
expect(await page.isVisible(`//*[text()='Not logged in']`)).to.be.true
})
})

it('Can sign in on multiple chains', async () => {
await page.goto(`${baseUrl}Guides/Sign%20in%20with%20Ethereum`)
await waitForExpect(async () => {
expect(await page.isVisible(`//*[text()='Not logged in']`)).to.be.true
})
let popupPromise = waitForPopup(context)
await page.click(XPath.text('button', 'Sign in'))
let popupPage = await popupPromise
await popupPage.click(XPath.text('button', 'Sign'))
await waitForExpect(async () => {
expect(await page.isVisible(`//*[text()='ChainId: ' and text()='5']`)).to.be.true
})

await metamask.switchToNetwork('Ethereum Mainnet')
await sleep(1000)

popupPromise = waitForPopup(context)
await page.click(XPath.text('button', 'Sign in'))
popupPage = await popupPromise
await popupPage.click(XPath.text('button', 'Sign'))
await waitForExpect(async () => {
expect(await page.isVisible(`//*[text()='ChainId: ' and text()='1']`)).to.be.true
})
await page.click(XPath.text('button', 'Sign out'))
await waitForExpect(async () => {
expect(await page.isVisible(`//*[text()='Not logged in']`)).to.be.true
})

await metamask.switchToNetwork('Goerli Test Network')

await waitForExpect(async () => {
expect(await page.isVisible(`//*[text()='ChainId: ' and text()='5']`)).to.be.true
})
})
})
})

Expand Down
2 changes: 1 addition & 1 deletion packages/playwright/src/metamask/MetaMask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class MetaMask {
log('Metamask account connected to pages.')
}

async switchToNetwork(network: 'Ethereum Mainnet' | 'Localhost 8545') {
async switchToNetwork(network: 'Ethereum Mainnet' | 'Localhost 8545' | 'Goerli Test Network') {
log('Switching network...')
await this.gotoMetamask()
await this.page.click('.network-display--clickable') // Network popup menu on the top right.
Expand Down
133 changes: 63 additions & 70 deletions packages/siwe/src/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React, { useEffect, ReactNode, useState, useCallback } from 'react'
import { createContext, useContext } from 'react'
import { SiweMessage } from 'siwe'
import { GNOSIS_SAFE_ABI } from './constants'
import { SiweFetchers, getFetchers } from './requests'
import { SiweFetchers, getFetchers, AuthResponse } from './requests'

export interface SiweContextValue {
signIn: (signInOptions?: SignInOptions) => void
Expand All @@ -13,7 +13,7 @@ export interface SiweContextValue {
isLoading: boolean
cancelLoading: () => void
message: SiweMessage | undefined
error: any
error: Error | undefined
}

const SiweContext = createContext<SiweContextValue>({
Expand All @@ -40,7 +40,6 @@ export interface SiweProviderProps {
children?: ReactNode
api?: SiweFetchers
}

const clearStorage = () => {
localStorage.removeItem('siweMessage')
localStorage.removeItem('getMessageHash')
Expand All @@ -51,65 +50,65 @@ export const SiweProvider = ({ children, backendUrl, api }: SiweProviderProps) =
const [isLoggedIn, setLoggedIn] = useState(false)
const [isLoading, setLoading] = useState(false)
const [message, setMessage] = useState<SiweMessage | undefined>(undefined)
const [error, setError] = useState<any>(undefined)
const { getNonce, getAuth, signIn, signOut } = api ?? getFetchers(backendUrl ?? '')

const getAuthRequestHandler = () => {
setLoading(true)
const [error, setError] = useState<Error | undefined>(undefined)
const {
getNonce: getNonceRequest,
getAuth: getAuthRequest,
signIn: signInRequest,
signOut: signOutRequest,
} = api ?? getFetchers(backendUrl ?? '', { chainId, address: account })

const getNonceHandler = async () => {
setError(undefined)
getAuth()
.then((res) => {
if (res.loggedIn) {
const siweMessage = res.message as SiweMessage
setMessage(siweMessage)
setLoading(false)
if (siweMessage.address !== account) {
setError(new Error('Account mismatch'))
return setLoggedIn(false)
}
if (siweMessage.chainId !== chainId) {
setError(new Error('ChainId mismatch'))
return setLoggedIn(false)
}
return setLoggedIn(true)
}
if (localStorage.getItem('getMessageHash')) {
const siweMessage = new SiweMessage(JSON.parse(localStorage.getItem('siweMessage') as string))
void createMultiSigListener({
message: siweMessage,
})
} else {
setLoading(false)
}
setLoggedIn(false)
})
.catch((err) => {
setError(err)
setLoggedIn(false)
setLoading(false)
})
try {
const { nonce } = await getNonceRequest()
return nonce
} catch (err: any) {
setError(err)
setLoading(false)
}
}

const signOutRequestHandler = async () => {
const getAuthHandler = async () => {
setLoading(true)
setError(undefined)
try {
await signOut()
} catch (err) {
const res: AuthResponse = await getAuthRequest()
if (res.loggedIn) {
const siweMessage = res.message as SiweMessage
setMessage(siweMessage)
setLoggedIn(true)
} else {
setMessage(undefined)
setLoggedIn(false)
}

if (localStorage.getItem('getMessageHash')) {
const siweMessage = new SiweMessage(JSON.parse(localStorage.getItem('siweMessage') as string))
void createMultiSigListener({
message: siweMessage,
})
return
}

setLoading(false)
} catch (err: any) {
setError(err)
} finally {
clearStorage()
setLoggedIn(false)
setMessage(undefined)
setLoading(false)
}
}

const getNonceRequestHandler = async () => {
const signOutHandler = async () => {
setError(undefined)
try {
const { nonce } = await getNonce()
return nonce
} catch (err) {
await signOutRequest()
} catch (err: any) {
setError(err)
clearStorage()
} finally {
setLoggedIn(false)
setMessage(undefined)
}
}

Expand All @@ -122,21 +121,20 @@ export const SiweProvider = ({ children, backendUrl, api }: SiweProviderProps) =
if (!account || !chainId) {
return
}
void getAuthRequestHandler()
void getAuthHandler()
}, [account, chainId])

const handleSignIn = useCallback(
const signIn = useCallback(
async (signInOptions?: SignInOptions) => {
if (!account || !chainId || !library) {
return
}
const signer = 'getSigner' in library ? library.getSigner() : undefined
if (!signer) return
const nonce = await getNonceRequestHandler()

if (!nonce) {
return
}
setLoading(true)
const nonce = await getNonceHandler()
if (!nonce) return

const message = new SiweMessage({
domain: signInOptions?.domain ?? window.location.host,
Expand All @@ -148,18 +146,15 @@ export const SiweProvider = ({ children, backendUrl, api }: SiweProviderProps) =
nonce,
})

setLoading(true)
const signature = await signer.signMessage(message.prepareMessage()).catch((err) => {
setError(err)
setLoading(false)
})
if (!signature) {
return
}
if (!signature) return

try {
await signIn({ signature, message })
} catch (err) {
await signInRequest({ signature, message })
} catch (err: any) {
return setError(err)
}

Expand All @@ -170,16 +165,16 @@ export const SiweProvider = ({ children, backendUrl, api }: SiweProviderProps) =
})
}

void getAuthRequestHandler()
void getAuthHandler()
},
[account, chainId, library]
)

const handleSignOut = useCallback(async () => {
const signOut = useCallback(async () => {
if (!account || !chainId) {
return
}
await signOutRequestHandler()
await signOutHandler()
}, [account, chainId])

const createMultiSigListener = async ({ message }: { message: SiweMessage }) => {
Expand All @@ -192,17 +187,15 @@ export const SiweProvider = ({ children, backendUrl, api }: SiweProviderProps) =
}

const onMultiSigSigned = async () => {
gnosisSafeContract.removeListener(gnosisSafeContract.filters.SignMsg(getMessageHash), onMultiSigSigned)
localStorage.removeItem('getMessageHash')
localStorage.removeItem('siweMessage')
void getAuthRequestHandler()
clearStorage()
void getAuthHandler()
}
gnosisSafeContract.once(gnosisSafeContract.filters.SignMsg(getMessageHash), onMultiSigSigned)
}

const value = {
signIn: handleSignIn,
signOut: handleSignOut,
signIn,
signOut,
isLoggedIn,
isLoading,
cancelLoading,
Expand Down
14 changes: 13 additions & 1 deletion packages/siwe/src/requests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export interface SiweFetchers {
signOut: () => Promise<void>
}

export interface UserData {
chainId?: number
address?: string
}

const failedAuthResponse = {
loggedIn: false,
message: undefined,
Expand All @@ -32,11 +37,14 @@ const failedNonceResponse = {
ok: false,
}

export const getFetchers = (backendUrl: string): SiweFetchers => {
export const getFetchers = (backendUrl: string, { address, chainId }: UserData): SiweFetchers => {
return {
getAuth: async () => {
const authRequest = await fetch(`${backendUrl}/siwe/me`, {
credentials: 'include',
headers: {
multichain: `${address}:${chainId}`,
},
})

if (!authRequest.ok) {
Expand Down Expand Up @@ -78,7 +86,11 @@ export const getFetchers = (backendUrl: string): SiweFetchers => {
},
signOut: async () => {
await fetch(`${backendUrl}/siwe/signout`, {
method: 'POST',
credentials: 'include',
headers: {
multichain: `${address}:${chainId}`,
},
})
},
}
Expand Down

2 comments on commit 14b9f68

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.