Skip to content

Commit

Permalink
feat: NextID plugin (DimensionDev#5458)
Browse files Browse the repository at this point in the history
* feat: init next id plugin

* feat: persona sign and wallet sign happy path

* feat: ui

* fix: unused i18n

* feat: impl proof binding

* feat: impl proof unbinding

* feat: bound item ui

* feat: add loading skeleton

* refactor: extract panel componet

* style: copy button tooltip color

* fix: should send identity address when unbind

* feat: use snackbar to notify

* feat: should disable bind when not on evm

* feat: improve snackbar ui

* feat: should clear wallet sign when change account

* fix: use last recognized identity to bind

* feat: should show connect persona button when not active persona for current profile

* refactor: ui improve

* refactor: code review feedback

* fix: first tab in web3 tabs

* feat: style

* feat: update production url

* fix: wrong params to proof server

* Revert "fix: wrong params to proof server"

This reverts commit ea6538b.

* fix: use current persona

* feat: should query from kv sever when in other twitter user profile

* fix: cspell

* fix: eslint

* refactor: review feedback

* feat: remove kv in next id

* fix: handle | char in compress point

* fix: should retry when connect persona
  • Loading branch information
Lanttcat authored Jan 26, 2022
1 parent 55d81c2 commit 3f93566
Show file tree
Hide file tree
Showing 42 changed files with 1,393 additions and 164 deletions.
12 changes: 12 additions & 0 deletions .i18n-codegen.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,18 @@
"trans": "Translate",
"sourceMap": "inline"
}
},
{
"input": "./packages/mask/src/plugins/NextID/locales/en-US.json",
"output": "./packages/mask/src/plugins/NextID/locales/i18n_generated",
"parser": "i18next",
"generator": {
"type": "i18next/react-hooks",
"hooks": "useI18N",
"namespace": "com.mask.next_id",
"trans": "Translate",
"sourceMap": "inline"
}
}
]
}
6 changes: 2 additions & 4 deletions packages/mask/shared-ui/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -907,10 +907,6 @@
"plugin_pets_loot_info_6": "Amulet of the Twins",
"plugin_pets_loot_info_7": "Gold Ring",
"popups_following_permissions": "The plugin ({{pluginName}}) (hosted on {{pluginURL}}) is going to request the following permissions:",
"popups_choose_persona": "Which persona would you like to sign this message?",
"popups_request_sign": "requested to sign the following message with your persona:",
"popups_unknown_source": "Unknown Source",
"popups_sign_request": "Sign request:",
"popups_permissions": "Permissions",
"popups_sites": "Sites",
"popups_mask_requests_permission": "Mask needs the following permissions",
Expand Down Expand Up @@ -993,6 +989,8 @@
"popups_persona_logout": "Log out",
"popups_persona_disconnect_tip": "After logging out, your associated social accounts can no longer decrypt past encrypted messages. If you need to reuse your account, you can recover your account with your identity, private key, local or cloud backup.",
"popups_persona_persona_name_exists": "The persona name already exists",
"popups_persona_sign_request_title": "Signature request",
"popups_persona_sign_request_message": "message",
"popups_password_do_not_match": "Incorrect backup password",
"popups_backup_password": "Backup Password",
"popups_rename_error_tip": "Maximum length is {{length}} characters long.",
Expand Down
4 changes: 0 additions & 4 deletions packages/mask/shared-ui/locales/qya-AA.json
Original file line number Diff line number Diff line change
Expand Up @@ -889,10 +889,6 @@
"plugin_pets_loot_info_6": "crwdns9443:0crwdne9443:0",
"plugin_pets_loot_info_7": "crwdns9445:0crwdne9445:0",
"popups_following_permissions": "crwdns10271:0{{pluginName}}crwdnd10271:0{{pluginURL}}crwdne10271:0",
"popups_choose_persona": "crwdns10273:0crwdne10273:0",
"popups_request_sign": "crwdns10275:0crwdne10275:0",
"popups_unknown_source": "crwdns10277:0crwdne10277:0",
"popups_sign_request": "crwdns10279:0crwdne10279:0",
"popups_permissions": "crwdns10281:0crwdne10281:0",
"popups_sites": "crwdns10283:0crwdne10283:0",
"popups_mask_requests_permission": "crwdns10285:0crwdne10285:0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export function ProfileTabContent(props: ProfileTabContentProps) {
.flatMap((x) => x.ProfileTabs?.map((y) => ({ ...y, pluginID: x.ID })) ?? [])
.filter((z) => z.Utils?.shouldDisplay?.(identity, addressNames) ?? true)
.sort((a, z) => {
// order those tabs from next id first
if (a.pluginID === PluginId.NextID) return -1
if (z.pluginID === PluginId.NextID) return 1

// order those tabs from collectible first
if (a.pluginID === PluginId.Collectible) return -1
if (z.pluginID === PluginId.Collectible) return 1
Expand Down
49 changes: 26 additions & 23 deletions packages/mask/src/components/shared/WalletStatusBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ const useStyles = makeStyles<{ isDashboard: boolean }>()((theme, { isDashboard }
}))
interface WalletStatusBox {
isDashboard?: boolean
disableChange?: boolean
}
export function WalletStatusBox(props: WalletStatusBox) {
const { t } = useI18N()
Expand Down Expand Up @@ -201,30 +202,32 @@ export function WalletStatusBox(props: WalletStatusBox) {
</Link>
</div>
</div>
<section>
{providerType === ProviderType.WalletConnect || providerType === ProviderType.Fortmatic ? (
<ActionButtonPromise
className={classes.actionButton}
color="primary"
size="small"
{!props.disableChange && (
<section>
{providerType === ProviderType.WalletConnect || providerType === ProviderType.Fortmatic ? (
<ActionButtonPromise
className={classes.actionButton}
color="primary"
size="small"
variant="contained"
init={t('wallet_status_button_disconnect')}
waiting={t('wallet_status_button_disconnecting')}
failed={t('failed')}
complete={t('done')}
executor={onDisconnect}
completeIcon={<></>}
failIcon={<></>}
/>
) : null}
<Button
className={classNames(classes.actionButton)}
variant="contained"
init={t('wallet_status_button_disconnect')}
waiting={t('wallet_status_button_disconnecting')}
failed={t('failed')}
complete={t('done')}
executor={onDisconnect}
completeIcon={<></>}
failIcon={<></>}
/>
) : null}
<Button
className={classNames(classes.actionButton)}
variant="contained"
size="small"
onClick={onChange}>
{t('wallet_status_button_change')}
</Button>
</section>
size="small"
onClick={onChange}>
{t('wallet_status_button_change')}
</Button>
</section>
)}
</section>
) : (
<section className={classes.connectButtonWrapper}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,8 @@ export async function INTERNAL_send(
// that would almost always causes an undesired warning tip.
if (Flags.EIP1559_enabled && isEIP1559Valid && isEIP1559Supported(chainIdFinally)) {
config.gasPrice = undefined
config.maxPriorityFeePerGas = formatGweiToWei(1.5).toString(16)
config.maxFeePerGas = (Number.parseInt(config.maxFeePerGas!, 16) * 0.8).toString(16)
config.maxPriorityFeePerGas = toHex(formatGweiToWei(1.5).toFixed())
config.maxFeePerGas = toHex(Number.parseInt(config.maxFeePerGas!, 16) * 0.8)
} else {
config.maxFeePerGas = undefined
config.maxPriorityFeePerGas = undefined
Expand Down Expand Up @@ -405,9 +405,7 @@ export async function INTERNAL_send(
const [hash] = payload.params as [string]

// redirect receipt queries to tx watcher
const transaction = await WalletRPC.getRecentTransaction(chainIdFinally, account, hash, {
receipt: true,
})
const transaction = await WalletRPC.getRecentTransaction(chainIdFinally, account, hash)

try {
callback(null, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export { __deprecated__getStorage, __deprecated__setStorage } from './storage'
export { resolveTCOLink } from '../../../../shared'
export { fetch, fetchJSON } from '../../../../background/services/helper/fetch'
export { requestExtensionPermission, queryExtensionPermission } from './extensionPermission'
export { createPersonaPayload, queryExistedBinding, bindProof } from './nextId'
export { fromHex, toBase64URL } from '@masknet/shared-base'

export async function openPopupWindow(route?: PopupRoutes, params?: Record<string, any>) {
const windows = await browser.windows.getAll()
Expand Down
119 changes: 119 additions & 0 deletions packages/mask/src/extension/background-script/HelperService/nextId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import {
toHex,
PersonaIdentifier,
compressSecp256k1Point,
fromHex,
toBase64,
decompressSecp256k1Key,
} 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 QueryBinding {
platform: string
identity: string
}

interface QueryBindingIDResponse {
persona: string
proofs: {
platform: string
identity: string
}[]
}

interface QueryBindingResponse {
ids: QueryBindingIDResponse[]
}

interface CreatePayloadBody {
action: string
platform: string
identity: string
public_key: string
}

interface PayloadResponse {
post_content: string
sign_payload: string
}

export async function bindProof(
persona: PersonaIdentifier,
action: 'create' | 'delete',
platform: string,
identity: string,
walletSignature?: string,
signature?: string,
) {
const publicKey = await queryPersonaHexPublicKey(persona)
if (!publicKey) return

const requestBody = {
action,
platform,
identity,
public_key: publicKey,
extra: {
...(walletSignature ? { wallet_signature: toBase64(fromHex(walletSignature)) } : {}),
...(signature ? { signature: toBase64(fromHex(signature)) } : {}),
},
}

return fetch(urlcat(BASE_URL, '/v1/proof'), {
body: JSON.stringify(requestBody),
method: 'POST',
mode: 'cors',
})
}

async function queryPersonaHexPublicKey(persona: PersonaIdentifier) {
const key256 = decompressSecp256k1Key(persona.compressedPoint.replace(/\|/g, '/'))
if (!key256.x || !key256.y) return null
const arr = compressSecp256k1Point(key256.x, key256.y)

return `0x${toHex(arr)}`
}

export async function queryExistedBinding(persona: PersonaIdentifier) {
const publicKey = await queryPersonaHexPublicKey(persona)
if (!publicKey) return

const response = await fetch(urlcat(BASE_URL, '/v1/proof', { platform: 'nextid', identity: publicKey }), {
mode: 'cors',
})

const result = (await response.json()) as QueryBindingResponse
return first(result.ids)
}

export async function createPersonaPayload(
persona: PersonaIdentifier,
action: 'create' | 'delete',
identity: string,
platform: string,
) {
const publicKey = await queryPersonaHexPublicKey(persona)
if (!publicKey) return

const requestBody: CreatePayloadBody = {
action,
platform,
identity,
public_key: publicKey,
}

const response = await fetch(urlcat(BASE_URL, '/v1/proof/payload'), {
body: JSON.stringify(requestBody),
method: 'POST',
mode: 'cors',
})

const result: PayloadResponse = await response.json()
return JSON.stringify(JSON.parse(result.sign_payload))
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ assertEnvironment(Environment.ManifestBackground)
export { validateMnemonic } from '../../utils/mnemonic-code'

// #region Profile
export { queryProfile, queryProfilePaged } from '../../database'
export { queryProfile, queryProfilePaged, queryPersonaByProfile } from '../../database'

export function queryProfiles(network?: string): Promise<Profile[]> {
return queryProfilesWithQuery({ network })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import {
ECDSASignature,
} from 'ethereumjs-util'
import { MaskMessages } from '../../../utils'
import { constructSignRequestURL } from '../../popups'
import { delay, PersonaIdentifier, fromBase64URL } from '@masknet/shared-base'
import { delay, PersonaIdentifier, fromBase64URL, PopupRoutes } from '@masknet/shared-base'
import { queryPersonasWithPrivateKey } from '../../../../background/database/persona/db'
import { openPopupWindow } from '../HelperService'
export interface SignRequest {
/** Use that who to sign this message. */
identifier?: string
/** The message to be signed. */
message: string
/** Use what method to sign this message? */
Expand All @@ -31,33 +33,18 @@ export interface SignRequestResult {
/** Message in hex */
messageHex: string
}
export async function signWithPersona({ message, method }: SignRequest): Promise<SignRequestResult> {

export async function signWithPersona({ message, method, identifier }: SignRequest): Promise<SignRequestResult> {
if (method !== 'eth') throw new Error('Unknown sign method')
const requestID = Math.random().toString(16).slice(3)
const newWindow = await browser.windows.create({
height: 600,
width: 400,
type: 'popup',
url: constructSignRequestURL({ message, requestID }),
})
await openPopupWindow(PopupRoutes.PersonaSignRequest, { message, requestID, identifier })

const waitForApprove = new Promise<PersonaIdentifier>((resolve, reject) => {
const listener = (tabID: number) => {
if (newWindow.tabs?.[0].id === tabID) reject(new Error('Sign rejected'))
}
browser.tabs.onRemoved.addListener(listener)
// reject this request after 3 mins
delay(1000 * 60 * 3).then(() => reject(new Error('Timeout')))
const removeListener = MaskMessages.events.signRequestApproved.on((approval) => {
delay(1000 * 60).then(() => reject(new Error('Timeout')))
MaskMessages.events.personaSignRequest.on((approval) => {
if (approval.requestID !== requestID) return
resolve(approval.selectedPersona)
})
setTimeout(() => {
waitForApprove.finally(() => {
browser.tabs.onRemoved.removeListener(listener)
removeListener()
browser.windows.remove(newWindow.id!).catch(() => {})
reject(new Error('Sign rejected'))
})
if (!approval.selectedPersona) reject(new Error('Persona Rejected'))
resolve(approval.selectedPersona!)
})
})
const signer = await waitForApprove
Expand Down
Loading

0 comments on commit 3f93566

Please sign in to comment.