Skip to content

Commit

Permalink
feat: impl eip-4361 in mask sdk (#11378)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jack-Works committed Feb 20, 2024
1 parent b47d2c1 commit 6ab8e0e
Show file tree
Hide file tree
Showing 17 changed files with 1,210 additions and 358 deletions.
3 changes: 0 additions & 3 deletions cspell.json
Expand Up @@ -19,19 +19,16 @@
],
"words": [
"aave",
"activityv",
"adai",
"addrs",
"aeth",
"algorand",
"amountstate",
"ampl",
"ampleforth",
"anchorme",
"apikey",
"arbitrum",
"areth",
"arrayify",
"artblocks",
"arthswap",
"arweave",
Expand Down
2 changes: 1 addition & 1 deletion packages/mask/content-script/index.ts
Expand Up @@ -6,8 +6,8 @@ if (!Reflect.get(globalThis, loaded)) {
Reflect.set(globalThis, loaded, true)

const { matchesAnySiteAdaptor } = await import(/* webpackMode: 'eager' */ '../shared/site-adaptors/definitions.js')
await import(/* webpackMode: 'eager' */ '../shared-ui/initialization/index.js')
if (matchesAnySiteAdaptor(location.href)) {
await import('../shared-ui/initialization/index.js')
await import('./site-adaptors/index.js')
const { activateSiteAdaptorUI } = await import('./site-adaptor-infra/define.js')
await activateSiteAdaptorUI()
Expand Down
283 changes: 258 additions & 25 deletions packages/mask/popups/components/SignRequestInfo/index.tsx
@@ -1,7 +1,13 @@
import { makeStyles } from '@masknet/theme'
import { memo } from 'react'
import { Fragment, memo, useEffect } from 'react'
import { useMaskSharedTrans } from '../../../shared-ui/index.js'
import { Box, Typography } from '@mui/material'
import { Box, Link, Typography } from '@mui/material'
import { isSameAddress, type EIP4361Message, type ParsedEIP4361Message } from '@masknet/web3-shared-base'
import { useInteractionWalletContext } from '../../pages/Wallet/Interaction/InteractionContext.js'
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'

const useStyles = makeStyles()((theme) => ({
container: {
Expand All @@ -27,55 +33,282 @@ const useStyles = makeStyles()((theme) => ({
fontSize: 12,
fontWeight: 700,
color: theme.palette.maskColor.second,
wordBreak: 'break-all',
wordBreak: 'break-word',
whiteSpace: 'pre-wrap',
},
messageTitle: {
fontSize: 14,
fontWeight: 700,
marginTop: theme.spacing(3),
},
dangerField: {
color: theme.palette.error.main,
},
}))

interface SignRequestInfoProps {
message: string
source?: string | null
message: string | EIP4361Message | undefined
rawMessage: string | object | undefined
origin: string | undefined
}

export const SignRequestInfo = memo<SignRequestInfoProps>(function SignRequestInfo({ message, source }) {
// TODO: render typed sign
export const SignRequestInfo = memo<SignRequestInfoProps>(({ message, rawMessage, origin }) => {
const t = useMaskSharedTrans()
const { classes } = useStyles()
const { classes, cx } = useStyles()

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

let EIP4361Message
let TextMessage
if (isEIP4361) {
TextMessage = message.message
if (message.parsed) {
if (!origin) throw new Error('EIP4361 message must have an origin')
EIP4361Message = (
<EIP4361Render invalidFields={message.invalidFields} message={message.parsed} messageOrigin={origin} />
)
} else {
EIP4361Message = (
<Alert className={classes.dangerField} open>
{t.popups_wallet_sign_in_message_invalid_eip4361()}
</Alert>
)
}
}
if (!TextMessage && typeof message === 'string') TextMessage = message
if (!TextMessage && typeof rawMessage === 'string') TextMessage = rawMessage

return (
<main className={classes.container}>
<Typography className={classes.title}>{t.popups_wallet_signature_request_title()}</Typography>
{source ?
<Typography className={classes.title}>
{isEIP4361 ? t.popups_wallet_sign_in_message() : t.popups_wallet_signature_request_title()}
</Typography>
{origin ?
<Box className={classes.source}>
<Typography fontSize={16} fontWeight={700}>
{t.popups_wallet_request_source()}
</Typography>
<Typography className={classes.sourceText}>{source}</Typography>
<Typography
className={cx(
classes.sourceText,
origin.startsWith('https://') ? undefined : classes.dangerField,
)}>
{origin}
</Typography>
{!origin.startsWith('https://') ?
<Alert className={classes.dangerField} open>
{t.popups_wallet_request_source_insecure()}
</Alert>
: null}
</Box>
: null}
{EIP4361Message}
<Typography className={classes.messageTitle}>{t.popups_wallet_sign_message()}</Typography>
<MessageDisplay message={message} />
<Typography className={classes.sourceText} component={isEIP4361 ? 'details' : 'p'}>
{TextMessage}
</Typography>
{rawMessage && message !== rawMessage ?
<>
<Typography className={classes.messageTitle}>{t.popups_wallet_sign_raw_message()}</Typography>
<Typography className={classes.sourceText} component={isEIP4361 ? 'details' : 'p'}>
{typeof rawMessage === 'string' ? rawMessage : JSON.stringify(rawMessage, null, 2)}
</Typography>
</>
: undefined}
</main>
)
})

function MessageDisplay({ message }: { message: string }) {
const t = useMaskSharedTrans()
const { classes } = useStyles()
if (message.startsWith('0x')) {
const string = new TextDecoder().decode(
new Uint8Array([...message.slice(2).matchAll(/([\da-f]{2})/gi)].map((i) => Number.parseInt(i[0], 16))),
)
interface EIP4361RenderProps {
message: ParsedEIP4361Message
invalidFields: Array<keyof ParsedEIP4361Message>
messageOrigin: string
}

const TextFragmentRender: RenderFragmentsContextType = {
Link: (props) => {
return (
<>
<Typography className={classes.sourceText}>{string}</Typography>
<Typography className={classes.messageTitle}>{t.popups_wallet_sign_raw_message()}</Typography>
<Typography className={classes.sourceText}>{message}</Typography>
</>
<Link href={props.href} style={props.style} target="_blank">
{props.children}
</Link>
)
}
return <Typography className={classes.sourceText}>{message}</Typography>
},
}

function EIP4361Render({ message, messageOrigin, invalidFields }: EIP4361RenderProps) {
const {
address,
chainId,
domain,
expiration_time,
nonce,
issued_at,
not_before,
request_id,
resources,
statement,
uri,
version,
} = message
const t = useMaskSharedTrans()
const { classes, cx } = useStyles()

const { interactionWallet, setInteractionWallet } = useInteractionWalletContext()
useEffect(() => {
if (!isValidAddress(address)) return
if (isSameAddress(address, interactionWallet)) return
setInteractionWallet(address)
}, [interactionWallet, address])

// TODO: show warning for non https request
const invalidDomain = invalidFields.includes('domain')
const invalidVersion = invalidFields.includes('version')
// TODO: invalid chainID
const invalidChainId = invalidFields.includes('chainId')
const invalidNotBefore = invalidFields.includes('not_before')
const invalidExpirationTime = invalidFields.includes('expiration_time')

const classesDangerTitle = cx(classes.messageTitle, classes.dangerField)
const classesDangerField = cx([classes.sourceText, classes.dangerField])

const normalFields = [
<Fragment key="statement">
<Typography className={classes.messageTitle}>{t.popups_wallet_signature_request_message()}</Typography>
<Typography className={classes.sourceText}>
{statement ?
<RenderFragmentsContext.Provider value={TextFragmentRender}>
<TypedMessageTextRender content={statement} serializable type="text" version={1} />
</RenderFragmentsContext.Provider>
: null}
</Typography>
</Fragment>,
<Fragment key="uri">
<Typography className={classes.messageTitle}>URI</Typography>
<Typography className={classes.sourceText}>
<RenderFragmentsContext.Provider value={TextFragmentRender}>
<TypedMessageTextRender content={uri} serializable type="text" version={1} />
</RenderFragmentsContext.Provider>
</Typography>
</Fragment>,
resources?.length ?
<Fragment key="resources">
<Typography className={classes.messageTitle}>{t.popups_wallet_sign_in_message_resource()}</Typography>
<Typography className={classes.sourceText}>
<RenderFragmentsContext.Provider value={TextFragmentRender}>
<TypedMessageTextRender content={resources.join('\n')} serializable type="text" version={1} />
</RenderFragmentsContext.Provider>
</Typography>
</Fragment>
: null,
<Fragment key="issuedAt">
<Typography className={classes.messageTitle}>{t.popups_wallet_sign_in_message_issued_at()}</Typography>
<Typography className={classes.sourceText}>{issued_at.toLocaleString()}</Typography>
</Fragment>,
]
const dangerFields = [
invalidVersion ?
<Fragment key="version">
<Typography className={classesDangerTitle}>{t.popups_wallet_sign_in_message_version()}</Typography>
<Typography className={classesDangerField}>{version}</Typography>
<Alert className={classes.dangerField} open>
{t.popups_wallet_sign_in_message_version_invalid()}
</Alert>
</Fragment>
: null,
]

const domainJSX = (
<Fragment key="domainJSX">
<Typography className={invalidDomain ? classesDangerTitle : classes.messageTitle}>
{t.popups_wallet_sign_in_message_domain()}
</Typography>
<Typography className={invalidDomain ? classesDangerField : classes.sourceText}>{domain}</Typography>
{invalidDomain ?
<Alert className={classes.dangerField} open>
{t.popups_wallet_sign_in_message_domain_invalid({ messageOrigin, domain: message.domain })}
</Alert>
: null}
</Fragment>
)
if (invalidDomain) dangerFields.push(domainJSX)
else normalFields.push(domainJSX)

const chainID_JSX = (
<Fragment key="chainID">
<Typography className={invalidChainId ? classesDangerTitle : classes.messageTitle}>
{t.chain_id()}
</Typography>
<Typography className={invalidChainId ? classesDangerField : classes.sourceText}>{chainId}</Typography>
{invalidChainId ?
<Alert className={classes.dangerField} open>
{t.popups_wallet_sign_in_message_chainID_invalid()}
</Alert>
: null}
</Fragment>
)
if (invalidChainId) dangerFields.push(chainID_JSX)
else normalFields.push(chainID_JSX)

const notBeforeJSX =
not_before ?
<Fragment key="notBefore">
<Typography className={invalidNotBefore ? classesDangerTitle : classes.messageTitle}>
{t.popups_wallet_sign_in_message_not_before()}
</Typography>
<Typography className={invalidNotBefore ? classesDangerField : classes.sourceText}>
{not_before.toLocaleString()}
</Typography>
{invalidNotBefore ?
<Alert className={classes.dangerField} open>
{t.popups_wallet_sign_in_message_not_before_invalid()}
</Alert>
: null}
</Fragment>
: null
if (invalidNotBefore) dangerFields.push(notBeforeJSX)
else normalFields.push(notBeforeJSX)

const expirationTimeJSX =
expiration_time ?
<Fragment key="expirationTime">
<Typography className={invalidExpirationTime ? classesDangerTitle : classes.messageTitle}>
{t.popups_wallet_sign_in_message_not_after()}
</Typography>
<Typography className={invalidExpirationTime ? classesDangerField : classes.sourceText}>
{expiration_time.toLocaleString()}
</Typography>
{invalidExpirationTime ?
<Alert className={classes.dangerField} open>
{t.popups_wallet_sign_in_message_expiration_time_invalid()}
</Alert>
: null}
</Fragment>
: null
if (invalidExpirationTime) dangerFields.push(expirationTimeJSX)
else normalFields.push(expirationTimeJSX)

const nonceJSX = (
<Fragment key="nonce">
<Typography className={classes.messageTitle}>{t.nonce()}</Typography>
<Typography className={classes.sourceText}>{nonce}</Typography>
</Fragment>
)
normalFields.push(nonceJSX)

const request_id_JSX =
request_id ?
<Fragment key="request_id">
<Typography className={classes.messageTitle}>{t.popups_wallet_sign_in_message_request_id()}</Typography>
<Typography className={classes.sourceText}>{request_id}</Typography>
</Fragment>
: null
normalFields.push(request_id_JSX)

return (
<>
{dangerFields}
{normalFields}
</>
)
}
Expand Up @@ -21,7 +21,7 @@ const PersonaSignRequest = memo(() => {
const personas = usePersonasFromDB()
const { currentPersona } = PersonaContext.useContainer()

const source = params.get('source')
const source = params.get('source') || undefined

useEffect(() => {
if (!personas.length) return
Expand Down Expand Up @@ -110,7 +110,7 @@ const PersonaSignRequest = memo(() => {

return (
<Box p={2}>
<SignRequestInfo message={message} source={source} />
<SignRequestInfo message={message} rawMessage={message} origin={source} />
<BottomController>
<ActionButton loading={cancelLoading} onClick={handleCancel} fullWidth variant="outlined">
{t.cancel()}
Expand Down
@@ -0,0 +1,16 @@
import { useWallet } from '@masknet/web3-hooks-base'
import { useState } from 'react'
import { createContainer } from 'unstated-next'

/**
* This context is used to allow different Wallet to be used (rather that the "selected" wallet in the main UI)
* when a interaction is going on (like signing a transaction).
*/
export const { Provider: InteractionWalletContext, useContainer: useInteractionWalletContext } = createContainer(
function () {
const wallet = useWallet()
const [interactionWallet, setInteractionWallet] = useState<string | undefined>()
return { interactionWallet: interactionWallet || wallet?.address, setInteractionWallet }
},
)
InteractionWalletContext.displayName = 'InteractionContext'

0 comments on commit 6ab8e0e

Please sign in to comment.