Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ethereum network check and error handling #137

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/ui/.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
PUBLIC_PROVIDER=https://goerli.infura.io/v3/6d4ffa9d4df447ebb73468f4efcb8e8e
PUBLIC_GROUP_ID=38219357914
PUBLIC_GLOBAL_ANONYMOUS_FEED_ADDRESS=0x7A5fe8899b9e189483Db0545c71D6cad92df93F0
PUBLIC_TARGET_CHAIN_ID=0x5
39 changes: 39 additions & 0 deletions packages/ui/src/lib/components/modal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script lang="ts">
import Button from './button.svelte'

let cls: string | undefined = undefined
export { cls as class }
export let message: string
export let action: (() => void) | undefined = undefined
</script>

<div class={`root ${cls}`}>
<div>
<p>{message}</p>
{#if action !== undefined}
<Button variant="secondary" label="OK" on:click={action} />
{/if}
</div>
</div>

<style lang="scss">
.root {
left: 0;
right: 0;
top: 0;
bottom: 0;
padding-left: var(--spacing-12);
padding-right: var(--spacing-12);
background-color: #0005;
z-index: 300;
position: fixed;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.root div {
background-color: white;
padding: 50px;
}
</style>
2 changes: 2 additions & 0 deletions packages/ui/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import {
PUBLIC_PROVIDER,
PUBLIC_GROUP_ID,
PUBLIC_GLOBAL_ANONYMOUS_FEED_ADDRESS,
PUBLIC_TARGET_CHAIN_ID,
} from '$env/static/public'

export const GLOBAL_ANONYMOUS_FEED_ADDRESS = PUBLIC_GLOBAL_ANONYMOUS_FEED_ADDRESS
export const GROUP_ID = PUBLIC_GROUP_ID
export const PROVIDER = PUBLIC_PROVIDER
export const TARGET_CHAIN_ID = PUBLIC_TARGET_CHAIN_ID
59 changes: 59 additions & 0 deletions packages/ui/src/lib/services/blockchain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { providers, Signer } from 'ethers'
import { TARGET_CHAIN_ID } from '$lib/constants'
import { browser } from '$app/environment'

type WindowWithEthereum = Window & typeof globalThis & { ethereum: providers.ExternalProvider }

export async function connectWallet(network?: providers.Networkish): Promise<Signer> {
const provider = new providers.Web3Provider(getEthereum(), network)
await provider.send('eth_requestAccounts', [])
return provider.getSigner()
}

export function canConnectWallet() {
return browser && Boolean((window as WindowWithEthereum)?.ethereum)
}

export function hasWallet(w?: Window & typeof globalThis): w is WindowWithEthereum {
return browser && Boolean(((w ?? window) as WindowWithEthereum)?.ethereum)
}

export function getEthereum(): providers.ExternalProvider {
if (hasWallet()) {
return (window as WindowWithEthereum)?.ethereum
}

throw new Error('No web3 wallet found')
}

export async function checkNetwork(targetChainId = TARGET_CHAIN_ID) {
if (!browser) return

const ethereum = (window as WindowWithEthereum)?.ethereum
if (ethereum && ethereum.request) {
const currentChainId = await ethereum.request({
method: 'eth_chainId',
})

if (Number.parseInt(currentChainId, 16) === Number.parseInt(targetChainId, 16)) {
return true
}
}

return false
}

export async function switchNetwork(targetChainId = TARGET_CHAIN_ID) {
if (!browser) return

const ethereum = (window as WindowWithEthereum)?.ethereum
if (ethereum && ethereum.request) {
await ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: targetChainId }],
})

