Parse and sign typed data

This commit is contained in:
Ilja 2022-02-15 14:41:46 +02:00
parent a9280ef124
commit 6d5c679ca7
11 changed files with 201 additions and 20 deletions

View File

@ -1,6 +1,7 @@
import ModalStore from '@/store/ModalStore' import ModalStore from '@/store/ModalStore'
import SessionProposalModal from '@/views/SessionProposalModal' import SessionProposalModal from '@/views/SessionProposalModal'
import SessionRequestModal from '@/views/SessionSignModal' import SessionRequestModal from '@/views/SessionSignModal'
import SessionSignTypedDataModal from '@/views/SessionSignTypedDataModal'
import { Modal as NextModal } from '@nextui-org/react' import { Modal as NextModal } from '@nextui-org/react'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
@ -11,6 +12,7 @@ export default function Modal() {
<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 === 'SessionSignModal' && <SessionRequestModal />} {view === 'SessionSignModal' && <SessionRequestModal />}
{view === 'SessionSignTypedDataModal' && <SessionSignTypedDataModal />}
</NextModal> </NextModal>
) )
} }

View File

@ -16,7 +16,7 @@ export type TEIP155Chain = keyof typeof EIP155_CHAINS
/** /**
* Chains * Chains
*/ */
export const EIP155_CHAINS = { export const EIP155_MAINNET_CHAINS = {
'eip155:1': { 'eip155:1': {
chainId: 1, chainId: 1,
name: 'Ethereum', name: 'Ethereum',
@ -70,6 +70,8 @@ export const EIP155_TEST_CHAINS = {
} }
} }
export const EIP155_CHAINS = { ...EIP155_MAINNET_CHAINS, ...EIP155_TEST_CHAINS }
/** /**
* Methods * Methods
*/ */
@ -78,6 +80,7 @@ export const EIP155_SIGNING_METHODS = {
ETH_SIGN: 'eth_sign', ETH_SIGN: 'eth_sign',
ETH_SIGN_TRANSACTION: 'eth_signTransaction', ETH_SIGN_TRANSACTION: 'eth_signTransaction',
ETH_SIGN_TYPED_DATA: 'eth_signTypedData', ETH_SIGN_TYPED_DATA: 'eth_signTypedData',
ETH_SIGN_TYPED_DATA_V3: 'eth_signTypedData_v3',
ETH_SIGN_TYPED_DATA_V4: 'eth_signTypedData_v4', ETH_SIGN_TYPED_DATA_V4: 'eth_signTypedData_v4',
ETH_SIGN_RAW_TRANSACTION: 'eth_sendRawTransaction', ETH_SIGN_RAW_TRANSACTION: 'eth_sendRawTransaction',
ETH_SEND_TRANSACTION: 'eth_sendTransaction' ETH_SEND_TRANSACTION: 'eth_sendTransaction'

View File

@ -20,9 +20,21 @@ export default function useWalletConnectEventsManager(initialized: boolean) {
const { method } = request const { method } = request
const requestSession = await walletConnectClient.session.get(topic) const requestSession = await walletConnectClient.session.get(topic)
// Hanle message signing requests of various formats
if ([EIP155_SIGNING_METHODS.ETH_SIGN, EIP155_SIGNING_METHODS.PERSONAL_SIGN].includes(method)) { if ([EIP155_SIGNING_METHODS.ETH_SIGN, EIP155_SIGNING_METHODS.PERSONAL_SIGN].includes(method)) {
ModalStore.open('SessionSignModal', { requestEvent, requestSession }) ModalStore.open('SessionSignModal', { requestEvent, requestSession })
} }
// Hanle data signing requests of various formats
if (
[
EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA,
EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA_V3,
EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA_V4
].includes(method)
) {
ModalStore.open('SessionSignTypedDataModal', { requestEvent, requestSession })
}
}, []) }, [])
useEffect(() => { useEffect(() => {

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 { EIP155_CHAINS, EIP155_TEST_CHAINS } from '@/data/EIP155Data' import { EIP155_MAINNET_CHAINS, EIP155_TEST_CHAINS } from '@/data/EIP155Data'
import SettingsStore from '@/store/SettingsStore' import SettingsStore from '@/store/SettingsStore'
import { wallet } from '@/utils/WalletUtil' import { wallet } from '@/utils/WalletUtil'
import { Text } from '@nextui-org/react' import { Text } from '@nextui-org/react'
@ -16,7 +16,7 @@ export default function HomePage() {
<Text h4 css={{ marginBottom: '$5' }}> <Text h4 css={{ marginBottom: '$5' }}>
Mainnets Mainnets
</Text> </Text>
{Object.values(EIP155_CHAINS).map(({ name, logo, rgb }) => ( {Object.values(EIP155_MAINNET_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} />
))} ))}

View File

@ -13,7 +13,7 @@ interface ModalData {
interface State { interface State {
open: boolean open: boolean
view?: 'SessionProposalModal' | 'SessionSignModal' view?: 'SessionProposalModal' | 'SessionSignModal' | 'SessionSignTypedDataModal'
data?: ModalData data?: ModalData
} }

View File

@ -11,7 +11,7 @@ interface State {
* State * State
*/ */
const state = proxy<State>({ const state = proxy<State>({
testNets: false testNets: Boolean(localStorage.getItem('TEST_NETS')) ?? false
}) })
/** /**
@ -22,6 +22,11 @@ const SettingsStore = {
toggleTestNets() { toggleTestNets() {
state.testNets = !state.testNets state.testNets = !state.testNets
if (state.testNets) {
localStorage.setItem('TEST_NETS', 'YES')
} else {
localStorage.removeItem('TEST_NETS')
}
} }
} }

View File

@ -26,3 +26,29 @@ export function convertHexToUtf8(value: string) {
return value return value
} }
/**
* Gets message from various signing request methods by filtering out
* a value that is not an address (thus is a message).
* If it is a hex string, it gets converted to utf8 string
*/
export function getSignParamsMessage(params: string[]) {
const message = params.filter(p => !utils.isAddress(p))[0]
return convertHexToUtf8(message)
}
/**
* Gets data from various signTypedData request methods by filtering out
* a value that is not an address (thus is data).
* If data is a string convert it to object
*/
export function getSignTypedDataParamsData(params: string[]) {
const data = params.filter(p => !utils.isAddress(p))[0]
if (typeof data === 'string') {
return JSON.parse(data)
}
return data
}

View File

@ -1,5 +1,5 @@
import { EIP155_SIGNING_METHODS } from '@/data/EIP155Data' import { EIP155_SIGNING_METHODS } from '@/data/EIP155Data'
import { convertHexToUtf8 } from '@/utils/HelperUtil' import { getSignParamsMessage, getSignTypedDataParamsData } from '@/utils/HelperUtil'
import { formatJsonRpcError, formatJsonRpcResult } from '@json-rpc-tools/utils' import { formatJsonRpcError, formatJsonRpcResult } from '@json-rpc-tools/utils'
import { RequestEvent } from '@walletconnect/types' import { RequestEvent } from '@walletconnect/types'
import { ERROR } from '@walletconnect/utils' import { ERROR } from '@walletconnect/utils'
@ -9,13 +9,28 @@ export async function approveEIP155Request(request: RequestEvent['request'], wal
const { method, params, id } = request const { method, params, id } = request
switch (method) { switch (method) {
/**
* Handle message signing requests
*/
case EIP155_SIGNING_METHODS.PERSONAL_SIGN: case EIP155_SIGNING_METHODS.PERSONAL_SIGN:
const personalSignResult = await wallet.signMessage(convertHexToUtf8(params[0]))
return formatJsonRpcResult(id, personalSignResult)
case EIP155_SIGNING_METHODS.ETH_SIGN: case EIP155_SIGNING_METHODS.ETH_SIGN:
const ethSignResult = await wallet.signMessage(convertHexToUtf8(params[1])) const message = getSignParamsMessage(params)
return formatJsonRpcResult(id, ethSignResult) const signedMessage = await wallet.signMessage(message)
return formatJsonRpcResult(id, signedMessage)
/**
* Handle data signing requests
*/
case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA:
case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA_V3:
case EIP155_SIGNING_METHODS.ETH_SIGN_TYPED_DATA_V4:
const { domain, types, message: data } = getSignTypedDataParamsData(params)
// https://github.com/ethers-io/ethers.js/issues/687#issuecomment-714069471
delete types.EIP712Domain
const signedData = await wallet._signTypedData(domain, types, data)
return formatJsonRpcResult(id, signedData)
default: default:
throw new Error(ERROR.UNKNOWN_JSONRPC_METHOD.format().message) throw new Error(ERROR.UNKNOWN_JSONRPC_METHOD.format().message)

View File

@ -1,17 +1,15 @@
import { Wallet } from 'ethers' import { Wallet } from 'ethers'
const STORAGE_KEY = 'WALLET_MNEMONIC'
export let wallet: Wallet export let wallet: Wallet
export function createOrRestoreWallet() { export function createOrRestoreWallet() {
const mnemonic = localStorage.getItem(STORAGE_KEY) const mnemonic = localStorage.getItem('WALLET_MNEMONIC')
if (mnemonic) { if (mnemonic) {
wallet = Wallet.fromMnemonic(mnemonic) wallet = Wallet.fromMnemonic(mnemonic)
} else { } else {
wallet = Wallet.createRandom() wallet = Wallet.createRandom()
// Don't store mnemonic in local storage in a production project! // Don't store mnemonic in local storage in a production project!
localStorage.setItem(STORAGE_KEY, wallet.mnemonic.phrase) localStorage.setItem('WALLET_MNEMONIC', wallet.mnemonic.phrase)
} }
} }

View File

@ -1,6 +1,6 @@
import { EIP155_CHAINS, EIP155_SIGNING_METHODS, TEIP155Chain } from '@/data/EIP155Data' import { EIP155_CHAINS, TEIP155Chain } from '@/data/EIP155Data'
import ModalStore from '@/store/ModalStore' import ModalStore from '@/store/ModalStore'
import { convertHexToUtf8 } from '@/utils/HelperUtil' import { getSignParamsMessage } from '@/utils/HelperUtil'
import { approveEIP155Request, rejectEIP155Request } from '@/utils/RequestHandlerUtil' import { approveEIP155Request, rejectEIP155Request } from '@/utils/RequestHandlerUtil'
import { walletConnectClient } from '@/utils/WalletConnectUtil' import { walletConnectClient } from '@/utils/WalletConnectUtil'
import { wallet } from '@/utils/WalletUtil' import { wallet } from '@/utils/WalletUtil'
@ -24,8 +24,7 @@ export default function SessionSignModal() {
const { name, icons, url } = requestSession.peer.metadata const { name, icons, url } = requestSession.peer.metadata
// Get message, convert it to UTF8 string if it is valid hex // Get message, convert it to UTF8 string if it is valid hex
let message = method === EIP155_SIGNING_METHODS.PERSONAL_SIGN ? params[0] : params[1] const message = getSignParamsMessage(params)
message = convertHexToUtf8(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() {
@ -54,7 +53,7 @@ export default function SessionSignModal() {
return ( return (
<Fragment> <Fragment>
<Modal.Header> <Modal.Header>
<Text h3>Request</Text> <Text h3>Sign Message</Text>
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>

View File

@ -0,0 +1,121 @@
import { EIP155_CHAINS, TEIP155Chain } from '@/data/EIP155Data'
import ModalStore from '@/store/ModalStore'
import { getSignTypedDataParamsData } from '@/utils/HelperUtil'
import { approveEIP155Request, rejectEIP155Request } from '@/utils/RequestHandlerUtil'
import { walletConnectClient } from '@/utils/WalletConnectUtil'
import { wallet } from '@/utils/WalletUtil'
import { Avatar, Button, Col, Container, Divider, Link, Modal, Row, Text } from '@nextui-org/react'
import { Fragment } from 'react'
export default function SessionSignTypedDataModal() {
// Get request and wallet data from store
const requestEvent = ModalStore.state.data?.requestEvent
const requestSession = ModalStore.state.data?.requestSession
// Ensure request and wallet are defined
if (!requestEvent || !requestSession) {
return <Text>Missing request data</Text>
}
// Get required request data
const { chainId } = requestEvent
const { method, params } = requestEvent.request
const { protocol } = requestSession.relay
const { name, icons, url } = requestSession.peer.metadata
// Get data
const data = getSignTypedDataParamsData(params)
// Handle approve action (logic varies based on request method)
async function onApprove() {
if (requestEvent) {
const response = await approveEIP155Request(requestEvent.request, wallet)
await walletConnectClient.respond({
topic: requestEvent.topic,
response
})
ModalStore.close()
}
}
// Handle reject action
async function onReject() {
if (requestEvent) {
const response = rejectEIP155Request(requestEvent.request)
await walletConnectClient.respond({
topic: requestEvent.topic,
response
})
ModalStore.close()
}
}
return (
<Fragment>
<Modal.Header>
<Text h3>Sign Typed Data</Text>
</Modal.Header>
<Modal.Body>
<Container css={{ padding: 0 }}>
<Row align="center">
<Col span={3}>
<Avatar src={icons[0]} />
</Col>
<Col span={14}>
<Text h5>{name}</Text>
<Link href={url}>{url}</Link>
</Col>
</Row>
<Divider y={2} />
<Row>
<Col>
<Text h5>Blockchain</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">{JSON.stringify(data)}</Text>
</Col>
</Row>
<Divider y={2} />
<Row>
<Col>
<Text h5>Method</Text>
<Text color="$gray400">{method}</Text>
</Col>
</Row>
<Divider y={2} />
<Row>
<Col>
<Text h5>Relay Protocol</Text>
<Text color="$gray400">{protocol}</Text>
</Col>
</Row>
</Container>
</Modal.Body>
<Modal.Footer>
<Button auto flat color="error" onClick={onReject}>
Reject
</Button>
<Button auto flat color="success" onClick={onApprove}>
Approve
</Button>
</Modal.Footer>
</Fragment>
)
}