Skip to content

Commit

Permalink
feat: impl eip-712 (#11607)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jack-Works committed May 11, 2024
1 parent 53cf086 commit b40200a
Show file tree
Hide file tree
Showing 8 changed files with 287 additions and 19 deletions.
22 changes: 21 additions & 1 deletion packages/mask/entry-sdk/bridge/eth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,21 @@ const methods: Methods = {
if (wallets.length) return wallets
return err.user_rejected_the_request()
},
async eth_signTypedData_v4(requestedAddress, typedData) {
await Services.Wallet.requestUnlockWallet()
const wallets = await Services.Wallet.sdk_getGrantedWallets(location.origin)
if (!wallets.some((addr) => isSameAddress(addr, requestedAddress)))
return err.the_requested_account_and_or_method_has_not_been_authorized_by_the_user()
return providers.EVMWeb3.getWeb3Provider({
providerType: ProviderType.MaskWallet,
account: requestedAddress,
silent: false,
readonly: false,
}).request({
method: EthereumMethodType.ETH_SIGN_TYPED_DATA,
params: [requestedAddress, typedData],
})
},
async personal_sign(challenge, requestedAddress) {
// check challenge is 0x hex
await Services.Wallet.requestUnlockWallet()
Expand Down Expand Up @@ -308,7 +323,12 @@ export async function eth_request(request: unknown): Promise<{ e?: MaskEthereumP
paramsArr.length = paramsSchema.items.length
}
const paramsValidated = paramsSchema.safeParse(paramsArr)
if (!paramsValidated.success) return { e: fromZodError(paramsValidated.error) }
if (!paramsValidated.success) {
if (process.env.NODE_ENV === 'development') {
console.debug('[Mask Wallet] Failed', request, 'received params', paramsArr)
}
return { e: fromZodError(paramsValidated.error) }
}

if (process.env.NODE_ENV === 'development') {
console.debug('[Mask Wallet]', request, paramsValidated.data)
Expand Down
47 changes: 35 additions & 12 deletions packages/mask/entry-sdk/bridge/eth/validator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { MaskEthereumProviderRpcError, fromMessage, ErrorCode } from '@masknet/sdk'
import type { Result } from 'ts-results-es'
import z, { type ZodError } from 'zod'
import { getMessage } from 'eip-712'

export const requestSchema = z.object({
method: z.string().nonempty(),
Expand Down Expand Up @@ -197,18 +198,40 @@ export const methodValidate = {
eth_signTypedData_v4: {
args: z.tuple([
_.address,
z
.object({
types: z
.object({
EIP712Domain: z.array(z.unknown()),
})
.describe('types'),
domain: z.object({}).passthrough(),
primaryType: z.string(),
message: z.object({}).passthrough(),
})
.describe('TypedData'),
z.preprocess(
(arg) => (typeof arg === 'string' ? JSON.parse(arg) : arg),
z
.object({
types: z
.object({
EIP712Domain: z.array(z.unknown()),
})
.catchall(
z.array(
z.object({
type: z.string(),
name: z.string(),
}),
),
)
.describe('types'),
domain: z.object({}).passthrough(),
primaryType: z.string(),
message: z.object({}).passthrough(),
})
.refine(
(val) => {
try {
getMessage(val as any)
return true
} catch {
return false
}
},
{ message: 'Typed data does not match JSON schema' },
)
.describe('TypedData'),
),
]),
return: _.hex,
},
Expand Down
1 change: 1 addition & 0 deletions packages/mask/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
"color": "^4.2.3",
"crypto-browserify": "^3.12.0",
"date-fns": "^2.30.0",
"eip-712": "^1.0.0",
"elliptic": "^6.5.4",
"event-iterator": "^2.0.0",
"file-loader": "^6.2.0",
Expand Down
182 changes: 182 additions & 0 deletions packages/mask/popups/components/SignRequestInfo/eip712.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { CopyButton, FormattedAddress } from '@masknet/shared'
import { makeStyles } from '@masknet/theme'
import { useWeb3Utils } from '@masknet/web3-hooks-base'
import { ChainId, formatEthereumAddress } from '@masknet/web3-shared-evm'
import { Alert, Link, Tooltip, Typography } from '@mui/material'
import { type ReactNode } from 'react'
import { z } from 'zod'

const useStyles = makeStyles()({
box: {
margin: 8,
border: '1px solid #ccc',
borderRadius: 4,
wordBreak: 'break-word',
},
object: {
paddingInline: 0,
marginInline: 16,
listStyle: 'none',
textTransform: 'capitalize',
},
fieldName: { fontWeight: 'bold' },
value: {},
list: {
paddingInline: 0,
marginInline: 32,
'& > li > ul': {
marginInline: 0,
},
},
error: { display: 'inline-flex' },
})

export function RenderEIP712({ data, messageTitle, title }: { data: Data; title: ReactNode; messageTitle: ReactNode }) {
const { classes } = useStyles()
const isDomainValid = def.safeParse(data.types.EIP712Domain)
const utils = useWeb3Utils()
return (
<>
{title}
<div className={classes.box}>
{isDomainValid.success ? renderField(data.domain, 'EIP712Domain', data.types) : null}
</div>
{messageTitle}
<div className={classes.box}>{renderField(data.message, data.primaryType, data.types)}</div>
</>
)

function renderField(fieldData: unknown, fieldType: string, schema: Record<string, Item>): ReactNode {
switch (fieldType) {
case 'bool':
return (
<Tooltip title={fieldType}>
{typeof fieldData !== 'boolean' ?
<Alert className={classes.error} severity="error">
Not a {fieldType}.
</Alert>
: <Typography component="span" className={classes.value}>
{String(fieldData)}
</Typography>
}
</Tooltip>
)
case 'bytes':
case 'bytes1':
case 'bytes32':
case 'uint8':
case 'uint256':
case 'int8':
case 'int256':
return (
<Tooltip title={fieldType}>
{typeof fieldData !== 'string' && typeof fieldData !== 'number' ?
<Alert className={classes.error} severity="error">
Not a {fieldType}.
</Alert>
: <Typography component="span" className={classes.value}>
{fieldData}
</Typography>
}
</Tooltip>
)
case 'string':
return (
<Tooltip title={fieldType}>
{typeof fieldData !== 'string' ?
<Alert className={classes.error} severity="error">
Not a {fieldType}.
</Alert>
: <Typography component="span" className={classes.value}>
{fieldData}
</Typography>
}
</Tooltip>
)
case 'address':
return typeof fieldData !== 'string' ?
<Alert className={classes.error} severity="error">
Not a {fieldType}.
</Alert>
: <Tooltip title={String(fieldData)}>
<Link
className={classes.value}
href={utils.explorerResolver.addressLink(ChainId.Mainnet, fieldData)}
target="_blank"
rel="noopener noreferrer">
<FormattedAddress address={fieldData} size={6} formatter={formatEthereumAddress} />
<CopyButton size={14} text={fieldData} />
</Link>
</Tooltip>
}
if (fieldType.match(/\[(\d+)?]$/)) {
const type = fieldType.replace(/\[(\d+)?]$/, '')
const data = Array.isArray(fieldData) ? fieldData : []
return (
<ol className={classes.list}>
{data.map((field, index) => (
// eslint-disable-next-line react/jsx-key
<li>{renderField(field, type, schema)}</li>
))}
</ol>
)
} else {
const define = schema[fieldType]
if (!define)
return (
<Alert className={classes.error} severity="error">
This request is missing the definition of {fieldType}
</Alert>
)
if (!(typeof fieldData === 'object' && fieldData !== null))
return (
<Alert className={classes.error} severity="error">
Field is not an object.
</Alert>
)
return (
<ul className={classes.object}>
{define.map((field) => (
// eslint-disable-next-line react/jsx-key
<li>
<Tooltip title={field.type}>
<span>
<Typography className={classes.fieldName} component="span">
{field.name}
</Typography>
</span>
</Tooltip>
:{' '}
{field.name in fieldData ?
renderField((fieldData as any)[field.name], field.type, schema)
: <Alert className={classes.error} severity="error">
?
</Alert>
}
</li>
))}
</ul>
)
}
}
}

const def = z
.object({
type: z.string(),
name: z.string(),
})
.array()
type Item = z.infer<typeof def>
const data = z.object({
types: z
.object({
EIP712Domain: z.array(z.unknown()),
})
.catchall(def)
.describe('types'),
domain: z.object({}).passthrough(),
primaryType: z.string(),
message: z.object({}).passthrough(),
})
type Data = z.infer<typeof data>
24 changes: 19 additions & 5 deletions packages/mask/popups/components/SignRequestInfo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { isValidAddress } from '@masknet/web3-shared-evm'
import { TypedMessageTextRender } from '../../../../typed-message/react/src/Renderer/Core/Text.js'
import { Alert } from '@masknet/shared'
import { RenderFragmentsContext, type RenderFragmentsContextType } from '@masknet/typed-message-react'
import { RenderEIP712 } from './eip712.js'

const useStyles = makeStyles()((theme) => ({
container: {
Expand Down Expand Up @@ -58,6 +59,7 @@ export const SignRequestInfo = memo<SignRequestInfoProps>(({ message, rawMessage
const { classes, cx } = useStyles()

const isEIP4361 = typeof message === 'object' && message.type === 'eip4361'
const isEIP712 = typeof rawMessage === 'object'

let EIP4361Message
let TextMessage
Expand Down Expand Up @@ -104,21 +106,33 @@ export const SignRequestInfo = memo<SignRequestInfoProps>(({ message, rawMessage
</Box>
: null}
{EIP4361Message}
<Typography className={classes.messageTitle}>{t.popups_wallet_sign_message()}</Typography>
<Typography className={classes.sourceText} component={isEIP4361 ? 'details' : 'p'}>
{TextMessage}
</Typography>
{typeof TextMessage === 'string' ?
<>
<Typography className={classes.messageTitle}>{t.popups_wallet_sign_message()}</Typography>
<Typography className={classes.sourceText} component={isEIP4361 ? 'details' : 'p'}>
{TextMessage}
</Typography>
</>
: undefined}
{isEIP712 ?
<RenderEIP712
data={rawMessage as any}
title={<Typography className={classes.messageTitle}>Typed data</Typography>}
messageTitle={<Typography className={classes.messageTitle}>Message</Typography>}
/>
: undefined}
{rawMessage && message !== rawMessage ?
<>
<Typography className={classes.messageTitle}>{t.popups_wallet_sign_raw_message()}</Typography>
<Typography className={classes.sourceText} component={isEIP4361 ? 'details' : 'p'}>
<Typography className={classes.sourceText} component={isEIP4361 || isEIP712 ? 'details' : 'p'}>
{typeof rawMessage === 'string' ? rawMessage : JSON.stringify(rawMessage, null, 2)}
</Typography>
</>
: undefined}
</main>
)
})
SignRequestInfo.displayName = 'SignRequestInfo'

interface EIP4361RenderProps {
message: ParsedEIP4361Message
Expand Down
4 changes: 4 additions & 0 deletions packages/mask/popups/pages/Wallet/Interaction/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,10 @@ const Interaction = memo((props: InteractionProps) => {
)
}

if (currentRequest.request.arguments.method === EthereumMethodType.ETH_SIGN_TYPED_DATA) {
if (typeof params[1] === 'object') params[1] = JSON.stringify(params[1])
}

if (currentRequest.request.arguments.method === EthereumMethodType.ETH_SEND_TRANSACTION) {
if (params[0].type === '0x0') {
delete params[0].type
Expand Down
2 changes: 1 addition & 1 deletion packages/mask/shared-ui/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -954,7 +954,7 @@
"popups_wallet_request_source": "Request Source",
"popups_wallet_request_source_insecure": "Your connection to this site is not encrypted which can be modified by a hostile third party, we strongly suggest you reject this request.",
"popups_wallet_sign_message": "Signing Messsage (Text)",
"popups_wallet_sign_raw_message": "Signing Message (Hex)",
"popups_wallet_sign_raw_message": "Signing Message (Raw)",
"popups_wallet_sign_in_message": "Sign-in Request",
"popups_wallet_sign_in_message_invalid_eip4361": "This message contains a invalid EIP-4361 message. It is better to reject this request.",
"popups_wallet_sign_in_message_domain": "Domain",
Expand Down

0 comments on commit b40200a

Please sign in to comment.