Feat/examples upgrade (#153)

* Added UI infastructure to support chain switching, need to update the session to swap chains

* Fixed type of chainId, string -> number

* Added helper method to find full chainname ex/ cosmoshub-4 -> cosmons:cosmoshub-4. Added a method updateSignClientChainId to emit a new update with the new namespace for the respected chain switch

* Added checks to ensure no dup accounts get added to namespaces during chainswitching from chain A->B->A for ex

* Fixed chainId type, eip155 uses number but the other chains use strings, making string the default type for chainId

* Update wallets/react-wallet-v2/src/utils/WalletConnectUtil.ts

Co-authored-by: Ben Kremer <ben@walletconnect.com>

* Switched the way account card components are generated in index page, removed unnecessary helper fx

* Fixed the chain changed event by updating the session if the chainId namespace does not currently exist, then emitting the correct chainChanged event on the same session

---------

Co-authored-by: Ben Kremer <ben@walletconnect.com>
This commit is contained in:
Jonathan Conn 2023-05-10 10:01:36 -04:00 committed by GitHub
parent 74c54bf6f0
commit d774f52135
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 124 additions and 38 deletions

View File

@ -1,25 +1,37 @@
import ChainCard from '@/components/ChainCard' import ChainCard from '@/components/ChainCard'
import SettingsStore from '@/store/SettingsStore'
import { eip155Addresses } from '@/utils/EIP155WalletUtil'
import { truncate } from '@/utils/HelperUtil' import { truncate } from '@/utils/HelperUtil'
import { updateSignClientChainId } from '@/utils/WalletConnectUtil'
import { Avatar, Button, Text, Tooltip } from '@nextui-org/react' import { Avatar, Button, Text, Tooltip } from '@nextui-org/react'
import Image from 'next/image' import Image from 'next/image'
import { useState } from 'react' import { useState } from 'react'
import { useSnapshot } from 'valtio'
interface Props { interface Props {
name: string name: string
logo: string logo: string
rgb: string rgb: string
address: string address: string
chainId: string
} }
export default function AccountCard({ name, logo, rgb, address }: Props) { export default function AccountCard({ name, logo, rgb, address, chainId }: Props) {
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
const { activeChainId, account } = useSnapshot(
SettingsStore.state,
);
function onCopy() { function onCopy() {
navigator?.clipboard?.writeText(address) navigator?.clipboard?.writeText(address)
setCopied(true) setCopied(true)
setTimeout(() => setCopied(false), 1500) setTimeout(() => setCopied(false), 1500)
} }
async function onChainChanged(chainId: string, address: string) {
SettingsStore.setActiveChainId(chainId);
await updateSignClientChainId(chainId.toString(), address);
}
return ( return (
<ChainCard rgb={rgb} flexDirection="row" alignItems="center"> <ChainCard rgb={rgb} flexDirection="row" alignItems="center">
<Avatar src={logo} /> <Avatar src={logo} />
@ -46,6 +58,19 @@ export default function AccountCard({ name, logo, rgb, address }: Props) {
/> />
</Button> </Button>
</Tooltip> </Tooltip>
<Button
size="sm"
css={{
minWidth: "auto",
backgroundColor: "rgba(255, 255, 255, 0.15)",
marginLeft: "$5",
}}
onPress={() => {
onChainChanged(chainId, address);
}}
>
{activeChainId === chainId ? `` : `🔄`}
</Button>
</ChainCard> </ChainCard>
) )
} }

View File

@ -26,6 +26,7 @@ export const POLKADOT_TEST_CHAINS = {
} }
} }
export const POLKADOT_CHAINS = { ...POLKADOT_MAINNET_CHAINS, ...POLKADOT_TEST_CHAINS }
/** /**
* Methods * Methods
*/ */

View File

