diff --git a/cspell.json b/cspell.json index d49cdd918982..b0bac3b05249 100644 --- a/cspell.json +++ b/cspell.json @@ -117,6 +117,7 @@ "juicebox", "kdfparams", "keccak", + "keybase", "kitts", "kred", "labelledby", diff --git a/packages/mask/background/database/persona/app.ts b/packages/mask/background/database/persona/app.ts index f3b8f60a8c47..907c065f112e 100644 --- a/packages/mask/background/database/persona/app.ts +++ b/packages/mask/background/database/persona/app.ts @@ -29,6 +29,7 @@ import type { AESJsonWebKey as Native_AESJsonWebKey, } from '@masknet/public-api' import { MaskMessages } from '../../../shared' +import { convertPersonaHexPublicKey } from './util' export async function consistentPersonaDBWriteAccess(action: () => Promise) { await action() } @@ -392,11 +393,14 @@ function partialPersonaRecordToDB( } function personaRecordOutDB(x: NativePersonaRecord): PersonaRecord { + const identifier = Identifier.fromString(x.identifier, ECKeyIdentifier).unwrap() + return { publicKey: x.publicKey as JsonWebKey as unknown as EC_Public_JsonWebKey, + publicHexKey: convertPersonaHexPublicKey(identifier), privateKey: x.privateKey as JsonWebKey as unknown as EC_Private_JsonWebKey, localKey: x.localKey as JsonWebKey as unknown as AESJsonWebKey, - identifier: Identifier.fromString(x.identifier, ECKeyIdentifier).unwrap(), + identifier, linkedProfiles: new IdentifierMap(new Map(Object.entries(x.linkedProfiles)), ProfileIdentifier), createdAt: new Date(x.createdAt), updatedAt: new Date(x.updatedAt), diff --git a/packages/mask/background/database/persona/type.ts b/packages/mask/background/database/persona/type.ts index 9f3b8a47ecce..340dd87ac826 100644 --- a/packages/mask/background/database/persona/type.ts +++ b/packages/mask/background/database/persona/type.ts @@ -112,6 +112,7 @@ export interface PersonaRecord { parameter: { path: string; withPassword: boolean } } publicKey: EC_Public_JsonWebKey + publicHexKey?: string privateKey?: EC_Private_JsonWebKey localKey?: AESJsonWebKey nickname?: string diff --git a/packages/mask/background/database/persona/util.ts b/packages/mask/background/database/persona/util.ts new file mode 100644 index 000000000000..992ad3ad8bf7 --- /dev/null +++ b/packages/mask/background/database/persona/util.ts @@ -0,0 +1,9 @@ +import { compressSecp256k1Point, decompressSecp256k1Key, PersonaIdentifier, toHex } from '@masknet/shared-base' + +export function convertPersonaHexPublicKey(persona: PersonaIdentifier) { + const key256 = decompressSecp256k1Key(persona.compressedPoint.replace(/\|/g, '/')) + if (!key256.x || !key256.y) return + const arr = compressSecp256k1Point(key256.x, key256.y) + + return `0x${toHex(arr)}` +} diff --git a/packages/mask/background/database/persona/web.ts b/packages/mask/background/database/persona/web.ts index 5117cf146805..c4f337b015cc 100644 --- a/packages/mask/background/database/persona/web.ts +++ b/packages/mask/background/database/persona/web.ts @@ -29,6 +29,7 @@ import type { PersonaRecord, } from './type' import { isEmpty } from 'lodash-unified' +import { convertPersonaHexPublicKey } from './util' /** * Database structure: * @@ -701,9 +702,12 @@ function personaRecordToDB(x: PersonaRecord): PersonaRecordDB { function personaRecordOutDB(x: PersonaRecordDB): PersonaRecord { // @ts-ignore delete x.hasPrivateKey + const identifier = Identifier.fromString(x.identifier, ECKeyIdentifier).unwrap() + const obj: PersonaRecord = { ...x, - identifier: Identifier.fromString(x.identifier, ECKeyIdentifier).unwrap(), + identifier, + publicHexKey: convertPersonaHexPublicKey(identifier), linkedProfiles: new IdentifierMap(x.linkedProfiles, ProfileIdentifier), } return obj diff --git a/packages/mask/background/services/helper/nextID.ts b/packages/mask/background/services/helper/nextID.ts index 30b480793fd6..c9dba1b0f509 100644 --- a/packages/mask/background/services/helper/nextID.ts +++ b/packages/mask/background/services/helper/nextID.ts @@ -80,6 +80,7 @@ async function queryPersonaHexPublicKey(persona: PersonaIdentifier) { return `0x${toHex(arr)}` } +/** @deprecated Use NextIDService */ export async function queryExistedBinding(persona: PersonaIdentifier) { const publicKey = await queryPersonaHexPublicKey(persona) if (!publicKey) return @@ -92,6 +93,7 @@ export async function queryExistedBinding(persona: PersonaIdentifier) { return first(result.ids) } +/** @deprecated Use NextIDService */ export async function createPersonaPayload( persona: PersonaIdentifier, action: 'create' | 'delete', diff --git a/packages/mask/shared-ui/locales/en-US.json b/packages/mask/shared-ui/locales/en-US.json index 5b42258a1c22..dc1dd69879f9 100644 --- a/packages/mask/shared-ui/locales/en-US.json +++ b/packages/mask/shared-ui/locales/en-US.json @@ -83,15 +83,22 @@ "setup_guide_find_username_text": "Mask needs the username to connect your profile to your persona.", "setup_guide_connect_auto": "Connect", "setup_guide_connect_failed": "Re-Connect", + "setup_guide_verify": "Verfiy", + "setup_guide_verify_dismiss": "Don't show again.", + "setup_guide_verify_checking": "Checking", + "setup_guide_verify_post_not_found": "No verification post found", + "setup_guide_verifying": "Verfiying", + "setup_guide_verifying_failed": "Re-Verfiy", "user_guide_tip_connected": "You have connected to this account successfully.", + "user_guide_tip_need_verify_on_next_id": "Mask Network requires you to post an verification tweet to access Next.ID-based products.", "setup_guide_say_hello_content": "Hello Mask world. This is my first encrypted message. Install https://mask.io to send me encrypted post. ", "setup_guide_say_hello_follow": "Follow {{account}} to explore Web 3.0.", - "setup_guide_pin_tip_0": "Don't forget to pin Mask Network in your browser toolbar to access web 3.0 easily.", - "setup_guide_pin_tip_1": "Click on ", - "setup_guide_pin_tip_1_s": " at top right of your browser.", - "setup_guide_pin_tip_2": "Find Mask Network in the list of extensions and click the ", - "setup_guide_pin_tip_2_s": " button.", - "setup_guide_pin_tip_3": "Pinned successfully.", + "setup_guide_pin_tip": "Don't forget to pin Mask Network in your browser toolbar to access web 3.0 easily.", + "setup_guide_pin_tip_step_click_left": "Click on ", + "setup_guide_pin_tip_step_click_right": " at top right of your browser.", + "setup_guide_pin_tip_step_find_left": "Find Mask Network in the list of extensions and click the ", + "setup_guide_pin_tip_step_find_right": " button.", + "setup_guide_pin_tip_successfully": "Pinned successfully.", "setup_guide_pin_dismiss": "Don't show again.", "user_guide_tip_1": "Connect wallet to explore multi-chain dApps.", "user_guide_tip_2": "Setup your exclusive NFT Avatar, explore the endless possibilities of Web 3.0.", diff --git a/packages/mask/src/components/DataSource/useNextID.ts b/packages/mask/src/components/DataSource/useNextID.ts new file mode 100644 index 000000000000..cc57b68e16ac --- /dev/null +++ b/packages/mask/src/components/DataSource/useNextID.ts @@ -0,0 +1,73 @@ +import { useAsync, useAsyncRetry } from 'react-use' +import type { NextIDPlatform } from '@masknet/shared-base' +import Services from '../../extension/service' +import { useMemo, useState } from 'react' +import { activatedSocialNetworkUI } from '../../social-network' +import { usePersonaConnectStatus } from './usePersonaConnectStatus' +import { currentSetupGuideStatus, dismissVerifyNextID } from '../../settings/settings' +import stringify from 'json-stable-stringify' +import { SetupGuideStep } from '../InjectedComponents/SetupGuide/types' +import type { SetupGuideCrossContextStatus } from '../../settings/types' +import { useLastRecognizedIdentity } from './useActivatedUI' +import { useValueRef } from '@masknet/shared' +import { queryExistedBindingByPersona, queryExistedBindingByPlatform, queryIsBound } from '@masknet/web3-providers' + +export const usePersonaBoundPlatform = (personaPublicKey: string) => { + useAsyncRetry(() => { + return queryExistedBindingByPersona(personaPublicKey) + }, [personaPublicKey]) +} + +let isOpenedVerifyDialog = false + +export const useNextIDBoundByPlatform = (platform: NextIDPlatform, identity: string) => { + useAsyncRetry(() => { + return queryExistedBindingByPlatform(platform, identity) + }, [platform, identity]) +} + +export function useNextIDConnectStatus() { + const ui = activatedSocialNetworkUI + const [enableNextID] = useState(ui.configuration.nextIDConfig?.enable) + const personaConnectStatus = usePersonaConnectStatus() + const lastStateRef = currentSetupGuideStatus[ui.networkIdentifier] + const lastState_ = useValueRef(lastStateRef) + const lastState = useMemo(() => { + try { + return JSON.parse(lastState_) + } catch { + return {} + } + }, [lastState_]) + + const lastRecognized = useLastRecognizedIdentity() + const [username] = useState( + lastState.username || (lastRecognized.identifier.isUnknown ? '' : lastRecognized.identifier.userId), + ) + + const { value: isVerified = false } = useAsync(async () => { + if (isOpenedVerifyDialog) return true + if (!enableNextID || !username || !personaConnectStatus.connected) return true + + const currentPersona = await Services.Settings.getCurrentPersona() + if (!currentPersona?.publicHexKey) return true + + if (dismissVerifyNextID[ui.networkIdentifier].value[`${username}_${currentPersona.identifier.toText()}`]) + return true + + const platform = ui.configuration.nextIDConfig?.platform as NextIDPlatform | undefined + if (!platform) return true + + const isBound = await queryIsBound(currentPersona.publicHexKey, platform, username) + if (isBound) return true + + currentSetupGuideStatus[ui.networkIdentifier].value = stringify({ + status: SetupGuideStep.VerifyOnNextID, + persona: currentPersona?.identifier.toText(), + }) + isOpenedVerifyDialog = true + return false + }, [username, enableNextID, lastStateRef.value]) + + return isVerified +} diff --git a/packages/mask/src/components/DataSource/usePersonaConnectStatus.ts b/packages/mask/src/components/DataSource/usePersonaConnectStatus.ts index 82f9f49f67b9..1d19ebb5e04f 100644 --- a/packages/mask/src/components/DataSource/usePersonaConnectStatus.ts +++ b/packages/mask/src/components/DataSource/usePersonaConnectStatus.ts @@ -4,7 +4,7 @@ import { useMemo } from 'react' import Services from '../../extension/service' import { currentSetupGuideStatus } from '../../settings/settings' import { activatedSocialNetworkUI } from '../../social-network' -import { SetupGuideStep } from '../InjectedComponents/SetupGuide' +import { SetupGuideStep } from '../InjectedComponents/SetupGuide/types' import { useLastRecognizedIdentity } from './useActivatedUI' import { useMyPersonas } from './useMyPersonas' diff --git a/packages/mask/src/components/InjectedComponents/SetupGuide.tsx b/packages/mask/src/components/InjectedComponents/SetupGuide.tsx index b2096114bb72..8af270978eff 100644 --- a/packages/mask/src/components/InjectedComponents/SetupGuide.tsx +++ b/packages/mask/src/components/InjectedComponents/SetupGuide.tsx @@ -1,375 +1,31 @@ -import { useMemo, useState, useEffect } from 'react' -import { Paper, Typography, IconButton, Box, Button, FormControlLabel, Checkbox } from '@mui/material' -import { makeStyles, MaskColorVar } from '@masknet/theme' -import CloseIcon from '@mui/icons-material/Close' -import { ActionButtonPromise } from '../../extension/options-page/DashboardComponents/ActionButton' +import { useMemo, useState, useEffect, useRef } from 'react' +import { makeStyles } from '@masknet/theme' import { useValueRef } from '@masknet/shared' import { useI18N, MaskMessages } from '../../utils' import { activatedSocialNetworkUI, SocialNetworkUI } from '../../social-network' import { currentSetupGuideStatus, dismissPinExtensionTip, userGuideStatus } from '../../settings/settings' import type { SetupGuideCrossContextStatus } from '../../settings/types' -import { PersonaIdentifier, ProfileIdentifier, Identifier, ECKeyIdentifier } from '@masknet/shared-base' import { makeTypedMessageText } from '@masknet/typed-message' +import { + PersonaIdentifier, + ProfileIdentifier, + Identifier, + ECKeyIdentifier, + NextIDPlatform, + toBase64, + fromHex, + NextIDAction, +} from '@masknet/shared-base' import Services from '../../extension/service' import { useLastRecognizedIdentity } from '../DataSource/useActivatedUI' -import { MaskIcon } from '../../resources/MaskIcon' import { useAsync, useCopyToClipboard } from 'react-use' -import classNames from 'classnames' -import ExtensionIcon from '@mui/icons-material/Extension' import stringify from 'json-stable-stringify' -import { VerifiedIcon, PinIcon } from '@masknet/icons' - -export enum SetupGuideStep { - FindUsername = 'find-username', - PinExtension = 'pin-extension', - Close = 'close', -} - -// #region wizard dialog -const useWizardDialogStyles = makeStyles()((theme) => ({ - root: { - padding: theme.spacing(3), - position: 'relative', - boxShadow: theme.palette.mode === 'dark' ? 'none' : theme.shadows[4], - border: `${theme.palette.mode === 'dark' ? 'solid' : 'none'} 1px ${theme.palette.divider}`, - borderRadius: 20, - [theme.breakpoints.down('sm')]: { - position: 'fixed', - bottom: 0, - left: 0, - margin: 0, - alignSelf: 'center', - borderRadius: 0, - boxShadow: 'none', - border: `solid 1px ${theme.palette.divider}`, - width: '100%', - }, - userSelect: 'none', - boxSizing: 'border-box', - width: 480, - '&.small': { - width: 384, - }, - overflow: 'hidden', - }, - button: { - width: 150, - height: 40, - minHeight: 40, - marginLeft: 0, - marginTop: 0, - [theme.breakpoints.down('sm')]: { - width: '100%', - }, - fontSize: 14, - wordBreak: 'keep-all', - '&,&:hover': { - color: `${MaskColorVar.twitterButtonText} !important`, - background: `${MaskColorVar.twitterButton} !important`, - }, - }, - close: { - color: theme.palette.text.primary, - position: 'absolute', - right: 10, - top: 10, - }, - tip: { - fontSize: 16, - fontWeight: 500, - lineHeight: '22px', - paddingTop: 16, - }, - label: { - fontSize: 16, - fontWeight: 400, - lineHeight: '22px', - }, - header: { - height: 40, - }, - content: {}, - connection: { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-around', - }, - connectItem: { - flex: 1, - height: 75, - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'space-between', - }, - line: { - width: 100, - height: 1, - borderTop: `dashed 1px ${MaskColorVar.borderSecondary}`, - }, - name: { - fontSize: 16, - fontWeight: 500, - }, - footer: {}, -})) - -const useStyles = makeStyles()((theme) => ({ - content: { - marginBottom: theme.spacing(2), - }, - footer: { - marginLeft: 0, - marginTop: theme.spacing(3), - flex: 1, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - flexDirection: 'column', - }, - tip: {}, -})) - -interface ContentUIProps { - dialogType: SetupGuideStep - content?: React.ReactNode - footer?: React.ReactNode - tip?: React.ReactNode - dismiss?: React.ReactNode -} - -function ContentUI(props: ContentUIProps) { - const { classes } = useStyles() - switch (props.dialogType) { - case SetupGuideStep.FindUsername: - case SetupGuideStep.PinExtension: - return ( - -
{props.content}
-
{props.tip}
- {props.footer ?
{props.footer}
: null} - {props.dismiss ?
{props.dismiss}
: null} -
- ) - default: - return null - } -} - -interface WizardDialogProps { - small?: boolean - title?: string - dialogType: SetupGuideStep - optional?: boolean - content?: React.ReactNode - tip?: React.ReactNode - footer?: React.ReactNode - dismiss?: React.ReactNode - onClose?: () => void -} - -function WizardDialog(props: WizardDialogProps) { - const { small, title, dialogType, content, tip, footer, dismiss, onClose } = props - const { classes } = useWizardDialogStyles() - - return ( - -
- - {title} - -
- - {onClose ? ( - - - - ) : null} -
- ) -} -// #endregion - -// #region find username -const useFindUsernameStyles = makeStyles()((theme) => ({ - avatar: { - display: 'block', - width: 48, - height: 48, - borderRadius: '50%', - border: `solid 1px ${MaskColorVar.border}`, - '&.connected': { - borderColor: MaskColorVar.success, - }, - }, - verified: { - position: 'absolute', - bottom: 0, - right: 0, - fontSize: 16, - color: MaskColorVar.success, - }, -})) - -interface FindUsernameProps extends Partial { - username: string - personaName?: string - avatar?: string - onUsernameChange?: (username: string) => void - onConnect: () => Promise - onDone?: () => void -} - -function FindUsername({ personaName, username, avatar, onConnect, onDone, onClose }: FindUsernameProps) { - const { t } = useI18N() - const [connected, setConnected] = useState(false) - - const { classes } = useWizardDialogStyles() - const { classes: findUsernameClasses } = useFindUsernameStyles() - - return ( - - - - - {personaName} - - - {username ? ( - <> - - - - - {connected ? : null} - - - {username} - - - - ) : null} - - } - tip={ - - } - footer={ - username ? ( - setConnected(true)} - disabled={!username || !personaName} - completeIcon={null} - failIcon={null} - failedOnClick="use executor" - data-testid="confirm_button"> - {t('confirm')} - - ) : null - } - onClose={onClose} - /> - ) -} -// #endregion - -interface PinExtensionProps { - onDone?: () => void -} -function PinExtension({ onDone }: PinExtensionProps) { - const pinImg = new URL('../../resources/extensionPinned.png', import.meta.url).toString() - const { classes } = useWizardDialogStyles() - const { t } = useI18N() - const [checked, setChecked] = useState(true) - - return ( - - - - - Mask Network - - - - - - - - } - tip={ - -
{t('setup_guide_pin_tip_0')}
-
    -
  1. - {t('setup_guide_pin_tip_1')} - - {t('setup_guide_pin_tip_1_s')} -
  2. -
  3. - {t('setup_guide_pin_tip_2')} - - {t('setup_guide_pin_tip_2_s')} -
  4. -
  5. {t('setup_guide_pin_tip_3')}
  6. -
-
- } - footer={ - - } - dismiss={ - { - setChecked(e.target.checked) - dismissPinExtensionTip.value = e.target.checked - }} - /> - } - label={t('setup_guide_pin_dismiss')} - /> - } - onClose={onDone} - /> - ) -} +import type { NextIDPayload } from '@masknet/shared-base' +import { SetupGuideStep } from './SetupGuide/types' +import { FindUsername } from './SetupGuide/FindUsername' +import { VerifyNextID } from './SetupGuide/VerifyNextID' +import { PinExtension } from './SetupGuide/PinExtension' +import { bindProof, createPersonaPayload, queryIsBound } from '@masknet/web3-providers' // #region setup guide ui interface SetupGuideUIProps { @@ -377,12 +33,21 @@ interface SetupGuideUIProps { onClose?: () => void } +interface SignInfo { + payload: NextIDPayload + personaSign: string + twitterPost: string +} + function SetupGuideUI(props: SetupGuideUIProps) { const { t } = useI18N() const { persona } = props const ui = activatedSocialNetworkUI const [, copyToClipboard] = useCopyToClipboard() const [step, setStep] = useState(SetupGuideStep.FindUsername) + const [enableNextID] = useState(ui.configuration.nextIDConfig?.enable) + const verifyPostCollectTimer = useRef(null) + const platform = ui.configuration.nextIDConfig?.platform as NextIDPlatform // #region parse setup status const lastStateRef = currentSetupGuideStatus[ui.networkIdentifier] @@ -409,10 +74,10 @@ function SetupGuideUI(props: SetupGuideUIProps) { const handler = (val: SocialNetworkUI.CollectingCapabilities.IdentityResolved) => { if (username === '' && !val.identifier.isUnknown) setUsername(val.identifier.userId) } - activatedSocialNetworkUI.collecting.identityProvider?.recognized.addListener(handler) + ui.collecting.identityProvider?.recognized.addListener(handler) return () => { - activatedSocialNetworkUI.collecting.identityProvider?.recognized.removeListener(handler) + ui.collecting.identityProvider?.recognized.removeListener(handler) } }, [username]) @@ -449,17 +114,88 @@ function SetupGuideUI(props: SetupGuideUIProps) { MaskMessages.events.ownPersonaChanged.sendToAll(undefined) } + const onVerify = async () => { + if (!persona_?.publicHexKey) return + const collectVerificationPost = ui.configuration.nextIDConfig?.collectVerificationPost + + const platform = ui.configuration.nextIDConfig?.platform as NextIDPlatform | undefined + if (!platform) return + + const isBound = await queryIsBound(persona_.publicHexKey, platform, username) + if (!isBound) { + const payload = await createPersonaPayload(persona_.publicHexKey, NextIDAction.Create, username, platform) + if (!payload) throw new Error('Failed to create persona payload.') + const signResult = await Services.Identity.signWithPersona({ + method: 'eth', + message: payload.signPayload, + identifier: persona_.publicHexKey, + }) + if (!signResult) throw new Error('Failed to sign by persona.') + const signature = signResult.signature.signature + const postContent = payload.postContent.replace('%SIG_BASE64%', toBase64(fromHex(signature))) + ui.automation?.nativeCompositionDialog?.appendText?.(postContent, { recover: false }) + + const waitingPost = new Promise((resolve, reject) => { + verifyPostCollectTimer.current = setInterval(async () => { + const post = collectVerificationPost?.(postContent) + if (post && persona_.publicHexKey) { + clearInterval(verifyPostCollectTimer.current!) + await bindProof( + persona_.publicHexKey, + NextIDAction.Create, + platform, + username, + undefined, + signature, + post.postId, + ) + resolve() + } + }, 1000) + + setTimeout(() => { + clearInterval(verifyPostCollectTimer.current!) + reject({ message: t('setup_guide_verify_post_not_found') }) + }, 1000 * 20) + }) + + await waitingPost + } + } + const onClose = () => { currentSetupGuideStatus[ui.networkIdentifier].value = '' setStep(SetupGuideStep.Close) } - const onDone = () => { + const onDone = async () => { + // check verify nextID id state + if (step === SetupGuideStep.FindUsername && enableNextID && persona_?.publicHexKey) { + const isBound = await queryIsBound(persona_.publicHexKey, platform, username) + if (!isBound) { + currentSetupGuideStatus[ui.networkIdentifier].value = stringify({ + status: SetupGuideStep.VerifyOnNextID, + }) + setStep(SetupGuideStep.VerifyOnNextID) + return + } + } + // check pin tip status if (step === SetupGuideStep.FindUsername && !dismissPinExtensionTip.value) { currentSetupGuideStatus[ui.networkIdentifier].value = stringify({ status: SetupGuideStep.PinExtension }) - return setStep(SetupGuideStep.PinExtension) + setStep(SetupGuideStep.PinExtension) + return + } + + // check verify on next id status + if (step === SetupGuideStep.VerifyOnNextID && enableNextID && persona_ && persona_.publicHexKey) { + const isBound = await queryIsBound(persona_.publicHexKey, platform, username) + if (!isBound) return + setStep(SetupGuideStep.PinExtension) + return } + // check user guide status const network = ui.networkIdentifier if (network === 'twitter.com' && userGuideStatus[network].value !== 'completed') { @@ -493,6 +229,21 @@ function SetupGuideUI(props: SetupGuideUIProps) { onConnect={onConnect} onDone={onDone} onClose={onClose} + enableNextID={enableNextID} + /> + ) + case SetupGuideStep.VerifyOnNextID: + return ( + ) case SetupGuideStep.PinExtension: diff --git a/packages/mask/src/components/InjectedComponents/SetupGuide/FindUsername.tsx b/packages/mask/src/components/InjectedComponents/SetupGuide/FindUsername.tsx new file mode 100644 index 000000000000..3b43cfc9051a --- /dev/null +++ b/packages/mask/src/components/InjectedComponents/SetupGuide/FindUsername.tsx @@ -0,0 +1,126 @@ +import { WizardDialog, WizardDialogProps, useWizardDialogStyles } from './WizardDialog' +import { useI18N } from '../../../utils' +import { useState } from 'react' +import { SetupGuideStep } from './types' +import { Box, Typography } from '@mui/material' +import { MaskIcon } from '../../../resources/MaskIcon' +import classNames from 'classnames' +import { VerifiedIcon } from '@masknet/icons' +import { ActionButtonPromise } from '../../../extension/options-page/DashboardComponents/ActionButton' +import { makeStyles, MaskColorVar } from '@masknet/theme' +import { noop } from 'lodash-unified' + +export const useFindUsernameStyles = makeStyles()((theme) => ({ + avatar: { + display: 'block', + width: 48, + height: 48, + borderRadius: '50%', + border: `solid 1px ${MaskColorVar.border}`, + '&.connected': { + borderColor: MaskColorVar.success, + }, + }, + verified: { + position: 'absolute', + bottom: 0, + right: 0, + fontSize: 16, + color: MaskColorVar.success, + }, +})) + +export interface FindUsernameProps extends Partial { + username: string + personaName?: string + avatar?: string + onUsernameChange?: (username: string) => void + onConnect: () => Promise + onDone?: () => void + enableNextID?: boolean +} + +export function FindUsername({ + personaName, + username, + avatar, + onConnect, + onDone, + onClose, + enableNextID, +}: FindUsernameProps) { + const { t } = useI18N() + const [connected, setConnected] = useState(false) + + const { classes } = useWizardDialogStyles() + const { classes: findUsernameClasses } = useFindUsernameStyles() + + return ( + + + + + {personaName} + + + {username ? ( + <> + + + + + {connected ? : null} + + + {username} + + + + ) : null} + + } + tip={ + + } + footer={ + username ? ( + setConnected(true)} + disabled={!username || !personaName} + completeIcon={null} + failIcon={null} + failedOnClick="use executor" + data-testid="confirm_button"> + {t('confirm')} + + ) : null + } + onClose={onClose} + /> + ) +} diff --git a/packages/mask/src/components/InjectedComponents/SetupGuide/PinExtension.tsx b/packages/mask/src/components/InjectedComponents/SetupGuide/PinExtension.tsx new file mode 100644 index 000000000000..79d9bcbe00ab --- /dev/null +++ b/packages/mask/src/components/InjectedComponents/SetupGuide/PinExtension.tsx @@ -0,0 +1,83 @@ +import { useWizardDialogStyles, WizardDialog } from './WizardDialog' +import { useI18N } from '../../../utils' +import { useState } from 'react' +import { SetupGuideStep } from './types' +import { Box, Button, Checkbox, FormControlLabel, Typography } from '@mui/material' +import { MaskIcon } from '../../../resources/MaskIcon' +import ExtensionIcon from '@mui/icons-material/Extension' +import { PinIcon } from '@masknet/icons' +import { dismissPinExtensionTip } from '../../../settings/settings' + +interface PinExtensionProps { + onDone?: () => void +} + +export function PinExtension({ onDone }: PinExtensionProps) { + const pinImg = new URL('../../../resources/extensionPinned.png', import.meta.url).toString() + const { classes } = useWizardDialogStyles() + const { t } = useI18N() + const [checked, setChecked] = useState(true) + + return ( + + + + + Mask Network + + + + + + + + } + tip={ + +
{t('setup_guide_pin_tip')}
+
    +
  1. + {t('setup_guide_pin_tip_step_click_left')} + + {t('setup_guide_pin_tip_step_click_right')} +
  2. +
  3. + {t('setup_guide_pin_tip_step_find_left')} + + {t('setup_guide_pin_tip_step_find_right')} +
  4. +
  5. {t('setup_guide_pin_tip_successfully')}
  6. +
