Wrap up eth_sign and personal_sign examples

This commit is contained in:
Ilja 2022-02-14 15:38:09 +02:00
parent de05b8c82c
commit e2ac17957e
14 changed files with 183 additions and 73 deletions

View File

@ -19,7 +19,8 @@
"react-qr-reader-es6": "2.2.1-2", "react-qr-reader-es6": "2.2.1-2",
"framer-motion": "6.2.6", "framer-motion": "6.2.6",
"ethers": "5.5.4", "ethers": "5.5.4",
"valtio": "1.3.0" "valtio": "1.3.0",
"@json-rpc-tools/utils": "1.7.6"
}, },
"devDependencies": { "devDependencies": {
"@walletconnect/types": "2.0.0-beta.22", "@walletconnect/types": "2.0.0-beta.22",

View File

@ -1,5 +1,6 @@
import { truncate } from '@/utils/HelperUtil' import { truncate } from '@/utils/HelperUtil'
import { Avatar, Card, Text } from '@nextui-org/react' import { Avatar, Card, Text } from '@nextui-org/react'
import Link from 'next/link'
interface Props { interface Props {
name: string name: string
@ -10,36 +11,38 @@ interface Props {
export default function AccountCard({ name, logo, rgb, address }: Props) { export default function AccountCard({ name, logo, rgb, address }: Props) {
return ( return (
<Card <Link href={`/sessions?address=${address}`} passHref>
bordered <Card
clickable bordered
borderWeight="light" clickable
css={{ borderWeight="light"
borderColor: `rgba(${rgb}, 0.4)`,
boxShadow: `0 0 10px 0 rgba(${rgb}, 0.15)`,
backgroundColor: `rgba(${rgb}, 0.25)`,
marginBottom: '$6',
minHeight: '70px'
}}
>
<Card.Body
css={{ css={{
flexDirection: 'row', borderColor: `rgba(${rgb}, 0.4)`,
alignItems: 'center', boxShadow: `0 0 10px 0 rgba(${rgb}, 0.15)`,
justifyContent: 'space-between', backgroundColor: `rgba(${rgb}, 0.25)`,
overflow: 'hidden' marginBottom: '$6',
minHeight: '70px'
}} }}
> >
<Avatar src={logo} /> <Card.Body
<div style={{ flex: 1 }}> css={{
<Text h5 css={{ marginLeft: '$9' }}> flexDirection: 'row',
{name} alignItems: 'center',
</Text> justifyContent: 'space-between',
<Text weight="light" size={13} css={{ marginLeft: '$9' }}> overflow: 'hidden'
{truncate(address, 19)} }}
</Text> >
</div> <Avatar src={logo} />
</Card.Body> <div style={{ flex: 1 }}>
</Card> <Text h5 css={{ marginLeft: '$9' }}>
{name}
</Text>
<Text weight="light" size={13} css={{ marginLeft: '$9' }}>
{truncate(address, 19)}
</Text>
</div>
</Card.Body>
</Card>
</Link>
) )
} }

View File

@ -1,6 +1,6 @@
import ModalStore from '@/store/ModalStore' import ModalStore from '@/store/ModalStore'
import SessionProposalModal from '@/views/SessionProposalModal' import SessionProposalModal from '@/views/SessionProposalModal'
import SessionRequestModal from '@/views/SessionRequestModal' import SessionRequestModal from '@/views/SessionSignModal'
import { Modal as NextModal } from '@nextui-org/react' import { Modal as NextModal } from '@nextui-org/react'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
@ -10,7 +10,7 @@ export default function Modal() {
return ( return (
<NextModal blur open={open} style={{ border: '1px solid rgba(139, 139, 139, 0.4)' }}> <NextModal blur open={open} style={{ border: '1px solid rgba(139, 139, 139, 0.4)' }}>
{view === 'SessionProposalModal' && <SessionProposalModal />} {view === 'SessionProposalModal' && <SessionProposalModal />}
{view === 'SessionRequestModal' && <SessionRequestModal />} {view === 'SessionSignModal' && <SessionRequestModal />}
</NextModal> </NextModal>
) )
} }

View File

@ -3,20 +3,20 @@
* @url https://chainlist.org * @url https://chainlist.org
*/ */
/**
* Types
*/
export type TChain = keyof typeof MAINNET_CHAINS
/** /**
* Utilities * Utilities
*/ */
export const LOGO_BASE_URL = 'https://blockchain-api.xyz/logos/' const LOGO_BASE_URL = 'https://blockchain-api.xyz/logos/'
/**
* Types
*/
export type TEIP155Chain = keyof typeof EIP155_CHAINS
/** /**
* Chains * Chains
*/ */
export const MAINNET_CHAINS = { export const EIP155_CHAINS = {
'eip155:1': { 'eip155:1': {
chainId: 1, chainId: 1,
name: 'Ethereum', name: 'Ethereum',
@ -46,11 +46,12 @@ export const MAINNET_CHAINS = {
/** /**
* Methods * Methods
*/ */
export const SIGNING_METHODS = { export const EIP155_SIGNING_METHODS = {
PERSONAL_SIGN: 'personal_sign', PERSONAL_SIGN: 'personal_sign',
SEND_TRANSACTION: 'eth_sendTransaction', ETH_SIGN: 'eth_sign',
SIGN: 'eth_sign', ETH_SIGN_TRANSACTION: 'eth_signTransaction',
SIGN_TRANSACTION: 'eth_signTransaction', ETH_SIGN_TYPED_DATA: 'eth_signTypedData',
SIGN_TYPED_DATA: 'eth_signTypedData', ETH_SIGN_TYPED_DATA_V4: 'eth_signTypedData_v4',
SIGN_TYPED_DATA_V4: 'eth_signTypedData_v4' ETH_SIGN_RAW_TRANSACTION: 'eth_sendRawTransaction',
ETH_SEND_TRANSACTION: 'eth_sendTransaction'
} }

View File

@ -1,3 +1,4 @@
import { EIP155_SIGNING_METHODS } from '@/data/EIP155Data'
import ModalStore from '@/store/ModalStore' import ModalStore from '@/store/ModalStore'
import { walletConnectClient } from '@/utils/WalletConnectUtil' import { walletConnectClient } from '@/utils/WalletConnectUtil'
import { CLIENT_EVENTS } from '@walletconnect/client' import { CLIENT_EVENTS } from '@walletconnect/client'
@ -5,28 +6,31 @@ import { SessionTypes } from '@walletconnect/types'
import { useCallback, useEffect } from 'react' import { useCallback, useEffect } from 'react'
export default function useWalletConnectEventsManager(initialized: boolean) { export default function useWalletConnectEventsManager(initialized: boolean) {
// 1. Open session proposal modal for confirmation / rejection
const onSessionProposal = useCallback((proposal: SessionTypes.Proposal) => { const onSessionProposal = useCallback((proposal: SessionTypes.Proposal) => {
ModalStore.open('SessionProposalModal', { proposal }) ModalStore.open('SessionProposalModal', { proposal })
}, []) }, [])
const onSessionCreated = useCallback((created: SessionTypes.Created) => { // 2. Open session created modal to show success feedback
// TODO show successful connection feedback here const onSessionCreated = useCallback((created: SessionTypes.Created) => {}, [])
}, [])
const onSessionRequest = useCallback(async (request: SessionTypes.RequestEvent) => { // 3. Open rpc request handling modal based on method that was used
const requestSession = await walletConnectClient.session.get(request.topic) const onSessionRequest = useCallback(async (requestEvent: SessionTypes.RequestEvent) => {
ModalStore.open('SessionRequestModal', { request, requestSession }) const { topic, request } = requestEvent
const { method } = request
const requestSession = await walletConnectClient.session.get(topic)
if ([EIP155_SIGNING_METHODS.ETH_SIGN, EIP155_SIGNING_METHODS.PERSONAL_SIGN].includes(method)) {
ModalStore.open('SessionSignModal', { requestEvent, requestSession })
}
}, []) }, [])
useEffect(() => { useEffect(() => {
if (initialized) { if (initialized) {
// 1. Open session proposal modal for confirmation / rejection
walletConnectClient.on(CLIENT_EVENTS.session.proposal, onSessionProposal) walletConnectClient.on(CLIENT_EVENTS.session.proposal, onSessionProposal)
// 2. Open session created modal to show success feedback
walletConnectClient.on(CLIENT_EVENTS.session.created, onSessionCreated) walletConnectClient.on(CLIENT_EVENTS.session.created, onSessionCreated)
// 3. Open rpc request handling modal
walletConnectClient.on(CLIENT_EVENTS.session.request, onSessionRequest) walletConnectClient.on(CLIENT_EVENTS.session.request, onSessionRequest)
} }
}, [initialized, onSessionProposal, onSessionCreated, onSessionRequest]) }, [initialized, onSessionProposal, onSessionCreated, onSessionRequest])

View File

@ -2,11 +2,11 @@ import Layout from '@/components/Layout'
import Modal from '@/components/Modal' import Modal from '@/components/Modal'
import useInitialization from '@/hooks/useInitialization' import useInitialization from '@/hooks/useInitialization'
import useWalletConnectEventsManager from '@/hooks/useWalletConnectEventsManager' import useWalletConnectEventsManager from '@/hooks/useWalletConnectEventsManager'
import '@/styles/main.css'
import { darkTheme, lightTheme } from '@/utils/ThemeUtil' import { darkTheme, lightTheme } from '@/utils/ThemeUtil'
import { NextUIProvider } from '@nextui-org/react' import { NextUIProvider } from '@nextui-org/react'
import { ThemeProvider } from 'next-themes' import { ThemeProvider } from 'next-themes'
import { AppProps } from 'next/app' import { AppProps } from 'next/app'
import '../../public/main.css'
export default function App({ Component, pageProps }: AppProps) { export default function App({ Component, pageProps }: AppProps) {
// Step 1 - Initialize wallets and wallet connect client // Step 1 - Initialize wallets and wallet connect client

View File

@ -1,6 +1,6 @@
import AccountCard from '@/components/AccountCard' import AccountCard from '@/components/AccountCard'
import PageHeader from '@/components/PageHeader' import PageHeader from '@/components/PageHeader'
import { MAINNET_CHAINS } from '@/data/EIP155Data' import { EIP155_CHAINS } from '@/data/EIP155Data'
import { wallet } from '@/utils/WalletUtil' import { wallet } from '@/utils/WalletUtil'
import { Fragment } from 'react' import { Fragment } from 'react'
@ -8,7 +8,7 @@ export default function HomePage() {
return ( return (
<Fragment> <Fragment>
<PageHeader>Accounts</PageHeader> <PageHeader>Accounts</PageHeader>
{Object.values(MAINNET_CHAINS).map(({ name, logo, rgb }) => ( {Object.values(EIP155_CHAINS).map(({ name, logo, rgb }) => (
<AccountCard key={name} name={name} logo={logo} rgb={rgb} address={wallet.address} /> <AccountCard key={name} name={name} logo={logo} rgb={rgb} address={wallet.address} />
))} ))}
</Fragment> </Fragment>

View File

@ -0,0 +1,20 @@
import PageHeader from '@/components/PageHeader'
import { truncate } from '@/utils/HelperUtil'
import { walletConnectClient } from '@/utils/WalletConnectUtil'
import { useRouter } from 'next/router'
import { Fragment, useEffect } from 'react'
export default function SessionsPage() {
const { query } = useRouter()
const address = (query?.address as string) ?? 'Unknown'
useEffect(() => {
console.log(walletConnectClient.session.values)
}, [])
return (
<Fragment>
<PageHeader>{truncate(address, 15)}</PageHeader>
</Fragment>
)
}

View File

@ -7,13 +7,13 @@ import { proxy } from 'valtio'
interface ModalData { interface ModalData {
proposal?: SessionTypes.Proposal proposal?: SessionTypes.Proposal
created?: SessionTypes.Created created?: SessionTypes.Created
request?: SessionTypes.RequestEvent requestEvent?: SessionTypes.RequestEvent
requestSession?: SessionTypes.Settled requestSession?: SessionTypes.Settled
} }
interface State { interface State {
open: boolean open: boolean
view?: 'SessionProposalModal' | 'SessionRequestModal' view?: 'SessionProposalModal' | 'SessionSignModal'
data?: ModalData data?: ModalData
} }

View File

@ -0,0 +1,28 @@
import { EIP155_SIGNING_METHODS } from '@/data/EIP155Data'
import { formatJsonRpcError, formatJsonRpcResult } from '@json-rpc-tools/utils'
import { RequestEvent } from '@walletconnect/types'
import { ERROR } from '@walletconnect/utils'
import { Wallet } from 'ethers'
export async function approveEIP155Request(request: RequestEvent['request'], wallet: Wallet) {
const { method, params, id } = request
switch (method) {
case EIP155_SIGNING_METHODS.PERSONAL_SIGN:
const personalSignResult = await wallet.signMessage(params[0])
return formatJsonRpcResult(id, personalSignResult)
case EIP155_SIGNING_METHODS.ETH_SIGN:
const ethSignResult = await wallet.signMessage(params[1])
return formatJsonRpcResult(id, ethSignResult)
default:
throw new Error(ERROR.UNKNOWN_JSONRPC_METHOD.format().message)
}
}
export function rejectEIP155Request(request: RequestEvent['request']) {
const { id } = request
return formatJsonRpcError(id, ERROR.JSONRPC_REQUEST_METHOD_REJECTED.format().message)
}

View File

@ -1,4 +1,4 @@
import { MAINNET_CHAINS, TChain } from '@/data/EIP155Data' import { EIP155_CHAINS, TEIP155Chain } from '@/data/EIP155Data'
import ModalStore from '@/store/ModalStore' import ModalStore from '@/store/ModalStore'
import { walletConnectClient } from '@/utils/WalletConnectUtil' import { walletConnectClient } from '@/utils/WalletConnectUtil'
import { wallet } from '@/utils/WalletUtil' import { wallet } from '@/utils/WalletUtil'
@ -66,7 +66,9 @@ export default function SessionProposalModal() {
<Col> <Col>
<Text h5>Blockchains</Text> <Text h5>Blockchains</Text>
<Text color="$gray400"> <Text color="$gray400">
{chains.map(chain => MAINNET_CHAINS[chain as TChain]?.name ?? chain).join(', ')} {chains
.map(chain => EIP155_CHAINS[chain as TEIP155Chain]?.name ?? chain)
.join(', ')}
</Text> </Text>
</Col> </Col>
</Row> </Row>

View File

@ -1,37 +1,57 @@
import { MAINNET_CHAINS, TChain } from '@/data/EIP155Data' import { EIP155_CHAINS, EIP155_SIGNING_METHODS, TEIP155Chain } from '@/data/EIP155Data'
import ModalStore from '@/store/ModalStore' import ModalStore from '@/store/ModalStore'
import { getSignMessage } from '@/utils/HelperUtil' import { approveEIP155Request, rejectEIP155Request } from '@/utils/RequestHandlerUtil'
import { walletConnectClient } from '@/utils/WalletConnectUtil'
import { wallet } from '@/utils/WalletUtil' import { wallet } from '@/utils/WalletUtil'
import { Avatar, Button, Col, Container, Divider, Link, Modal, Row, Text } from '@nextui-org/react' import { Avatar, Button, Col, Container, Divider, Link, Modal, Row, Text } from '@nextui-org/react'
import { utils } from 'ethers'
import { Fragment } from 'react' import { Fragment } from 'react'
export default function SessionRequestModal() { export default function SessionSignModal() {
// Get request and wallet data from store // Get request and wallet data from store
const request = ModalStore.state.data?.request const requestEvent = ModalStore.state.data?.requestEvent
const requestSession = ModalStore.state.data?.requestSession const requestSession = ModalStore.state.data?.requestSession
// Ensure request and wallet are defined // Ensure request and wallet are defined
if (!request || !requestSession) { if (!requestEvent || !requestSession) {
return <Text>Missing request data</Text> return <Text>Missing request data</Text>
} }
// Get required request data // Get required request data
const { chainId } = request const { chainId } = requestEvent
const { method, params } = request.request const { method, params } = requestEvent.request
const { protocol } = requestSession.relay const { protocol } = requestSession.relay
const { name, icons, url } = requestSession.peer.metadata const { name, icons, url } = requestSession.peer.metadata
// Get message as utf string
let message = method === EIP155_SIGNING_METHODS.PERSONAL_SIGN ? params[0] : params[1]
if (utils.isHexString(message)) {
message = utils.toUtf8String(message)
}
// Handle approve action (logic varies based on request method) // Handle approve action (logic varies based on request method)
async function onApprove() { async function onApprove() {
// Handle sign requests if (requestEvent) {
if (['eth_sign', 'personal_sign'].includes(method)) { const response = await approveEIP155Request(requestEvent.request, wallet)
const message = getSignMessage(params, wallet.address) await walletConnectClient.respond({
const signedMessage = wallet.signMessage(message) topic: requestEvent.topic,
response
})
ModalStore.close()
} }
} }
// Handle reject action // Handle reject action
async function onReject() {} async function onReject() {
if (requestEvent) {
const response = rejectEIP155Request(requestEvent.request)
await walletConnectClient.respond({
topic: requestEvent.topic,
response
})
ModalStore.close()
}
}
return ( return (
<Fragment> <Fragment>
@ -56,7 +76,18 @@ export default function SessionRequestModal() {
<Row> <Row>
<Col> <Col>
<Text h5>Blockchain</Text> <Text h5>Blockchain</Text>
<Text color="$gray400">{MAINNET_CHAINS[chainId as TChain]?.name ?? chainId}</Text> <Text color="$gray400">
{EIP155_CHAINS[chainId as TEIP155Chain]?.name ?? chainId}
</Text>
</Col>
</Row>
<Divider y={2} />
<Row>
<Col>
<Text h5>Message</Text>
<Text color="$gray400">{message}</Text>
</Col> </Col>
</Row> </Row>

View File

@ -417,6 +417,21 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@json-rpc-tools/types@^1.7.6":
version "1.7.6"
resolved "https://registry.yarnpkg.com/@json-rpc-tools/types/-/types-1.7.6.tgz#5abd5fde01364a130c46093b501715bcce5bdc0e"
integrity sha512-nDSqmyRNEqEK9TZHtM15uNnDljczhCUdBmRhpNZ95bIPKEDQ+nTDmGMFd2lLin3upc5h2VVVd9tkTDdbXUhDIQ==
dependencies:
keyvaluestorage-interface "^1.0.0"
"@json-rpc-tools/utils@1.7.6":
version "1.7.6"
resolved "https://registry.yarnpkg.com/@json-rpc-tools/utils/-/utils-1.7.6.tgz#67f04987dbaa2e7adb6adff1575367b75a9a9ba1"
integrity sha512-HjA8x/U/Q78HRRe19yh8HVKoZ+Iaoo3YZjakJYxR+rw52NHo6jM+VE9b8+7ygkCFXl/EHID5wh/MkXaE/jGyYw==
dependencies:
"@json-rpc-tools/types" "^1.7.6"
"@pedrouid/environment" "^1.0.1"
"@next/env@12.0.10": "@next/env@12.0.10":
version "12.0.10" version "12.0.10"
resolved "https://registry.yarnpkg.com/@next/env/-/env-12.0.10.tgz#561640fd62279218ccd2798ae907bae8d94a7730" resolved "https://registry.yarnpkg.com/@next/env/-/env-12.0.10.tgz#561640fd62279218ccd2798ae907bae8d94a7730"
@ -517,6 +532,11 @@
"@nodelib/fs.scandir" "2.1.5" "@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0" fastq "^1.6.0"
"@pedrouid/environment@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@pedrouid/environment/-/environment-1.0.1.tgz#858f0f8a057340e0b250398b75ead77d6f4342ec"
integrity sha512-HaW78NszGzRZd9SeoI3JD11JqY+lubnaOx7Pewj5pfjqWXOEATpeKIFb9Z4t2WBUK2iryiXX3lzWwmYWgUL0Ug==
"@react-aria/focus@3.5.0": "@react-aria/focus@3.5.0":
version "3.5.0" version "3.5.0"
resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.5.0.tgz#02b85f97d6114af1eccc0902ce40723b626cb7f9" resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.5.0.tgz#02b85f97d6114af1eccc0902ce40723b626cb7f9"