// refresh
window.location.reload()
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ethers, providers, Signer, type BigNumberish, type ContractTransaction } from 'ethers'
import { Identity } from '@semaphore-protocol/identity'
import { ethers, Signer, type BigNumberish, type ContractTransaction } from 'ethers'
import { Group, type Member } from '@semaphore-protocol/group'
import { Identity } from '@semaphore-protocol/identity'
import {
generateProof,
packToSolidityProof,
Expand All @@ -16,19 +16,6 @@ import { GLOBAL_ANONYMOUS_FEED_ADDRESS, GROUP_ID } from '$lib/constants'
import { solidityKeccak256, type BytesLike, type Hexable } from 'ethers/lib/utils'
import type { PromiseOrValue } from '$lib/assets/typechain/common'

type WindowWithEthereum = Window &
typeof globalThis & { ethereum: providers.ExternalProvider | providers.JsonRpcFetchFunc }

export async function connectWallet(network?: providers.Networkish): Promise<Signer> {
const provider = new providers.Web3Provider((window as WindowWithEthereum).ethereum, network)
await provider.send('eth_requestAccounts', [])
return provider.getSigner()
}

export function canConnectWallet() {
return Boolean((window as WindowWithEthereum)?.ethereum)
}

export async function createIdentity(signer: Signer, secret: string) {
const identitySeed = await signer.signMessage(secret)
return new Identity(identitySeed)
Expand Down
18 changes: 18 additions & 0 deletions packages/ui/src/lib/stores/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { writable, type Writable } from 'svelte/store'

export interface UserStore extends Writable<Error[]> {
add: (error: Error) => void
}

function createErrorStore(): UserStore {
const store = writable<Error[]>([])

return {
...store,
add: (error: Error) => {
store.update((errors) => [...errors, error])
},
}
}

export const errors = createErrorStore()
12 changes: 9 additions & 3 deletions packages/ui/src/lib/stores/post.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { writable, type Writable } from 'svelte/store'
import { browser } from '$app/environment'
import { getGlobalAnonymousFeed } from '$lib/services'
import { getGlobalAnonymousFeed } from '$lib/services/zk'
import { providers } from 'ethers'
import { PROVIDER } from '$lib/constants'
import { errors } from '$lib/stores/errors'
import type { ErrorWithCode } from '$lib/types'

export interface Post {
timestamp: number
Expand Down Expand Up @@ -36,8 +38,12 @@ async function pullFeed() {
localStorage.setItem('messages', JSON.stringify(messages))
}
}
} catch (e) {
console.error(e)
} catch (error) {
if ((error as ErrorWithCode).code === 'NETWORK_ERROR') {
errors.add(new Error('Could not connect to the ethereum blockchain.'))
} else {
errors.add(error as Error)
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ export interface IconProps {
size?: number
class?: string
}

export interface ErrorWithCode extends Error {
code?: string
}
71 changes: 71 additions & 0 deletions packages/ui/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,77 @@
import '@fontsource/source-sans-pro'
import '@fontsource/source-serif-pro'
import './styles.css'

import Modal from '$lib/components/modal.svelte'

import { errors } from '$lib/stores/errors'
import { onDestroy, onMount } from 'svelte'
import { checkNetwork, getEthereum, switchNetwork } from '$lib/services/blockchain'
import type { providers } from 'ethers'
import { TARGET_CHAIN_ID } from '$lib/constants'
import type { ErrorWithCode } from '$lib/types'
import { browser } from '$app/environment'

function switchToTargetNetwork() {
$errors = [new Error('You are connected to a wrong ethereum network. Please switch to Goerli')]
switchNetwork(TARGET_CHAIN_ID).catch(
() =>
($errors = [
new Error('Network change rejected, please change the network manually.'),
...$errors,
]),
)
}

onMount(() => {
if (!browser) return
const ethereum = getEthereum() as providers.Provider

checkNetwork(TARGET_CHAIN_ID)
.then((networkMatches) => {
if (!networkMatches) switchToTargetNetwork()
})
.catch(errors.add)

ethereum.on('chainChanged', (chainId: string) => {
if (chainId !== TARGET_CHAIN_ID) {
switchToTargetNetwork()
} else {
window.location.reload()
}
})

ethereum.on('disconnect', (error: ErrorWithCode) => {
console.log(error)
errors.add(
new Error(
'Disconnected from the ethereum network. Please check your internet connection and refresh the page',
),
)
})
})

onDestroy(() => {
try {
const ethereum = getEthereum() as providers.Provider

ethereum.off('chainChanged')
ethereum.off('disconnect')
} catch (error) {
console.error(error)
}
})
</script>

{#if $errors.length > 0}
<Modal
message={$errors[0].message}
action={() => {
const [err, ...rest] = $errors
$errors = rest
console.error(`Resolved error ${err.message}`)
}}
/>
{/if}

<slot />
6 changes: 3 additions & 3 deletions packages/ui/src/routes/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script lang="ts">
// import Header from '$lib/components/header.svelte'
import HeaderTop from '$lib/components/header-top.svelte'
import HeaderDescription from '$lib/components/header-description.svelte'
import Post from '$lib/components/post.svelte'
Expand All @@ -11,14 +10,15 @@
import { profile } from '$lib/stores/profile'
import { goto } from '$app/navigation'
import Masonry from '$lib/masonry.svelte'
import { checkNetwork } from '$lib/services/blockchain'

checkNetwork()

let y: number
</script>

<svelte:window bind:scrollY={y} />
<div>
<!-- <Header loggedin={$profile.signer !== undefined} /> -->

<HeaderTop loggedin={$profile.signer !== undefined} />
<HeaderDescription />

Expand Down
27 changes: 22 additions & 5 deletions packages/ui/src/routes/post/new/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import Close from '$lib/components/icons/close.svelte'
import SendAltFilled from '$lib/components/icons/send-alt-filled.svelte'
import InputString from '$lib/components/input-string.svelte'
import { profile } from '$lib/stores/profile'
import { goto } from '$app/navigation'
import { ROUTES } from '$lib/routes'
import {
Expand All @@ -12,8 +11,13 @@
getGlobalAnonymousFeed,
getRandomExternalNullifier,
validateProofOnChain,
} from '$lib/services/index'
} from '$lib/services/zk'

import { profile } from '$lib/stores/profile'
import { posts } from '$lib/stores/post'
import { errors } from '$lib/stores/errors'

import type { ErrorWithCode } from '$lib/types'

let cls: string | undefined = undefined
export { cls as class }
Expand All @@ -24,16 +28,21 @@
async function submit() {
try {
const signer = $profile.signer
if (!signer) throw new Error('no signer')
if (!signer)
throw new Error(
'Failed to post - could not connect to web3 wallet. Try and refresh the page.',
)

const identity = $profile.identities.anonymous
if (!identity) throw new Error('no identity')
if (!identity)
throw new Error('Failed to post - no identity found. Try and refresh the page.')

const globalAnonymousFeed = getGlobalAnonymousFeed(signer)
const group = await getContractGroup(globalAnonymousFeed)

const externalNullifier = getRandomExternalNullifier()
const proof = await generateGroupProof(group, identity, postText, externalNullifier)

const tx = await validateProofOnChain(globalAnonymousFeed, proof, postText, externalNullifier)

const res = await tx.wait()
Expand All @@ -45,7 +54,15 @@
})
goto(ROUTES.HOME)
} catch (error) {
console.error(error)
if ((error as ErrorWithCode)?.code === 'ACTION_REJECTED') {
errors.add(
new Error(
"You've rejected the post transaction. If you still want to publish your post, press publish button again.",
),
)
} else {
errors.add(error as Error)
}
}
}
</script>
Expand Down
5 changes: 2 additions & 3 deletions packages/ui/src/routes/profile/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@
import Wallet from '$lib/components/icons/wallet.svelte'
import WalletInfo from '$lib/components/wallet-info.svelte'
import { formatAddress } from '$lib/utils'
import { connectWallet, canConnectWallet } from '$lib/services/blockchain'
import {
connectWallet,
canConnectWallet,
createIdentity,
getGlobalAnonymousFeed,
getContractGroup,
joinGroupOffChain,
joinGroupOnChain,
} from '$lib/services'
} from '$lib/services/zk'
import { profile } from '$lib/stores/profile'

let y: number
Expand Down