+
+ } + footer={ + + } + dismiss={ + { + setChecked(e.target.checked) + dismissPinExtensionTip.value = e.target.checked + }} + /> + } + label={t('setup_guide_pin_dismiss')} + /> + } + onClose={onDone} + /> + ) +} diff --git a/packages/mask/src/components/InjectedComponents/SetupGuide/VerifyNextID.tsx b/packages/mask/src/components/InjectedComponents/SetupGuide/VerifyNextID.tsx new file mode 100644 index 000000000000..5c7c8811db5e --- /dev/null +++ b/packages/mask/src/components/InjectedComponents/SetupGuide/VerifyNextID.tsx @@ -0,0 +1,125 @@ +import { useI18N } from '../../../utils' +import { Box, Checkbox, FormControlLabel, Typography } from '@mui/material' +import { MaskIcon } from '../../../resources/MaskIcon' +import classNames from 'classnames' +import { VerifiedIcon } from '@masknet/icons' +import { ActionButtonPromise } from '../../../extension/options-page/DashboardComponents/ActionButton' +import { useWizardDialogStyles, WizardDialogProps, WizardDialog } from './WizardDialog' +import { useFindUsernameStyles } from './FindUsername' +import { SetupGuideStep } from './types' +import { dismissVerifyNextID } from '../../../settings/settings' +import { useState } from 'react' +import type { PersonaIdentifier } from '@masknet/shared-base' + +interface VerifyNextIDProps extends Partial { + username: string + personaName?: string + personaIdentifier?: PersonaIdentifier + network: string + avatar?: string + onUsernameChange?: (username: string) => void + onVerify: () => Promise + onDone?: () => void +} + +export const VerifyNextID = ({ + personaName, + personaIdentifier, + username, + avatar, + onVerify, + onDone, + onClose, + network, +}: VerifyNextIDProps) => { + const { t } = useI18N() + + const { classes } = useWizardDialogStyles() + const { classes: findUsernameClasses } = useFindUsernameStyles() + const [checked, setChecked] = useState(false) + + if (!personaIdentifier) return null + + return ( + + + + + {personaName} + + + {username ? ( + <> + + + + + + + + {username} + + + + ) : null} + + } + tip={ + ${t('user_guide_tip_connected')}

${t( + 'user_guide_tip_need_verify_on_next_id', + )}

`, + }} + /> + } + footer={ + username ? ( + + {t('setup_guide_verify')} + + ) : null + } + dismiss={ + { + setChecked(e.target.checked) + dismissVerifyNextID[network].value = { + ...dismissVerifyNextID[network].value, + [`${username}_${personaIdentifier.toText()}`]: e.target.checked, + } + }} + /> + } + label={t('setup_guide_verify_dismiss')} + /> + } + onClose={onClose} + /> + ) +} diff --git a/packages/mask/src/components/InjectedComponents/SetupGuide/WizardDialog.tsx b/packages/mask/src/components/InjectedComponents/SetupGuide/WizardDialog.tsx new file mode 100644 index 000000000000..64076e21fc26 --- /dev/null +++ b/packages/mask/src/components/InjectedComponents/SetupGuide/WizardDialog.tsx @@ -0,0 +1,168 @@ +import { Box, IconButton, Paper, Typography } from '@mui/material' +import classNames from 'classnames' +import CloseIcon from '@mui/icons-material/Close' +import { makeStyles, MaskColorVar } from '@masknet/theme' +import { SetupGuideStep } from './types' + +interface ContentUIProps { + dialogType: SetupGuideStep + content?: React.ReactNode + footer?: React.ReactNode + tip?: React.ReactNode + dismiss?: React.ReactNode +} + +const useStyles = makeStyles()((theme) => ({ + content: { + marginBottom: theme.spacing(2), + }, + footer: { + marginLeft: 0, + marginTop: theme.spacing(3), + flex: 1, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'column', + }, + tip: {}, +})) + +function ContentUI(props: ContentUIProps) { + const { classes } = useStyles() + switch (props.dialogType) { + case SetupGuideStep.FindUsername: + case SetupGuideStep.PinExtension: + return ( + +
{props.content}
+
{props.tip}
+ {props.footer ?
{props.footer}
: null} + {props.dismiss ?
{props.dismiss}
: null} +
+ ) + default: + return null + } +} + +export const useWizardDialogStyles = makeStyles()((theme) => ({ + root: { + padding: theme.spacing(3), + position: 'relative', + boxShadow: theme.palette.mode === 'dark' ? 'none' : theme.shadows[4], + border: `${theme.palette.mode === 'dark' ? 'solid' : 'none'} 1px ${theme.palette.divider}`, + borderRadius: 20, + [theme.breakpoints.down('sm')]: { + position: 'fixed', + bottom: 0, + left: 0, + margin: 0, + alignSelf: 'center', + borderRadius: 0, + boxShadow: 'none', + border: `solid 1px ${theme.palette.divider}`, + width: '100%', + }, + userSelect: 'none', + boxSizing: 'border-box', + width: 480, + '&.small': { + width: 384, + }, + overflow: 'hidden', + }, + button: { + width: 150, + height: 40, + minHeight: 40, + marginLeft: 0, + marginTop: 0, + [theme.breakpoints.down('sm')]: { + width: '100%', + }, + fontSize: 14, + wordBreak: 'keep-all', + '&,&:hover': { + color: `${MaskColorVar.twitterButtonText} !important`, + background: `${MaskColorVar.twitterButton} !important`, + }, + }, + close: { + color: theme.palette.text.primary, + position: 'absolute', + right: 10, + top: 10, + }, + tip: { + fontSize: 16, + fontWeight: 500, + lineHeight: '22px', + paddingTop: 16, + }, + label: { + fontSize: 16, + fontWeight: 400, + lineHeight: '22px', + }, + header: { + height: 40, + }, + content: {}, + connection: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-around', + }, + connectItem: { + flex: 1, + height: 75, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'space-between', + }, + line: { + width: 100, + height: 1, + borderTop: `dashed 1px ${MaskColorVar.borderSecondary}`, + }, + name: { + fontSize: 16, + fontWeight: 500, + }, + footer: {}, +})) + +export interface WizardDialogProps { + small?: boolean + title?: string + dialogType: SetupGuideStep + optional?: boolean + content?: React.ReactNode + tip?: React.ReactNode + footer?: React.ReactNode + dismiss?: React.ReactNode + onClose?: () => void +} + +export function WizardDialog(props: WizardDialogProps) { + const { small, title, dialogType, content, tip, footer, dismiss, onClose } = props + const { classes } = useWizardDialogStyles() + + return ( + +
+ + {title} + +
+ + {onClose ? ( + + + + ) : null} +
+ ) +} diff --git a/packages/mask/src/components/InjectedComponents/SetupGuide/types.ts b/packages/mask/src/components/InjectedComponents/SetupGuide/types.ts new file mode 100644 index 000000000000..1079a2b05b2d --- /dev/null +++ b/packages/mask/src/components/InjectedComponents/SetupGuide/types.ts @@ -0,0 +1,6 @@ +export enum SetupGuideStep { + FindUsername = 'find-username', + VerifyOnNextID = 'next-id-verify', + PinExtension = 'pin-extension', + Close = 'close', +} diff --git a/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx b/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx index ad85644bf5a5..79728142ea19 100644 --- a/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx +++ b/packages/mask/src/components/InjectedComponents/ToolboxUnstyled.tsx @@ -33,6 +33,7 @@ import { MaskFilledIcon } from '../../resources/MaskIcon' import { makeStyles } from '@masknet/theme' import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord' import { usePersonaConnectStatus } from '../DataSource/usePersonaConnectStatus' +import { useNextIDConnectStatus } from '../DataSource/useNextID' const useStyles = makeStyles()((theme) => ({ title: { @@ -83,6 +84,7 @@ export interface ToolboxHintProps { } export function ToolboxHintUnstyled(props: ToolboxHintProps) { const { t } = useI18N() + const isNextIDVerified = useNextIDConnectStatus() const { ListItemButton = MuiListItemButton, ListItemText = MuiListItemText, @@ -109,6 +111,7 @@ export function ToolboxHintUnstyled(props: ToolboxHintProps) { }, [personaConnectStatus, walletTitle, t]) const onClick = () => { + if (!isNextIDVerified) return personaConnectStatus.action ? personaConnectStatus.action() : openWallet() } diff --git a/packages/mask/src/extension/background-script/SettingsService.ts b/packages/mask/src/extension/background-script/SettingsService.ts index bceaf352f5b4..a5f6064f9e09 100644 --- a/packages/mask/src/extension/background-script/SettingsService.ts +++ b/packages/mask/src/extension/background-script/SettingsService.ts @@ -9,7 +9,7 @@ import { pluginIDSettings, } from '../../settings/settings' import { currentDataProviderSettings } from '../../plugins/Trader/settings' -import { queryMyPersonas } from './IdentityService' +import { queryMyPersonas, queryPersona } from './IdentityService' import { currentAccountSettings, currentNetworkSettings, @@ -26,6 +26,7 @@ import { } from '../../plugins/Wallet/settings' import { Flags, MaskMessages } from '../../../shared' import { indexedDB_KVStorageBackend, inMemory_KVStorageBackend } from '../../../background/database/kv-storage' +import type { Persona } from '../../database' function create(settings: InternalSettings) { async function get() { @@ -76,6 +77,12 @@ export async function getWalletAllowTestChain() { return Flags.wallet_allow_testnet } +export async function getCurrentPersona(): Promise { + const currentPersonaIdentifier = await getCurrentPersonaIdentifier() + if (!currentPersonaIdentifier) return undefined + return queryPersona(currentPersonaIdentifier) +} + export async function getCurrentPersonaIdentifier(): Promise { await currentPersonaIdentifier.readyPromise const personas = (await queryMyPersonas()) diff --git a/packages/mask/src/extension/background-script/SocialNetworkService.ts b/packages/mask/src/extension/background-script/SocialNetworkService.ts index 7761972e6172..84be88b859fc 100644 --- a/packages/mask/src/extension/background-script/SocialNetworkService.ts +++ b/packages/mask/src/extension/background-script/SocialNetworkService.ts @@ -4,7 +4,7 @@ import { requestSNSAdaptorPermission } from '../../social-network/utils/permissi import { currentSetupGuideStatus } from '../../settings/settings' import stringify from 'json-stable-stringify' -import { SetupGuideStep } from '../../components/InjectedComponents/SetupGuide' +import { SetupGuideStep } from '../../components/InjectedComponents/SetupGuide/types' import type { PersonaIdentifier } from '@masknet/shared-base' import { delay } from '@dimensiondev/kit' diff --git a/packages/mask/src/settings/settings.ts b/packages/mask/src/settings/settings.ts index 690d31f7d0c9..12f283dde83a 100644 --- a/packages/mask/src/settings/settings.ts +++ b/packages/mask/src/settings/settings.ts @@ -61,6 +61,10 @@ export const sayHelloShowed: NetworkSettings = createNetworkSettings('s export const dismissPinExtensionTip = createGlobalSettings('dismissPinExtensionTip', false, { primary: () => '', }) +export const dismissVerifyNextID: NetworkSettings<{ [key in string]: boolean }> = createNetworkSettings( + 'dismissVerifyNextID', + {}, +) export const bioDescription: NetworkSettings = createNetworkSettings('bioDescription', '') export const personalHomepage: NetworkSettings = createNetworkSettings('personalHomepage', '') // This is a misuse of concept "NetworkSettings" as "namespaced settings" diff --git a/packages/mask/src/settings/types.ts b/packages/mask/src/settings/types.ts index 44fa3978b96c..6a887fd97ae2 100644 --- a/packages/mask/src/settings/types.ts +++ b/packages/mask/src/settings/types.ts @@ -1,7 +1,7 @@ // WARNING: Types with @public mark is exposed as public API to the native side, // if you make an incompatible change in this file, it will break the API. import type { ChainId } from '@masknet/web3-shared-evm' -import type { SetupGuideStep } from '../components/InjectedComponents/SetupGuide' +import type { SetupGuideStep } from '../components/InjectedComponents/SetupGuide/types' export interface ChainBlockNumber { chainId: ChainId blockNumber: number diff --git a/packages/mask/src/social-network-adaptor/twitter.com/ui-provider.ts b/packages/mask/src/social-network-adaptor/twitter.com/ui-provider.ts index d14dee9b94c2..d69bb1e8467a 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/ui-provider.ts +++ b/packages/mask/src/social-network-adaptor/twitter.com/ui-provider.ts @@ -26,7 +26,7 @@ import { injectMaskUserBadgeAtTwitter } from './injection/MaskIcon' import { pasteImageToCompositionDefault } from '../../social-network/defaults/automation/AttachImageToComposition' import { injectPostInspectorAtTwitter } from './injection/PostInspector' import { injectPostActionsAtTwitter } from './injection/PostActions' -import { ProfileIdentifier } from '@masknet/shared-base' +import { NextIDPlatform, PostIdentifier, ProfileIdentifier } from '@masknet/shared-base' import { unreachable } from '@dimensiondev/kit' import { makeStyles } from '@masknet/theme' import { injectNFTAvatarInTwitter } from './injection/NFT/NFTAvatarInTwitter' @@ -37,6 +37,9 @@ import { injectUserNFTAvatarAtTweet } from './injection/NFT/TweetNFTAvatar' import { injectNFTContractAtTwitter } from './injection/NFT/NFTContract' import { injectNFTAvatarClipInTwitter } from './injection/NFT/NFTAvatarClip' import { TwitterRenderFragments } from './customization/render-fragments' +import { timelinePostContentSelector } from './utils/selector' +import { postContentMessageParser, postIdParser } from './utils/fetch' +import { isTypedMessageText } from '@masknet/typed-message' const useInjectedDialogClassesOverwriteTwitter = makeStyles()((theme) => { const smallQuery = `@media (max-width: ${theme.breakpoints.values.sm}px)` @@ -184,6 +187,31 @@ const twitterUI: SocialNetworkUI.Definition = { avatarClipNFT: injectNFTAvatarClipInTwitter, }, configuration: { + nextIDConfig: { + enable: true, + platform: NextIDPlatform.Twitter, + collectVerificationPost: (keyword: string) => { + const userId = + IdentityProviderTwitter.recognized.value.identifier || globalUIState.profiles.value[0].identifier + const postNodes = timelinePostContentSelector().evaluate() + + for (const postNode of postNodes) { + const postId = postIdParser(postNode) + const postContent = postContentMessageParser(postNode) + const isVerified = + postId && + postContent[0] && + isTypedMessageText(postContent[0]) && + (postContent[0]?.content ?? '').toLowerCase() === keyword.toLowerCase() + + if (isVerified && userId) { + return new PostIdentifier(userId, postId) + } + } + + return null + }, + }, steganography: { password() { // ! Change this might be a breaking change ! @@ -197,4 +225,5 @@ const twitterUI: SocialNetworkUI.Definition = { }, }, } + export default twitterUI diff --git a/packages/mask/src/social-network-adaptor/twitter.com/utils/selector.ts b/packages/mask/src/social-network-adaptor/twitter.com/utils/selector.ts index 87a02a576810..3cfa83227e68 100644 --- a/packages/mask/src/social-network-adaptor/twitter.com/utils/selector.ts +++ b/packages/mask/src/social-network-adaptor/twitter.com/utils/selector.ts @@ -143,6 +143,15 @@ export const postsImageSelector = (node: HTMLElement) => '[data-testid="tweet"] ~ div img[src*="media"]', // image in detail page for new twitter ].join(), ) + +export const timelinePostContentSelector = () => + querySelectorAll( + [ + '[data-testid="tweet"] div + div div[lang]', // text tweets + '[data-testid="tweet"] div + div div[data-testid="card.wrapper"]', // link box tweets + ].join(), + ) + export const postsContentSelector = () => querySelectorAll( [ diff --git a/packages/mask/src/social-network/types.ts b/packages/mask/src/social-network/types.ts index 6a98ad910db8..ab4172151655 100644 --- a/packages/mask/src/social-network/types.ts +++ b/packages/mask/src/social-network/types.ts @@ -292,6 +292,7 @@ export namespace SocialNetworkUI { } export namespace Configuration { export interface Define { + nextIDConfig?: NextIDConfig steganography?: SteganographyConfig setupWizard?: SetupWizardConfig } @@ -306,6 +307,11 @@ export namespace SocialNetworkUI { export interface SetupWizardConfig { disableSayHello?: boolean } + export interface NextIDConfig { + enable?: boolean + platform: string + collectVerificationPost: (keyword: string) => PostIdentifier | null + } } } diff --git a/packages/shared-base/src/NextID/type.ts b/packages/shared-base/src/NextID/type.ts new file mode 100644 index 000000000000..c80d44f9528d --- /dev/null +++ b/packages/shared-base/src/NextID/type.ts @@ -0,0 +1,29 @@ +export enum NextIDAction { + Create = 'create', + Delete = 'delete', +} + +export enum NextIDPlatform { + NextId = 'nextid', + Twitter = 'twitter', + Keybase = 'keybase', + Ethereum = 'ethereum', + GitHub = 'github', +} + +export interface NextIDPayload { + postContent: string + signPayload: string +} + +export interface NextIDPersonaBindings { + persona: string + proofs: { + platform: string + identity: string + }[] +} + +export interface NextIDBindings { + ids: NextIDPersonaBindings[] +} diff --git a/packages/shared-base/src/index.ts b/packages/shared-base/src/index.ts index f4038d746655..23ad9dc2a590 100644 --- a/packages/shared-base/src/index.ts +++ b/packages/shared-base/src/index.ts @@ -15,3 +15,4 @@ export { DashboardRoutes } from './Routes/DashboardRoutes' export * from './Messages/Mask' export * from './Results' export * from './convert' +export * from './NextID/type' diff --git a/packages/web3-providers/src/NextID/index.ts b/packages/web3-providers/src/NextID/index.ts new file mode 100644 index 000000000000..34a890ca439d --- /dev/null +++ b/packages/web3-providers/src/NextID/index.ts @@ -0,0 +1,106 @@ +import { fromHex, toBase64, NextIDBindings, NextIDPlatform, NextIDPayload, NextIDAction } from '@masknet/shared-base' +import urlcat from 'urlcat' +import { first } from 'lodash-unified' + +const BASE_URL = + process.env.channel === 'stable' && process.env.NODE_ENV === 'production' + ? 'https://proof-service.next.id/' + : 'https://js43x8ol17.execute-api.ap-east-1.amazonaws.com/api/' + +interface CreatePayloadBody { + action: string + platform: string + identity: string + public_key: string +} + +export async function bindProof( + personaPublicKey: string, + action: NextIDAction, + platform: string, + identity: string, + walletSignature?: string, + signature?: string, + proofLocation?: string, +) { + const requestBody = { + action, + platform, + identity, + public_key: personaPublicKey, + ...(proofLocation ? { proof_location: proofLocation } : {}), + extra: { + ...(walletSignature ? { wallet_signature: toBase64(fromHex(walletSignature)) } : {}), + ...(signature ? { signature: toBase64(fromHex(signature)) } : {}), + }, + } + + const response = await fetch(urlcat(BASE_URL, '/v1/proof'), { + body: JSON.stringify(requestBody), + method: 'POST', + mode: 'cors', + }) + + const result = (await response.json()) as { message: string } + + if (!response.ok) throw new Error(result.message) + return response +} + +export async function queryExistedBindingByPersona(personaPublicKey: string) { + const response = await fetch(urlcat(BASE_URL, '/v1/proof', { platform: 'nextid', identity: personaPublicKey }), { + mode: 'cors', + }) + + const result = (await response.json()) as NextIDBindings + // Will have only one item when query by personaPublicKey + return first(result.ids) +} + +export async function queryExistedBindingByPlatform(platform: NextIDPlatform, identity: string) { + if (!platform && !identity) return [] + + const response = await fetch(urlcat(BASE_URL, '/v1/proof', { platform: platform, identity: identity }), { + mode: 'cors', + }) + + const result = (await response.json()) as NextIDBindings + return result.ids +} + +export async function queryExistedBinding(platform: NextIDPlatform, identity: string): Promise { + throw new Error('To be implemented.') +} + +export async function queryIsBound(personaPublicKey: string, platform: NextIDPlatform, identity: string) { + if (!platform && !identity) return false + + const ids = await queryExistedBindingByPlatform(platform, identity) + return ids.some((x) => x.persona.toLowerCase() === personaPublicKey.toLowerCase()) +} + +export async function createPersonaPayload( + personaPublicKey: string, + action: NextIDAction, + identity: string, + platform: NextIDPlatform, +): Promise { + const requestBody: CreatePayloadBody = { + action, + platform, + identity, + public_key: personaPublicKey, + } + + const response = await fetch(urlcat(BASE_URL, '/v1/proof/payload'), { + body: JSON.stringify(requestBody), + method: 'POST', + mode: 'cors', + }) + + const result = await response.json() + return { + postContent: result.post_content, + signPayload: JSON.stringify(JSON.parse(result.sign_payload)), + } +} diff --git a/packages/web3-providers/src/index.ts b/packages/web3-providers/src/index.ts index c4e28fac5830..ce3bbf389730 100644 --- a/packages/web3-providers/src/index.ts +++ b/packages/web3-providers/src/index.ts @@ -11,6 +11,7 @@ import { TokenPriceAPI } from './token-price' export * from './types' export * from './opensea/utils' +export * from './NextID' export const OpenSea = new OpenSeaAPI() export const Rarible = new RaribleAPI()