diff --git a/packages/icons/brands/Blocto.tsx b/packages/icons/brands/Blocto.tsx
new file mode 100644
index 000000000000..475355bf36dd
--- /dev/null
+++ b/packages/icons/brands/Blocto.tsx
@@ -0,0 +1,49 @@
+import { createIcon } from '../utils'
+import type { SvgIcon } from '@mui/material'
+
+export const BloctoIcon: typeof SvgIcon = createIcon(
+ 'Blocto',
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ '0 0 43 50',
+)
diff --git a/packages/icons/brands/index.ts b/packages/icons/brands/index.ts
index 107479bd226b..de5508807f7f 100644
--- a/packages/icons/brands/index.ts
+++ b/packages/icons/brands/index.ts
@@ -12,3 +12,4 @@ export * from './MaskGrey'
export * from './Discord'
export * from './MaskBanner'
export * from './MaskPlaceholder'
+export * from './Blocto'
diff --git a/packages/mask/src/components/shared/VerifyWallet/CurrentWalletBox.tsx b/packages/mask/src/components/shared/VerifyWallet/CurrentWalletBox.tsx
new file mode 100644
index 000000000000..6feb2f06b437
--- /dev/null
+++ b/packages/mask/src/components/shared/VerifyWallet/CurrentWalletBox.tsx
@@ -0,0 +1,216 @@
+import { useCallback } from 'react'
+import { ExternalLink } from 'react-feather'
+import classNames from 'classnames'
+import { ProviderType } from '@masknet/web3-shared-evm'
+import { Button, Link, Typography } from '@mui/material'
+import { makeStyles } from '@masknet/theme'
+import { useRemoteControlledDialog } from '@masknet/shared-base-ui'
+import {
+ useAccount,
+ useWeb3State,
+ useChainId,
+ useNetworkDescriptor,
+ useProviderDescriptor,
+ useProviderType,
+ useReverseAddress,
+ useWallet,
+} from '@masknet/plugin-infra/web3'
+import { FormattedAddress, WalletIcon } from '@masknet/shared'
+import { WalletMessages } from '../../../plugins/Wallet/messages'
+import { useI18N } from '../../../utils'
+import Services from '../../../extension/service'
+import { ActionButtonPromise } from '../../../extension/options-page/DashboardComponents/ActionButton'
+
+const useStyles = makeStyles()((theme) => ({
+ currentAccount: {
+ padding: '12px 8px',
+ width: '100%',
+ background: '#fff',
+ boxSizing: 'border-box',
+ marginBottom: theme.spacing(2),
+ display: 'flex',
+ borderRadius: 8,
+ alignItems: 'center',
+ },
+
+ accountInfo: {
+ flexGrow: 1,
+ marginLeft: theme.spacing(1),
+ },
+ accountName: {
+ fontSize: 16,
+ marginRight: 4,
+ fontWeight: 'bold',
+ },
+ infoRow: {
+ display: 'flex',
+ alignItems: 'center',
+ },
+ actionButton: {
+ fontSize: 14,
+ marginLeft: theme.spacing(1),
+ padding: '8px 12px',
+ fontWeight: 'bold',
+ background: 'rgba(15, 20, 25, 1)',
+ color: '#fff',
+ minWidth: 70,
+ borderRadius: '6px',
+ },
+ address: {
+ fontSize: 12,
+ marginRight: theme.spacing(1),
+ display: 'inline-block',
+ color: theme.palette.text.secondary,
+ },
+ link: {
+ color: theme.palette.text.primary,
+ fontSize: 14,
+ display: 'flex',
+ alignItems: 'center',
+ },
+ linkIcon: {
+ marginRight: theme.spacing(1),
+ },
+ twitterProviderBorder: {
+ width: 14,
+ height: 14,
+ },
+ connectButtonWrapper: {
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ margin: theme.spacing(2, 0),
+ },
+ domain: {
+ fontSize: 16,
+ lineHeight: '18px',
+ marginLeft: 6,
+ padding: 4,
+ borderRadius: 8,
+ backgroundColor: '#ffffff',
+ color: theme.palette.common.black,
+ },
+}))
+interface CurrentWalletBox {
+ isDashboard?: boolean
+ disableChange?: boolean
+}
+export function CurrentWalletBox(props: CurrentWalletBox) {
+ const { t } = useI18N()
+ const { classes } = useStyles()
+ const chainId = useChainId()
+ const account = useAccount()
+ const wallet = useWallet()
+ const providerType = useProviderType()
+ const providerDescriptor = useProviderDescriptor()
+ const networkDescriptor = useNetworkDescriptor()
+ const { Utils } = useWeb3State() ?? {}
+
+ const { value: domain } = useReverseAddress(account)
+
+ // #region change provider
+ const { openDialog: openSelectProviderDialog } = useRemoteControlledDialog(
+ WalletMessages.events.selectProviderDialogUpdated,
+ )
+ // #endregion
+
+ // #region walletconnect
+ const { setDialog: setWalletConnectDialog } = useRemoteControlledDialog(
+ WalletMessages.events.walletConnectQRCodeDialogUpdated,
+ )
+ // #endregion
+
+ const onDisconnect = useCallback(async () => {
+ switch (providerType) {
+ case ProviderType.WalletConnect:
+ setWalletConnectDialog({
+ open: true,
+ uri: await Services.Ethereum.createConnectionURI(),
+ })
+ break
+ case ProviderType.Fortmatic:
+ await Services.Ethereum.disconnectFortmatic(chainId)
+ break
+ }
+ }, [chainId, providerType, setWalletConnectDialog])
+
+ const onChange = useCallback(() => {
+ openSelectProviderDialog()
+ }, [openSelectProviderDialog])
+
+ return account ? (
+
+
+
+
+ {providerType !== ProviderType.MaskWallet ? (
+
+ {domain && Utils?.formatDomainName
+ ? Utils.formatDomainName(domain)
+ : providerDescriptor?.name}
+
+ ) : (
+ <>
+
+ {wallet?.name ?? providerDescriptor?.name}
+
+ {domain && Utils?.formatDomainName ? (
+ {Utils.formatDomainName(domain)}
+ ) : null}
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {!props.disableChange && (
+
+ {providerType === ProviderType.WalletConnect || providerType === ProviderType.Fortmatic ? (
+ >}
+ failIcon={<>>}
+ />
+ ) : null}
+
+ {t('wallet_status_button_change')}
+
+
+ )}
+
+ ) : (
+
+
+ {t('plugin_wallet_on_connect')}
+
+
+ )
+}
diff --git a/packages/mask/src/components/shared/VerifyWallet/Steps.tsx b/packages/mask/src/components/shared/VerifyWallet/Steps.tsx
new file mode 100644
index 000000000000..566a677ebd47
--- /dev/null
+++ b/packages/mask/src/components/shared/VerifyWallet/Steps.tsx
@@ -0,0 +1,161 @@
+import { makeStyles } from '@masknet/theme'
+import { CurrentWalletBox } from './CurrentWalletBox'
+import {
+ step1ActiveIcon,
+ stepSuccessIcon,
+ dividerDisableIcon,
+ dividerSuccessIcon,
+ dividerActiveIcon,
+ step2DisableIcon,
+ step2ActiveIcon,
+} from './constants'
+import { ImageIcon } from '@masknet/shared'
+import { Typography } from '@mui/material'
+import ActionButton from '../../../extension/options-page/DashboardComponents/ActionButton'
+import { useNavigate } from 'react-router-dom'
+
+const useStyles = makeStyles()((theme) => ({
+ container: {
+ width: '100%',
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ },
+ stepBox: {
+ marginTop: 12,
+ display: 'flex',
+ alignItems: 'flex-start',
+ flexGrow: 'grow',
+ },
+ stepLine: {
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ marginRight: 12,
+ },
+ stepRowBox: {
+ width: '100%',
+ display: 'flex',
+ flexDirection: 'column',
+ minHeight: 200,
+ },
+ stepRow: {
+ color: theme.palette.text.secondary,
+ minHeight: 100,
+ },
+ stepIntro: {
+ marginTop: 12,
+ fontSize: 14,
+ },
+ divider: {
+ margin: '4px 0',
+ },
+ actionBox: {
+ width: 'calc(100% - 32px)',
+ gap: 12,
+ position: 'fixed',
+ bottom: 16,
+ display: 'flex',
+ alignItems: 'center',
+ },
+ roundBtn: {
+ borderRadius: 99,
+ },
+}))
+
+export enum SignSteps {
+ Ready = 0,
+ FirstStepDone = 1,
+ SecondStepDone = 2,
+}
+
+interface StepsProps {
+ step: SignSteps
+ personaSign: () => void
+ walletSign: () => void
+}
+
+export function Steps(props: StepsProps) {
+ const { classes } = useStyles()
+ const navigate = useNavigate()
+ const { step, personaSign, walletSign } = props
+
+ const stepIconMap = {
+ [SignSteps.Ready]: {
+ step1: step1ActiveIcon,
+ divider: dividerDisableIcon,
+ step2: step2DisableIcon,
+ },
+ [SignSteps.FirstStepDone]: {
+ step1: stepSuccessIcon,
+ divider: dividerActiveIcon,
+ step2: step2ActiveIcon,
+ },
+ [SignSteps.SecondStepDone]: {
+ step1: stepSuccessIcon,
+ divider: dividerSuccessIcon,
+ step2: stepSuccessIcon,
+ },
+ }
+
+ const onConfirm = () => {
+ if (step === SignSteps.Ready) {
+ personaSign()
+ } else if (step === SignSteps.FirstStepDone) {
+ walletSign()
+ } else {
+ navigate(-1)
+ }
+ }
+
+ const onCancel = () => {
+ navigate(-1)
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ Persona Name Sign
+
+ Sign seamlessly with Persona, ensure the validity of data.
+
+
+
+ Wallet Name Sign
+
+ After two steps, you will own, view, utilize all your cyber identities through Next.ID. You
+ can also disconnect them easily.
+
+
+
+
+
+
+ Cancel
+
+
+ {step === 2 ? 'Done' : 'Confirm'}
+
+
+
+ )
+}
diff --git a/packages/mask/src/components/shared/VerifyWallet/constants.ts b/packages/mask/src/components/shared/VerifyWallet/constants.ts
new file mode 100644
index 000000000000..fe083043d000
--- /dev/null
+++ b/packages/mask/src/components/shared/VerifyWallet/constants.ts
@@ -0,0 +1,7 @@
+export const step1ActiveIcon = new URL('../assets/stepAssets/step1Active.png', import.meta.url)
+export const step2DisableIcon = new URL('../assets/stepAssets/step2Disable.png', import.meta.url)
+export const step2ActiveIcon = new URL('../assets/stepAssets/step2Active.png', import.meta.url)
+export const stepSuccessIcon = new URL('../assets/stepAssets/stepSuccess.png', import.meta.url)
+export const dividerDisableIcon = new URL('../assets/stepAssets/dividerDisable.png', import.meta.url)
+export const dividerActiveIcon = new URL('../assets/stepAssets/dividerActive.png', import.meta.url)
+export const dividerSuccessIcon = new URL('../assets/stepAssets/dividerDone.png', import.meta.url)
diff --git a/packages/mask/src/components/shared/assets/stepAssets/dividerActive.png b/packages/mask/src/components/shared/assets/stepAssets/dividerActive.png
new file mode 100644
index 000000000000..260dae99f2f0
Binary files /dev/null and b/packages/mask/src/components/shared/assets/stepAssets/dividerActive.png differ
diff --git a/packages/mask/src/components/shared/assets/stepAssets/dividerDisable.png b/packages/mask/src/components/shared/assets/stepAssets/dividerDisable.png
new file mode 100644
index 000000000000..3eed001bf264
Binary files /dev/null and b/packages/mask/src/components/shared/assets/stepAssets/dividerDisable.png differ
diff --git a/packages/mask/src/components/shared/assets/stepAssets/dividerDone.png b/packages/mask/src/components/shared/assets/stepAssets/dividerDone.png
new file mode 100644
index 000000000000..91f645ea7995
Binary files /dev/null and b/packages/mask/src/components/shared/assets/stepAssets/dividerDone.png differ
diff --git a/packages/mask/src/components/shared/assets/stepAssets/step1Active.png b/packages/mask/src/components/shared/assets/stepAssets/step1Active.png
new file mode 100644
index 000000000000..6ff3cf904013
Binary files /dev/null and b/packages/mask/src/components/shared/assets/stepAssets/step1Active.png differ
diff --git a/packages/mask/src/components/shared/assets/stepAssets/step2Active.png b/packages/mask/src/components/shared/assets/stepAssets/step2Active.png
new file mode 100644
index 000000000000..8fc56f653eb1
Binary files /dev/null and b/packages/mask/src/components/shared/assets/stepAssets/step2Active.png differ
diff --git a/packages/mask/src/components/shared/assets/stepAssets/step2Disable.png b/packages/mask/src/components/shared/assets/stepAssets/step2Disable.png
new file mode 100644
index 000000000000..98fd95280d71
Binary files /dev/null and b/packages/mask/src/components/shared/assets/stepAssets/step2Disable.png differ
diff --git a/packages/mask/src/components/shared/assets/stepAssets/stepSuccess.png b/packages/mask/src/components/shared/assets/stepAssets/stepSuccess.png
new file mode 100644
index 000000000000..032d8502b4a6
Binary files /dev/null and b/packages/mask/src/components/shared/assets/stepAssets/stepSuccess.png differ
diff --git a/packages/mask/src/extension/popups/assets/blocto.svg b/packages/mask/src/extension/popups/assets/blocto.svg
new file mode 100644
index 000000000000..646d48924816
--- /dev/null
+++ b/packages/mask/src/extension/popups/assets/blocto.svg
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/mask/src/extension/popups/assets/mask.svg b/packages/mask/src/extension/popups/assets/mask.svg
new file mode 100644
index 000000000000..3666da4207ed
--- /dev/null
+++ b/packages/mask/src/extension/popups/assets/mask.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/mask/src/extension/popups/assets/metamask.svg b/packages/mask/src/extension/popups/assets/metamask.svg
new file mode 100644
index 000000000000..2a8c95af16a7
--- /dev/null
+++ b/packages/mask/src/extension/popups/assets/metamask.svg
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/mask/src/extension/popups/assets/walletconnect.svg b/packages/mask/src/extension/popups/assets/walletconnect.svg
new file mode 100644
index 000000000000..764df3dbd43b
--- /dev/null
+++ b/packages/mask/src/extension/popups/assets/walletconnect.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/mask/src/extension/popups/pages/Personas/index.tsx b/packages/mask/src/extension/popups/pages/Personas/index.tsx
index e65200c4b638..a64919dcf9af 100644
--- a/packages/mask/src/extension/popups/pages/Personas/index.tsx
+++ b/packages/mask/src/extension/popups/pages/Personas/index.tsx
@@ -10,6 +10,7 @@ const Home = lazy(() => import('./Home'))
const Logout = lazy(() => import('./Logout'))
const PersonaRename = lazy(() => import('./Rename'))
const PersonaSignRequest = lazy(() => import('./PersonaSignRequest'))
+const VerifyWallet = lazy(() => import('../Wallet/VerifyWallet'))
const SelectPersona = lazy(() => import('./SelectPersona'))
const Accounts = lazy(() => import('./Accounts'))
const AccountDetail = lazy(() => import('./AccountDetail'))
@@ -29,6 +30,7 @@ const Persona = memo(() => {
} />
} />
} />
+ } />
} />
diff --git a/packages/mask/src/extension/popups/pages/Wallet/ConnectWallet/constants.ts b/packages/mask/src/extension/popups/pages/Wallet/ConnectWallet/constants.ts
new file mode 100644
index 000000000000..ed1b39f91e7f
--- /dev/null
+++ b/packages/mask/src/extension/popups/pages/Wallet/ConnectWallet/constants.ts
@@ -0,0 +1,8 @@
+const supportedWallets = [
+ { title: 'MetaMask', icon: new URL('../../../assets/metamask.svg', import.meta.url).toString() },
+ { title: 'Mask', icon: new URL('../../../assets/mask.svg', import.meta.url).toString() },
+ { title: 'WalletConnect', icon: new URL('../../../assets/walletconnect.svg', import.meta.url).toString() },
+ { title: 'Blocto', icon: new URL('../../../assets/blocto.svg', import.meta.url).toString() },
+]
+
+export { supportedWallets }
diff --git a/packages/mask/src/extension/popups/pages/Wallet/ConnectWallet/index.tsx b/packages/mask/src/extension/popups/pages/Wallet/ConnectWallet/index.tsx
new file mode 100644
index 000000000000..b9f1e4ebe884
--- /dev/null
+++ b/packages/mask/src/extension/popups/pages/Wallet/ConnectWallet/index.tsx
@@ -0,0 +1,60 @@
+import { makeStyles } from '@masknet/theme'
+import { Typography } from '@mui/material'
+import { memo } from 'react'
+import { supportedWallets } from './constants'
+import { useNavigate } from 'react-router-dom'
+import { PopupRoutes } from '@masknet/shared-base'
+
+const useStyles = makeStyles()((theme) => ({
+ container: {
+ width: '100%',
+ display: 'flex',
+ flexWrap: 'wrap',
+ columnGap: 12,
+ rowGap: 16,
+ padding: '16px',
+ boxSizing: 'border-box',
+ },
+ walletItem: {
+ cursor: 'pointer',
+ background: '#fff',
+ width: 'calc( 50% - 6px )',
+ borderRadius: '8px',
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ gap: 4,
+ padding: '12px 0',
+ boxSizing: 'content-box',
+ fontSize: 12,
+ fontWeight: 700,
+ color: theme.palette.text.secondary,
+ fontFamily: 'Helvetica',
+ height: 'fit-content',
+ },
+ walletIcon: {
+ width: '5rem',
+ height: '5rem',
+ },
+}))
+const ConnectWalletPage = memo(() => {
+ const { classes } = useStyles()
+ const navigate = useNavigate()
+ const clickItem = () => {
+ navigate(PopupRoutes.VerifyWallet)
+ }
+ return (
+
+ {supportedWallets.map((item, idx) => {
+ return (
+
+
+
{item.title}
+
+ )
+ })}
+
+ )
+})
+
+export default ConnectWalletPage
diff --git a/packages/mask/src/extension/popups/pages/Wallet/VerifyWallet/index.tsx b/packages/mask/src/extension/popups/pages/Wallet/VerifyWallet/index.tsx
new file mode 100644
index 000000000000..ceb359a0cf9f
--- /dev/null
+++ b/packages/mask/src/extension/popups/pages/Wallet/VerifyWallet/index.tsx
@@ -0,0 +1,79 @@
+import { useWallet } from '@masknet/web3-shared-evm'
+import { NextIDAction, NextIDPayload, NextIDPlatform } from '@masknet/shared-base'
+import { makeStyles } from '@masknet/theme'
+import { NextIDProof } from '@masknet/web3-providers'
+import { memo, useState } from 'react'
+import { SignSteps, Steps } from '../../../../../components/shared/VerifyWallet/Steps'
+import Services from '../../../../service'
+import { PersonaContext } from '../../Personas/hooks/usePersonaContext'
+
+const useStyles = makeStyles()((theme) => ({
+ container: {
+ display: 'flex',
+ flexDirection: 'column',
+ flex: 1,
+ background: 'rgba(247, 249, 250, 1)',
+ padding: '8px 16px 16px 16px',
+ },
+}))
+const VerifyWallet = memo(() => {
+ const { classes } = useStyles()
+ const { currentPersona } = PersonaContext.useContainer()
+ const wallet = useWallet()
+
+ const [step, setStep] = useState(SignSteps.Ready)
+ const [signature, setSignature] = useState()
+ const [payload, setPayload] = useState()
+ if (!currentPersona || !wallet) return null
+ const personaSilentSign = async () => {
+ try {
+ const payload = await NextIDProof.createPersonaPayload(
+ currentPersona.publicHexKey as string,
+ NextIDAction.Create,
+ wallet.address,
+ NextIDPlatform.Ethereum,
+ 'default',
+ )
+ if (!payload) throw new Error('Failed to create persona payload.')
+ setPayload(payload)
+ const signResult = await Services.Identity.generateSignResult(
+ currentPersona.identifier,
+ payload.signPayload,
+ )
+ setSignature(signResult.signature.signature)
+ if (!signResult) throw new Error('Failed to sign persona.')
+ setStep(SignSteps.FirstStepDone)
+ } catch (error) {
+ console.error(error)
+ }
+ }
+ const walletSign = async () => {
+ if (!payload) throw new Error('payload error')
+ try {
+ const walletSig = await Services.Ethereum.personalSign(payload.signPayload, wallet.address)
+ if (!walletSig) throw new Error('Wallet sign failed')
+ await NextIDProof.bindProof(
+ payload.uuid,
+ currentPersona.publicHexKey as string,
+ NextIDAction.Create,
+ NextIDPlatform.Ethereum,
+ wallet.address,
+ payload.createdAt,
+ {
+ walletSignature: walletSig,
+ signature: signature,
+ },
+ )
+ setStep(SignSteps.SecondStepDone)
+ } catch (error) {
+ console.error(error)
+ }
+ }
+ return (
+
+
+
+ )
+})
+
+export default VerifyWallet
diff --git a/packages/mask/src/extension/popups/pages/Wallet/index.tsx b/packages/mask/src/extension/popups/pages/Wallet/index.tsx
index 884165e68f49..ca90500df96f 100644
--- a/packages/mask/src/extension/popups/pages/Wallet/index.tsx
+++ b/packages/mask/src/extension/popups/pages/Wallet/index.tsx
@@ -33,6 +33,8 @@ const SetPaymentPassword = lazy(() => import('./SetPaymentPassword'))
const WalletRecovery = lazy(() => import('./WalletRecovery'))
const LegacyWalletRecovery = lazy(() => import('./LegacyWalletRecovery'))
const ReplaceTransaction = lazy(() => import('./ReplaceTransaction'))
+const VerifyWallet = lazy(() => import('./VerifyWallet'))
+const ConnectWallet = lazy(() => import('./ConnectWallet'))
const r = relativeRouteOf(PopupRoutes.Wallet)
export default function Wallet() {
@@ -118,6 +120,7 @@ export default function Wallet() {
} />
} />
} />
+ } />
)}
diff --git a/packages/shared-base/src/Routes/PopupRoutes.ts b/packages/shared-base/src/Routes/PopupRoutes.ts
index 2d91f177a4c7..880024ec71c0 100644
--- a/packages/shared-base/src/Routes/PopupRoutes.ts
+++ b/packages/shared-base/src/Routes/PopupRoutes.ts
@@ -34,4 +34,6 @@ export enum PopupRoutes {
ThirdPartyRequestPermission = '/3rd-request-permission',
SignRequest = '/sign-request',
Swap = '/swap',
+ ConnectWallet = '/wallet/connect',
+ VerifyWallet = '/personas/verify',
}