@ -35,26 +35,26 @@ export default function HomePage() {
<Text h4 css={{ marginBottom: '$5' }}> <Text h4 css={{ marginBottom: '$5' }}>
Mainnets Mainnets
</Text> </Text>
{Object.values(EIP155_MAINNET_CHAINS).map(({ name, logo, rgb }) => ( {Object.entries(EIP155_MAINNET_CHAINS).map(([caip10, {name, logo, rgb}]) => (
<AccountCard key={name} name={name} logo={logo} rgb={rgb} address={eip155Address} /> <AccountCard key={name} name={name} logo={logo} rgb={rgb} address={eip155Address} chainId={caip10.toString()}/>
))} ))}
{Object.values(COSMOS_MAINNET_CHAINS).map(({ name, logo, rgb }) => ( {Object.entries(COSMOS_MAINNET_CHAINS).map(([caip10, {name, logo, rgb}]) => (
<AccountCard key={name} name={name} logo={logo} rgb={rgb} address={cosmosAddress} /> <AccountCard key={name} name={name} logo={logo} rgb={rgb} address={cosmosAddress} chainId={caip10}/>
))} ))}
{Object.values(SOLANA_MAINNET_CHAINS).map(({ name, logo, rgb }) => ( {Object.entries(SOLANA_MAINNET_CHAINS).map(([caip10, {name, logo, rgb}]) => (
<AccountCard key={name} name={name} logo={logo} rgb={rgb} address={solanaAddress} /> <AccountCard key={name} name={name} logo={logo} rgb={rgb} address={solanaAddress} chainId={caip10}/>
))} ))}
{Object.values(POLKADOT_MAINNET_CHAINS).map(({ name, logo, rgb }) => ( {Object.entries(POLKADOT_MAINNET_CHAINS).map(([caip10, {name, logo, rgb}]) => (
<AccountCard key={name} name={name} logo={logo} rgb={rgb} address={polkadotAddress} /> <AccountCard key={name} name={name} logo={logo} rgb={rgb} address={polkadotAddress} chainId={caip10}/>
))} ))}
{Object.values(ELROND_MAINNET_CHAINS).map(({ name, logo, rgb }) => ( {Object.entries(ELROND_MAINNET_CHAINS).map(([caip10, {name, logo, rgb}]) => (
<AccountCard key={name} name={name} logo={logo} rgb={rgb} address={elrondAddress} /> <AccountCard key={name} name={name} logo={logo} rgb={rgb} address={elrondAddress} chainId={caip10}/>
))} ))}
{Object.values(TRON_MAINNET_CHAINS).map(({ name, logo, rgb }) => ( {Object.entries(TRON_MAINNET_CHAINS).map(([caip10, {name, logo, rgb}]) => (
<AccountCard key={name} name={name} logo={logo} rgb={rgb} address={tronAddress} /> <AccountCard key={name} name={name} logo={logo} rgb={rgb} address={tronAddress} chainId={caip10}/>
))} ))}
{Object.values(TEZOS_MAINNET_CHAINS).map(({ name, logo, rgb }) => ( {Object.entries(TEZOS_MAINNET_CHAINS).map(([caip10, {name, logo, rgb}]) => (
<AccountCard key={name} name={name} logo={logo} rgb={rgb} address={tezosAddress} /> <AccountCard key={name} name={name} logo={logo} rgb={rgb} address={tezosAddress} chainId={caip10}/>
))} ))}
{testNets ? ( {testNets ? (
@ -62,26 +62,26 @@ export default function HomePage() {
<Text h4 css={{ marginBottom: '$5' }}> <Text h4 css={{ marginBottom: '$5' }}>
Testnets Testnets
</Text> </Text>
{Object.values(EIP155_TEST_CHAINS).map(({ name, logo, rgb }) => ( {Object.entries(EIP155_TEST_CHAINS).map(([caip10, {name, logo, rgb}]) => (
<AccountCard key={name} name={name} logo={logo} rgb={rgb} address={eip155Address} /> <AccountCard key={name} name={name} logo={logo} rgb={rgb} address={eip155Address} chainId={caip10.toString()}/>
))} ))}
{Object.values(SOLANA_TEST_CHAINS).map(({ name, logo, rgb }) => ( {Object.entries(SOLANA_TEST_CHAINS).map(([caip10, {name, logo, rgb}]) => (
<AccountCard key={name} name={name} logo={logo} rgb={rgb} address={solanaAddress} /> <AccountCard key={name} name={name} logo={logo} rgb={rgb} address={solanaAddress} chainId={caip10}/>
))} ))}
{Object.values(POLKADOT_TEST_CHAINS).map(({ name, logo, rgb }) => ( {Object.entries(POLKADOT_TEST_CHAINS).map(([caip10, {name, logo, rgb}]) => (
<AccountCard key={name} name={name} logo={logo} rgb={rgb} address={polkadotAddress} /> <AccountCard key={name} name={name} logo={logo} rgb={rgb} address={polkadotAddress} chainId={caip10}/>
))} ))}
{Object.values(NEAR_TEST_CHAINS).map(({ name, logo, rgb }) => ( {Object.entries(NEAR_TEST_CHAINS).map(([caip10, {name, logo, rgb}]) => (
<AccountCard key={name} name={name} logo={logo} rgb={rgb} address={nearAddress} /> <AccountCard key={name} name={name} logo={logo} rgb={rgb} address={nearAddress} chainId={caip10}/>
))} ))}
{Object.values(ELROND_TEST_CHAINS).map(({ name, logo, rgb }) => ( {Object.entries(ELROND_TEST_CHAINS).map(([caip10, {name, logo, rgb}]) => (
<AccountCard key={name} name={name} logo={logo} rgb={rgb} address={elrondAddress} /> <AccountCard key={name} name={name} logo={logo} rgb={rgb} address={elrondAddress} chainId={caip10}/>
))} ))}
{Object.values(TRON_TEST_CHAINS).map(({ name, logo, rgb }) => ( {Object.entries(TRON_TEST_CHAINS).map(([caip10, {name, logo, rgb}]) => (
<AccountCard key={name} name={name} logo={logo} rgb={rgb} address={tronAddress} /> <AccountCard key={name} name={name} logo={logo} rgb={rgb} address={tronAddress} chainId={caip10}/>
))} ))}
{Object.values(TEZOS_TEST_CHAINS).map(({ name, logo, rgb }) => ( {Object.entries(TEZOS_TEST_CHAINS).map(([caip10, {name, logo, rgb}]) => (
<AccountCard key={name} name={name} logo={logo} rgb={rgb} address={tezosAddress} /> <AccountCard key={name} name={name} logo={logo} rgb={rgb} address={tezosAddress} chainId={caip10}/>
))} ))}
</Fragment> </Fragment>
) : null} ) : null}

View File

@ -1,11 +1,13 @@
import PageHeader from '@/components/PageHeader' import PageHeader from '@/components/PageHeader'
import ProjectInfoCard from '@/components/ProjectInfoCard' import ProjectInfoCard from '@/components/ProjectInfoCard'
import SessionChainCard from '@/components/SessionChainCard' import SessionChainCard from '@/components/SessionChainCard'
import SettingsStore from '@/store/SettingsStore'
import { signClient } from '@/utils/WalletConnectUtil' import { signClient } from '@/utils/WalletConnectUtil'
import { Button, Divider, Loading, Row, Text } from '@nextui-org/react' import { Button, Divider, Loading, Row, Text } from '@nextui-org/react'
import { getSdkError } from '@walletconnect/utils' import { getSdkError } from '@walletconnect/utils'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { Fragment, useEffect, useState } from 'react' import { Fragment, useEffect, useState } from 'react'
import { useSnapshot } from 'valtio'
/** /**
* Component * Component
@ -16,6 +18,8 @@ export default function SessionPage() {
const { query, replace } = useRouter() const { query, replace } = useRouter()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const { activeChainId } = useSnapshot(SettingsStore.state);
useEffect(() => { useEffect(() => {
if (query?.topic) { if (query?.topic) {
setTopic(query.topic as string) setTopic(query.topic as string)
@ -48,11 +52,10 @@ export default function SessionPage() {
async function onSessionEmit() { async function onSessionEmit() {
setLoading(true) setLoading(true)
console.log('baleg')
await signClient.emit({ await signClient.emit({
topic, topic,
event: { name: 'chainChanged', data: 'Hello World' }, event: { name: 'chainChanged', data: 'Hello World' },
chainId: 'eip155:1' chainId: activeChainId.toString() // chainId: 'eip155:1'
}) })
setLoading(false) setLoading(false)
} }

View File

@ -15,6 +15,7 @@ interface State {
tronAddress: string tronAddress: string
tezosAddress: string tezosAddress: string
relayerRegionURL: string relayerRegionURL: string
activeChainId: string
} }
/** /**
@ -23,6 +24,7 @@ interface State {
const state = proxy<State>({ const state = proxy<State>({
testNets: typeof localStorage !== 'undefined' ? Boolean(localStorage.getItem('TEST_NETS')) : true, testNets: typeof localStorage !== 'undefined' ? Boolean(localStorage.getItem('TEST_NETS')) : true,
account: 0, account: 0,
activeChainId: '1',
eip155Address: '', eip155Address: '',
cosmosAddress: '', cosmosAddress: '',
solanaAddress: '', solanaAddress: '',
@ -78,6 +80,10 @@ const SettingsStore = {
state.tezosAddress = tezosAddress state.tezosAddress = tezosAddress
}, },
setActiveChainId(value: string) {
state.activeChainId = value
},
toggleTestNets() { toggleTestNets() {
state.testNets = !state.testNets state.testNets = !state.testNets
if (state.testNets) { if (state.testNets) {

View File

@ -1,9 +1,12 @@
import { COSMOS_MAINNET_CHAINS, TCosmosChain } from '@/data/COSMOSData' import { COSMOS_MAINNET_CHAINS, TCosmosChain } from '@/data/COSMOSData'
import { EIP155_CHAINS, TEIP155Chain } from '@/data/EIP155Data' import { EIP155_CHAINS, TEIP155Chain } from '@/data/EIP155Data'
import { NEAR_TEST_CHAINS, TNearChain } from '@/data/NEARData'
import { SOLANA_CHAINS, TSolanaChain } from '@/data/SolanaData'
import { ELROND_CHAINS, TElrondChain } from '@/data/ElrondData' import { ELROND_CHAINS, TElrondChain } from '@/data/ElrondData'
import { NEAR_TEST_CHAINS, TNearChain } from '@/data/NEARData'
import { POLKADOT_CHAINS, TPolkadotChain } from '@/data/PolkadotData'
import { SOLANA_CHAINS, TSolanaChain } from '@/data/SolanaData'
import { TEZOS_CHAINS, TTezosChain } from '@/data/TezosData'
import { TRON_CHAINS, TTronChain } from '@/data/TronData' import { TRON_CHAINS, TTronChain } from '@/data/TronData'
import { utils } from 'ethers' import { utils } from 'ethers'
/** /**
@ -137,11 +140,13 @@ export function isTezosChain(chain: string) {
*/ */
export function formatChainName(chainId: string) { export function formatChainName(chainId: string) {
return ( return (
EIP155_CHAINS[chainId as TEIP155Chain]?.name ??
COSMOS_MAINNET_CHAINS[chainId as TCosmosChain]?.name ?? COSMOS_MAINNET_CHAINS[chainId as TCosmosChain]?.name ??
SOLANA_CHAINS[chainId as TSolanaChain]?.name ?? EIP155_CHAINS[chainId as TEIP155Chain]?.name ??
NEAR_TEST_CHAINS[chainId as TNearChain]?.name ??
ELROND_CHAINS[chainId as TElrondChain]?.name ?? ELROND_CHAINS[chainId as TElrondChain]?.name ??
NEAR_TEST_CHAINS[chainId as TNearChain]?.name ??
POLKADOT_CHAINS[chainId as TPolkadotChain]?.name ??
SOLANA_CHAINS[chainId as TSolanaChain]?.name ??
TEZOS_CHAINS[chainId as TTezosChain]?.name ??
TRON_CHAINS[chainId as TTronChain]?.name ?? TRON_CHAINS[chainId as TTronChain]?.name ??
chainId chainId
) )

View File

@ -1,5 +1,4 @@
import SignClient from '@walletconnect/sign-client' import SignClient from '@walletconnect/sign-client'
export let signClient: SignClient export let signClient: SignClient
export async function createSignClient(relayerRegionURL: string) { export async function createSignClient(relayerRegionURL: string) {
@ -15,3 +14,50 @@ export async function createSignClient(relayerRegionURL: string) {
} }
}) })
} }
export async function updateSignClientChainId(chainId: string, address: string) {
// get most recent session
const session = signClient.session.getAll()[0]
if (!session) return
// if chainId does not exist in session, an update is required first
if (!session.namespaces[chainId]) {
const newNamespace = {
[chainId]: {
accounts: [`${chainId}:${address}`],
methods: [
'eth_sendTransaction',
'eth_signTransaction',
'eth_sign',
'personal_sign',
'eth_signTypedData'
],
events: ['chainChanged', 'accountsChanged']
}
}
try {
// need to wait for update to finish before emit
await signClient.update({
topic: session.topic,
namespaces: { ...session.namespaces, ...newNamespace }
})
} catch (err: unknown) {
console.error(`Failed to update session: ${err}`)
}
}
const payload = {
topic: session.topic,
event: {
name: 'chainChanged',
data: [address]
},
chainId
}
try {
signClient.emit(payload)
} catch (err: unknown) {
console.error(`Failed to emit chainChanged event: ${err}`)
}
}