diff --git a/packages/dashboard/src/pages/CreateMaskWallet/components/CreateMnemonic/index.tsx b/packages/dashboard/src/pages/CreateMaskWallet/components/CreateMnemonic/index.tsx index d672c8b0df22..4ec5787969e6 100644 --- a/packages/dashboard/src/pages/CreateMaskWallet/components/CreateMnemonic/index.tsx +++ b/packages/dashboard/src/pages/CreateMaskWallet/components/CreateMnemonic/index.tsx @@ -117,7 +117,7 @@ const CreateMnemonic = memo(() => { await PluginServices.Wallet.updateMaskAccount({ account: address_, }) - await PluginServices.Wallet.selectMaskAccount([address_]) + await PluginServices.Wallet.resolveMaskAccount([address_]) } return address_ diff --git a/packages/mask/public/patches.js b/packages/mask/public/patches.js index 4e3f6866390a..921d13f28584 100644 --- a/packages/mask/public/patches.js +++ b/packages/mask/public/patches.js @@ -89,6 +89,8 @@ globalThis.regeneratorRuntime = undefined console.trace('[Trusted Types](default policy): Possible XSS happened. Please remove it.', string) return string }, + createScriptURL: (string) => string, + createScript: (string) => string, }) } } diff --git a/packages/mask/src/extension/popups/pages/Wallet/ContractInteraction/index.tsx b/packages/mask/src/extension/popups/pages/Wallet/ContractInteraction/index.tsx index 133fb3bc6204..a2e57dcac062 100644 --- a/packages/mask/src/extension/popups/pages/Wallet/ContractInteraction/index.tsx +++ b/packages/mask/src/extension/popups/pages/Wallet/ContractInteraction/index.tsx @@ -39,6 +39,7 @@ import { } from '@masknet/web3-shared-base' import { CopyIconButton } from '../../../components/CopyIconButton' import { useTitle } from '../../../hook/useTitle' +import { WalletRPC } from '../../../../../plugins/Wallet/messages' const useStyles = makeStyles()(() => ({ container: { @@ -266,7 +267,7 @@ const ContractInteraction = memo(() => { const [{ loading }, handleConfirm] = useAsyncFn(async () => { if (request) { try { - await connection?.confirmRequest?.() + await WalletRPC.confirmRequest(request.payload) navigate(-1) } catch (error_) { setTransferError(true) @@ -277,7 +278,7 @@ const ContractInteraction = memo(() => { const [{ loading: rejectLoading }, handleReject] = useAsyncFn(async () => { if (!request) return - await connection?.rejectRequest?.() + await WalletRPC.rejectRequest(request.payload) navigate(PopupRoutes.Wallet, { replace: true }) }, [request]) @@ -311,21 +312,6 @@ const ContractInteraction = memo(() => { .plus(tokenValueUSD) .toString() - console.log('DEBUG: ContractInteraction') - console.log({ - amount, - gasFee, - gas, - maxPriorityFeePerGas: maxPriorityFeePerGas ?? defaultPrices?.maxPriorityFeePerGas, - maxFeePerGas: maxFeePerGas ?? defaultPrices?.maxFeePerGas, - defaultPrice: (gasPrice as string) ?? defaultPrices?.gasPrice, - request, - tokenPrice, - tokenAmount, - tokenDecimals, - nativeTokenPrice, - }) - useUpdateEffect(() => { if (!request && !requestLoading) { navigate(PopupRoutes.Wallet, { replace: true }) diff --git a/packages/mask/src/extension/popups/pages/Wallet/ImportWallet/index.tsx b/packages/mask/src/extension/popups/pages/Wallet/ImportWallet/index.tsx index d226ae090c32..6f219928eb98 100644 --- a/packages/mask/src/extension/popups/pages/Wallet/ImportWallet/index.tsx +++ b/packages/mask/src/extension/popups/pages/Wallet/ImportWallet/index.tsx @@ -177,7 +177,7 @@ const ImportWallet = memo(() => { account: wallet, }) } - await WalletRPC.selectMaskAccount([wallet]) + await WalletRPC.resolveMaskAccount([wallet]) navigate(PopupRoutes.Wallet, { replace: true }) await Services.Helper.removePopupWindow() break @@ -186,11 +186,12 @@ const ImportWallet = memo(() => { await WalletRPC.updateMaskAccount({ account: privateKeyWallet, }) - await WalletRPC.selectMaskAccount([privateKeyWallet]) + await WalletRPC.resolveMaskAccount([privateKeyWallet]) await connection?.connect({ account: privateKeyWallet, providerType: ProviderType.MaskWallet, + popupsWindow: false, }) await Services.Helper.removePopupWindow() diff --git a/packages/mask/src/extension/popups/pages/Wallet/SelectWallet/index.tsx b/packages/mask/src/extension/popups/pages/Wallet/SelectWallet/index.tsx index 62bc44b10dfa..53525e8e2e0e 100644 --- a/packages/mask/src/extension/popups/pages/Wallet/SelectWallet/index.tsx +++ b/packages/mask/src/extension/popups/pages/Wallet/SelectWallet/index.tsx @@ -120,7 +120,7 @@ const SelectWallet = memo(() => { if (isPopup) { navigate(-1) } else { - await WalletRPC.selectMaskAccount([]) + await WalletRPC.resolveMaskAccount([]) await Services.Helper.removePopupWindow() } }, [isPopup]) @@ -142,7 +142,7 @@ const SelectWallet = memo(() => { account: selected, }) if (chainId) { - await WalletRPC.selectMaskAccount([selected]) + await WalletRPC.resolveMaskAccount([selected]) } return Services.Helper.removePopupWindow() }, [chainId, selected, isPopup]) diff --git a/packages/mask/src/extension/popups/pages/Wallet/SignRequest/index.tsx b/packages/mask/src/extension/popups/pages/Wallet/SignRequest/index.tsx index 86022d44d015..c7da1f333ef0 100644 --- a/packages/mask/src/extension/popups/pages/Wallet/SignRequest/index.tsx +++ b/packages/mask/src/extension/popups/pages/Wallet/SignRequest/index.tsx @@ -12,6 +12,7 @@ import { PopupRoutes } from '@masknet/shared-base' import { useTitle } from '../../../hook/useTitle' import { EthereumMethodType } from '@masknet/web3-shared-evm' import { NetworkPluginID } from '@masknet/web3-shared-base' +import { WalletRPC } from '../../../../../plugins/Wallet/messages' const useStyles = makeStyles()(() => ({ container: { @@ -117,9 +118,13 @@ const SignRequest = memo(() => { }, [value]) const [{ loading }, handleConfirm] = useAsyncFn(async () => { - if (value && connection?.confirmRequest) { + const goBack = new URLSearchParams(routeLocation.search).get('goBack') + + if (value) { try { - await connection.confirmRequest() + await WalletRPC.confirmRequest(value.payload, { + disableClose: !!goBack, + }) navigate(-1) } catch (error_) { setTransferError(true) @@ -128,8 +133,8 @@ const SignRequest = memo(() => { }, [value, routeLocation.search, connection]) const [{ loading: rejectLoading }, handleReject] = useAsyncFn(async () => { - if (!value || !connection?.rejectRequest) return - await connection.rejectRequest() + if (!value) return + await WalletRPC.rejectRequest(value.payload) navigate(PopupRoutes.Wallet, { replace: true }) }, [value, connection]) diff --git a/packages/mask/src/extension/popups/pages/Wallet/WalletRecovery/index.tsx b/packages/mask/src/extension/popups/pages/Wallet/WalletRecovery/index.tsx index e8603e22e2e5..addeeb6e4478 100644 --- a/packages/mask/src/extension/popups/pages/Wallet/WalletRecovery/index.tsx +++ b/packages/mask/src/extension/popups/pages/Wallet/WalletRecovery/index.tsx @@ -2,9 +2,9 @@ import { memo } from 'react' import { makeStyles } from '@masknet/theme' import { PageHeader } from '../components/PageHeader' import { MaskMessages, useI18N } from '../../../../../utils' -import { useLocation } from 'react-router-dom' +import { useLocation, useNavigate } from 'react-router-dom' import { useAsync, useAsyncFn } from 'react-use' -import { ChainId, formatEthereumAddress, ProviderType } from '@masknet/web3-shared-evm' +import { formatEthereumAddress } from '@masknet/web3-shared-evm' import Services from '../../../../service' import { LoadingPlaceholder } from '../../../components/LoadingPlaceholder' import { Typography } from '@mui/material' @@ -78,6 +78,7 @@ const WalletRecovery = memo(() => { const { t } = useI18N() const { classes } = useStyles() const location = useLocation() + const navigate = useNavigate() const web3State = useWeb3State(NetworkPluginID.PLUGIN_EVM) @@ -127,7 +128,8 @@ const WalletRecovery = memo(() => { await Services.Backup.restoreUnconfirmedBackup({ id: backupId, action: 'confirm' }) // Set default wallet - if (json.wallets) await web3State.Provider?.connect(ChainId.Mainnet, ProviderType.MaskWallet) + if (json.wallets) await WalletRPC.setDefaultMaskAccount() + // Send event after successful recovery MaskMessages.events.restoreSuccess.sendToAll(undefined) diff --git a/packages/mask/src/plugin-infra/host.ts b/packages/mask/src/plugin-infra/host.ts index 8b37170ba81b..a39e56aac91d 100644 --- a/packages/mask/src/plugin-infra/host.ts +++ b/packages/mask/src/plugin-infra/host.ts @@ -1,7 +1,5 @@ // All plugin manager need to call createPluginHost so let's register plugins implicitly. import './register' - -import type { JsonRpcPayload, JsonRpcResponse } from 'web3-core-helpers' import type { Plugin } from '@masknet/plugin-infra' import { Emitter } from '@servie/events' import { MaskMessages } from '../../shared/messages' @@ -28,15 +26,7 @@ export function createSharedContext(pluginID: string, signal: AbortSignal): Plug nativeType: nativeAPI?.type, hasNativeAPI, - send: async (payload: JsonRpcPayload) => { - if (nativeAPI?.type === 'iOS') { - return nativeAPI.api.send(payload) as unknown as JsonRpcResponse - } else { - const response = await nativeAPI?.api.sendJsonString(JSON.stringify(payload)) - if (!response) throw new Error('Failed to send request to native APP.') - return JSON.parse(response) as JsonRpcResponse - } - }, + send: WalletRPC.sendPayload, openPopupWindow: Services.Helper.openPopupWindow, closePopupWindow: Services.Helper.removePopupWindow, @@ -66,18 +56,17 @@ export function createSharedContext(pluginID: string, signal: AbortSignal): Plug updateAccount: WalletRPC.updateMaskAccount, resetAccount: WalletRPC.resetMaskAccount, - selectAccountPrepare: WalletRPC.selectMaskAccountPrepare, + selectAccount: WalletRPC.selectMaskAccount, signTransaction: WalletRPC.signTransaction, signTypedData: WalletRPC.signTypedData, signPersonalMessage: WalletRPC.signPersonalMessage, + getWallets: WalletRPC.getWallets, + getWalletPrimary: WalletRPC.getWalletPrimary, addWallet: WalletRPC.updateWallet, updateWallet: WalletRPC.updateWallet, removeWallet: WalletRPC.removeWallet, - - shiftUnconfirmedRequest: WalletRPC.shiftUnconfirmedRequest, - pushUnconfirmedRequest: WalletRPC.pushUnconfirmedRequest, } } diff --git a/packages/mask/src/plugins/Wallet/services/account.ts b/packages/mask/src/plugins/Wallet/services/account.ts index ef298d56ff6e..a0e634ff9535 100644 --- a/packages/mask/src/plugins/Wallet/services/account.ts +++ b/packages/mask/src/plugins/Wallet/services/account.ts @@ -1,3 +1,4 @@ +import { first } from 'lodash-unified' import { EthereumAddress } from 'wallet.ts' import { ChainId, chainResolver, networkResolver, NetworkType } from '@masknet/web3-shared-evm' import { @@ -6,6 +7,18 @@ import { currentMaskWalletNetworkSettings, } from '../settings' import { Flags } from '../../../../shared' +import { WalletRPC } from '../messages' +import { defer, DeferTuple } from '@dimensiondev/kit' + +export async function setDefaultMaskAccount() { + if (currentMaskWalletAccountSettings.value) return + const wallets = await WalletRPC.getWallets() + const address = first(wallets)?.address + if (address) + await updateMaskAccount({ + account: address, + }) +} export async function updateMaskAccount(options: { account?: string; chainId?: ChainId; networkType?: NetworkType }) { if (options.chainId && !options.networkType) options.networkType = chainResolver.chainNetworkType(options.chainId) @@ -24,14 +37,20 @@ export async function resetMaskAccount() { } // #region select wallet with popups -let callbackMemorized: (accounts: string[]) => void | undefined +const deferred: DeferTuple = defer() + +export async function selectMaskAccount(): Promise { + return deferred[0] +} -export async function selectMaskAccountPrepare(callback: (accounts: string[]) => void) { - callbackMemorized = callback +export async function resolveMaskAccount(accounts: string[]) { + const [, resolve] = deferred + resolve?.(accounts) } -export async function selectMaskAccount(accounts: string[]) { - callbackMemorized?.(accounts) +export async function rejectMaskAccount() { + const [, resolve] = deferred + resolve?.([]) } // #endregion diff --git a/packages/mask/src/plugins/Wallet/services/index.ts b/packages/mask/src/plugins/Wallet/services/index.ts index 8de0d72d3c63..58817eeea3f6 100644 --- a/packages/mask/src/plugins/Wallet/services/index.ts +++ b/packages/mask/src/plugins/Wallet/services/index.ts @@ -3,3 +3,4 @@ export * from './keystore' export * from './wallet/index' export * from './legacyWallet' export * from './rpc' +export * from './send' diff --git a/packages/mask/src/plugins/Wallet/services/maskwallet/index.ts b/packages/mask/src/plugins/Wallet/services/maskwallet/index.ts new file mode 100644 index 000000000000..34d3bcb077a0 --- /dev/null +++ b/packages/mask/src/plugins/Wallet/services/maskwallet/index.ts @@ -0,0 +1,88 @@ +import type { MaskBaseAPI } from '@masknet/web3-providers' +import { api } from '@dimensiondev/mask-wallet-core/proto' +import { OnDemandWorker } from '@masknet/shared-base' + +type Request = InstanceType +type Response = InstanceType + +const Worker = new OnDemandWorker(new URL('../../../../../web-workers/wallet.ts', import.meta.url), { + name: 'MaskWallet', +}) + +enum ErrorCode { + KdfParamsInvalid = '-3001', + PasswordIncorrect = '-3002', + InvalidKeyIvLength = '-3003', + InvalidCiphertext = '-3004', + InvalidPrivateKey = '-3005', + InvalidPublicKey = '-3006', + InvalidMnemonic = '-3007', + InvalidSeed = '-3008', + InvalidDerivationPath = '-3009', + InvalidKeyStoreJSON = '-3010', + NotSupportedPublicKeyType = '-3011', + NotSupportedCurve = '-3012', + NotSupportedCipher = '-3013', +} + +const ErrorMessage = { + [ErrorCode.KdfParamsInvalid]: 'Invalid kdf parameters.', + [ErrorCode.PasswordIncorrect]: 'Incorrect payment password.', + [ErrorCode.InvalidKeyIvLength]: 'Invalid key IV length.', + [ErrorCode.InvalidCiphertext]: 'Invalid cipher text.', + [ErrorCode.InvalidPrivateKey]: 'Invalid private key.', + [ErrorCode.InvalidPublicKey]: 'Invalid public key.', + [ErrorCode.InvalidMnemonic]: 'Invalid mnemonic words.', + [ErrorCode.InvalidSeed]: 'Invalid seed.', + [ErrorCode.InvalidDerivationPath]: 'Invalid derivation path.', + [ErrorCode.InvalidKeyStoreJSON]: 'Invalid keystore JSON.', + [ErrorCode.NotSupportedPublicKeyType]: 'Not supported public key type.', + [ErrorCode.NotSupportedCurve]: 'Not supported curve.', + [ErrorCode.NotSupportedCipher]: 'Not supported cipher.', +} + +function send(input: I, output: O) { + return (value: Request[I]) => { + return new Promise((resolve, reject) => { + const req: MaskBaseAPI.Input = { id: Math.random(), data: { [input]: value } } + Worker.postMessage(req) + Worker.addEventListener('message', function f(message) { + if (message.data.id !== req.id) return + + Worker.removeEventListener('message', f) + const data: MaskBaseAPI.Output = message.data + if (data.response.error) + return reject( + new Error(ErrorMessage[data.response.error.errorCode as ErrorCode] || 'Unknown Error'), + ) + resolve(data.response[output]) + }) + }) + } +} + +export const Coin = api.Coin +export const StoredKeyType = api.StoredKeyType +export const StoredKeyImportType = api.StoredKeyImportType +export const StoredKeyExportType = api.StoredKeyExportType + +export const loadStoredKey = send('param_load_stored_key', 'resp_load_stored_key') +export const createStoredKey = send('param_create_stored_key', 'resp_create_stored_key') +export const importPrivateKey = send('param_import_private_key', 'resp_import_private_key') +export const importMnemonic = send('param_import_mnemonic', 'resp_import_mnemonic') +export const importJSON = send('param_import_json', 'resp_import_json') +export const createAccountOfCoinAtPath = send( + 'param_create_account_of_coin_at_path', + 'resp_create_account_of_coin_at_path', +) +export const exportPrivateKey = send('param_export_private_key', 'resp_export_private_key') +export const exportPrivateKeyOfPath = send('param_export_private_key_of_path', 'resp_export_private_key') +export const exportMnemonic = send('param_export_mnemonic', 'resp_export_mnemonic') +export const exportKeyStoreJSONOfAddress = send('param_export_key_store_json_of_address', 'resp_export_key_store_json') +export const exportKeyStoreJSONOfPath = send('param_export_key_store_json_of_path', 'resp_export_key_store_json') +export const exportUpdateKeyStorePassword = send('param_update_key_store_password', 'resp_update_key_store_password') +export const signTransaction = send('param_sign_transaction', 'resp_sign_transaction') +export const getLibVersion = send('param_get_version', 'resp_get_version') +export const validate = send('param_validation', 'resp_validate') +export const getSupportImportTypes = send('param_get_stored_key_import_type', 'resp_get_stored_key_import_type') +export const getSupportExportTypes = send('param_get_stored_key_export_type', 'resp_get_stored_key_export_type') diff --git a/packages/mask/src/plugins/Wallet/services/send.ts b/packages/mask/src/plugins/Wallet/services/send.ts new file mode 100644 index 000000000000..b5c12257c68d --- /dev/null +++ b/packages/mask/src/plugins/Wallet/services/send.ts @@ -0,0 +1,150 @@ +import Web3 from 'web3' +import type { HttpProvider } from 'web3-core' +import type { JsonRpcPayload, JsonRpcResponse } from 'web3-core-helpers' +import { defer } from '@dimensiondev/kit' +import { ChainId, EthereumMethodType, getPayloadId, getRPCConstants } from '@masknet/web3-shared-evm' +import { openPopupWindow, removePopupWindow } from '../../../../background/services/helper' +import { nativeAPI } from '../../../../shared/native-rpc' +import { WalletRPC } from '../messages' + +type Options = { + account?: string + chainId?: ChainId + disableClose?: boolean + popupsWindow?: boolean +} + +const UNCONFIRMED_CALLBACK_MAP = new Map void>() +const RISK_METHOD_LIST = [ + EthereumMethodType.ETH_SIGN, + EthereumMethodType.PERSONAL_SIGN, + EthereumMethodType.ETH_SIGN_TYPED_DATA, + EthereumMethodType.ETH_DECRYPT, + EthereumMethodType.ETH_GET_ENCRYPTION_PUBLIC_KEY, + EthereumMethodType.ETH_SEND_TRANSACTION, +] + +function isRiskMethod(method: EthereumMethodType) { + return RISK_METHOD_LIST.includes(method) +} + +let id = 0 +const { RPC_WEIGHTS = [] } = getRPCConstants(ChainId.Mainnet) +const seed = Math.floor(Math.random() * RPC_WEIGHTS.length) +const providerPool = new Map() + +function createProviderInstance(url: string) { + const instance = providerPool.get(url) + if (instance) return instance + + const newInstance = new Web3.providers.HttpProvider(url, { + timeout: 30 * 1000, // ms + // @ts-ignore + clientConfig: { + keepalive: true, + keepaliveInterval: 1, // ms + }, + reconnect: { + auto: true, + delay: 5000, // ms + maxAttempts: Number.MAX_SAFE_INTEGER, + onTimeout: true, + }, + }) + providerPool.set(url, newInstance) + return newInstance +} + +async function createProvider(chainId = ChainId.Mainnet) { + const { RPC_URLS = [], RPC_WEIGHTS = [] } = getRPCConstants(chainId) + const url = RPC_URLS[RPC_WEIGHTS[seed]] + if (!url) throw new Error('Failed to create provider.') + return createProviderInstance(url) +} + +/** + * Send to built-in RPC endpoints. + */ +export async function send( + payload: JsonRpcPayload, + callback: (error: Error | null, response?: JsonRpcResponse) => void, + options?: Options, +) { + const provider = await createProvider(options?.chainId) + return provider.send(payload, callback) +} + +/** + * The entrance of all RPC requests to MaskWallet. + */ +export async function sendPayload(payload: JsonRpcPayload, options?: Options) { + if (nativeAPI?.type === 'iOS') { + return nativeAPI.api.send(payload) as unknown as JsonRpcResponse + } else if (nativeAPI?.type === 'Android') { + const response = await nativeAPI?.api.sendJsonString(JSON.stringify(payload)) + if (!response) throw new Error('Failed to send request to native APP.') + return JSON.parse(response) as JsonRpcResponse + } else { + return new Promise(async (resolve, reject) => { + const callback = (error: Error | null, response?: JsonRpcResponse) => { + if (error) reject(error) + else if (response) resolve(response) + } + + id += 1 + + const payload_ = { + ...payload, + id, + } + + if (isRiskMethod(payload_.method as EthereumMethodType)) { + await WalletRPC.pushUnconfirmedRequest(payload_) + UNCONFIRMED_CALLBACK_MAP.set(payload_.id, callback) + if (options?.popupsWindow) openPopupWindow() + return + } + + send(payload, callback, options) + }) + } +} + +export async function confirmRequest(payload: JsonRpcPayload, options?: Options) { + const pid = getPayloadId(payload) + if (!pid) return + const [deferred, resolve, reject] = defer() + send( + payload, + (error, response) => { + UNCONFIRMED_CALLBACK_MAP.get(pid)?.(error, response) + if (error) { + reject(error) + return + } + if (response?.error) { + reject(new Error(`Failed to send transaction: ${response.error}`)) + return + } + WalletRPC.deleteUnconfirmedRequest(payload) + .then(() => { + if (!options?.disableClose) removePopupWindow() + }) + .finally(() => { + UNCONFIRMED_CALLBACK_MAP.delete(pid) + }) + resolve(response) + }, + options, + ) + return deferred +} + +export async function rejectRequest(payload: JsonRpcPayload) { + const pid = getPayloadId(payload) + if (!pid) return + UNCONFIRMED_CALLBACK_MAP.get(pid)?.(new Error('User rejected transaction.')) + await WalletRPC.deleteUnconfirmedRequest(payload) + await removePopupWindow() + UNCONFIRMED_CALLBACK_MAP.delete(pid) +} diff --git a/packages/mask/src/plugins/Wallet/services/wallet/index.ts b/packages/mask/src/plugins/Wallet/services/wallet/index.ts index 1ef2f4c32890..f72bf72fa1e6 100644 --- a/packages/mask/src/plugins/Wallet/services/wallet/index.ts +++ b/packages/mask/src/plugins/Wallet/services/wallet/index.ts @@ -6,11 +6,11 @@ import { personalSign, signTypedData as signTypedData_, SignTypedDataVersion } f import { encodeText } from '@dimensiondev/kit' import { isSameAddress } from '@masknet/web3-shared-base' import type { Transaction } from '@masknet/web3-shared-evm' -import { Mask } from '@masknet/web3-providers' import { api } from '@dimensiondev/mask-wallet-core/proto' import { MAX_DERIVE_COUNT, HD_PATH_WITHOUT_INDEX_ETHEREUM } from '@masknet/plugin-wallet' import * as database from './database' import * as password from './password' +import * as Mask from '../maskwallet' import { hasNativeAPI } from '../../../../../shared/native-rpc' import type { WalletRecord } from './type' diff --git a/packages/mask/web-workers/wallet.ts b/packages/mask/web-workers/wallet.ts new file mode 100644 index 000000000000..b736c7772f4c --- /dev/null +++ b/packages/mask/web-workers/wallet.ts @@ -0,0 +1,38 @@ +import './prepare' +import type { api } from '@dimensiondev/mask-wallet-core/proto' +// How to interact with this worker: +export type Input = { id: number; data: api.IMWRequest } +export type Output = { id: number; response: api.MWResponse } + +async function load() { + if (process.env.manifest === '3') { + return import('@dimensiondev/mask-wallet-core/bundle') + } else { + const { default: init, ...rest } = await import('@dimensiondev/mask-wallet-core/web') + // @ts-expect-error + await init() + return rest + } +} +const promise = (async () => { + const { request } = await load() + const { api } = await import('@dimensiondev/mask-wallet-core/proto') + return { request, api } +})() + +self.addEventListener('message', async (ev: MessageEvent) => { + const { api, request } = await promise + + const { id, data } = ev.data as Input + if (!id) return + + try { + const payload = api.MWRequest.encode(data).finish() + const wasmResult = request(payload) + const response = api.MWResponse.decode(wasmResult) + self.postMessage({ id, response }) + } catch (error) { + const out: Output = { id, response: { error: { errorMsg: String(error) } } } + self.postMessage(out) + } +}) diff --git a/packages/plugin-infra/src/types.ts b/packages/plugin-infra/src/types.ts index 5ab8c4eb1e2d..39bf61a1ef17 100644 --- a/packages/plugin-infra/src/types.ts +++ b/packages/plugin-infra/src/types.ts @@ -13,7 +13,7 @@ import type { Wallet, Web3EnableRequirement, } from '@masknet/web3-shared-base' -import type { SchemaType, Transaction } from '@masknet/web3-shared-evm' +import type { ChainId, SchemaType, Transaction } from '@masknet/web3-shared-evm' import type { Emitter } from '@servie/events' import type { Web3Plugin } from './web3-types' import type { Subscription } from 'use-subscription' @@ -141,13 +141,22 @@ export namespace Plugin.Shared { /** Native API supported */ hasNativeAPI: boolean /** Send request to native API */ - send(payload: JsonRpcPayload): Promise + send( + payload: JsonRpcPayload, + options?: { + account?: string + chainId?: ChainId + popupsWindow?: boolean + }, + ): Promise /** Open popup window */ openPopupWindow(route?: PopupRoutes, params?: Record): Promise /** Close popup window */ closePopupWindow(): Promise + /** Select a Mask Wallet account */ + selectAccount(): Promise /** Update Mask Wallet account */ updateAccount(account: { account?: string @@ -157,8 +166,6 @@ export namespace Plugin.Shared { }): Promise /** Reset Mask Wallet account */ resetAccount(): Promise - /** Prepare to select a Mask Wallet account */ - selectAccountPrepare(callback: (accounts: string[]) => void): Promise /** Sign a message with persona */ personaSignMessage(payload: PersonaSignRequest): Promise @@ -170,17 +177,16 @@ export namespace Plugin.Shared { /** Sign typed data */ signTypedData(address: string, message: string): Promise + /** Get all wallets */ + getWallets(): Promise + /** Get the primary wallet */ + getWalletPrimary(): Promise /** Add a new wallet */ addWallet(id: string, wallet: Wallet): Promise /** Update a wallet */ updateWallet(id: string, wallet: Partial): Promise /** Remove a old wallet */ removeWallet(id: string, password?: string): Promise - - /** get the latest unconfirmed request */ - shiftUnconfirmedRequest(): Promise - /** add an unconfirmed request */ - pushUnconfirmedRequest(payload: JsonRpcPayload): Promise } export interface Definition { /** diff --git a/packages/plugin-infra/src/web3-state/Wallet.ts b/packages/plugin-infra/src/web3-state/Wallet.ts index 37719f7ab8e0..1bde2f01d7a2 100644 --- a/packages/plugin-infra/src/web3-state/Wallet.ts +++ b/packages/plugin-infra/src/web3-state/Wallet.ts @@ -12,7 +12,7 @@ export class WalletState implements Web3WalletState { } async getAllWallets() { - return this.context.wallets.getCurrentValue() + return this.context.getWallets() } async addWallet(id: string, wallet: Wallet) { diff --git a/packages/plugins/EVM/src/state/Connection/composer.ts b/packages/plugins/EVM/src/state/Connection/composer.ts index 40bce6c71a7f..0bc5e1fc54f2 100644 --- a/packages/plugins/EVM/src/state/Connection/composer.ts +++ b/packages/plugins/EVM/src/state/Connection/composer.ts @@ -14,7 +14,6 @@ import { SharedContextSettings, Web3StateSettings } from '../../settings' import { AddressBook } from './middleware/AddressBook' import { Interceptor } from './middleware/Interceptor' import { Nonce } from './middleware/Nonce' -import { Popup } from './middleware/Popup' import { Squash } from './middleware/Squash' import { RecentTransaction } from './middleware/Transaction' import { Translator } from './middleware/Translator' @@ -194,7 +193,6 @@ composer.use(new Squash()) composer.use(new Nonce()) composer.use(new Translator()) composer.use(new Interceptor()) -composer.use(new Popup()) composer.use(new RecentTransaction()) composer.use(new AddressBook()) diff --git a/packages/plugins/EVM/src/state/Connection/connection.ts b/packages/plugins/EVM/src/state/Connection/connection.ts index 91d81288c81a..2822f2ab677d 100644 --- a/packages/plugins/EVM/src/state/Connection/connection.ts +++ b/packages/plugins/EVM/src/state/Connection/connection.ts @@ -101,6 +101,7 @@ class Connection implements EVM_Connection { await Web3StateSettings.value.Provider?.connect( context.chainId, context.providerType, + options?.popupsWindow, ), ) break @@ -618,26 +619,6 @@ class Connection implements EVM_Connection { ) } - confirmRequest(options?: EVM_Web3ConnectionOptions) { - return this.hijackedRequest( - { - method: EthereumMethodType.MASK_CONFIRM_TRANSACTION, - params: [], - }, - options, - ) - } - - rejectRequest(options?: EVM_Web3ConnectionOptions) { - return this.hijackedRequest( - { - method: EthereumMethodType.MASK_REJECT_TRANSACTION, - params: [], - }, - options, - ) - } - replaceRequest(hash: string, transaction: Transaction, options?: EVM_Web3ConnectionOptions) { return this.hijackedRequest( { diff --git a/packages/plugins/EVM/src/state/Connection/middleware/Popup.ts b/packages/plugins/EVM/src/state/Connection/middleware/Popup.ts deleted file mode 100644 index 07d23e5ebb0b..000000000000 --- a/packages/plugins/EVM/src/state/Connection/middleware/Popup.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { defer } from '@dimensiondev/kit' -import type { JsonRpcPayload } from 'web3-core-helpers' -import { EthereumMethodType, getPayloadConfig, ProviderType } from '@masknet/web3-shared-evm' -import type { Context, Middleware } from '../types' -import { SharedContextSettings } from '../../../settings' - -export class Popup implements Middleware { - private previousRequests: Array<{ - context: Context - resume: () => void - }> = [] - - private isRiskPayload(payload: JsonRpcPayload) { - return [ - EthereumMethodType.ETH_SIGN, - EthereumMethodType.PERSONAL_SIGN, - EthereumMethodType.ETH_SIGN_TYPED_DATA, - EthereumMethodType.ETH_DECRYPT, - EthereumMethodType.ETH_GET_ENCRYPTION_PUBLIC_KEY, - EthereumMethodType.ETH_SEND_TRANSACTION, - EthereumMethodType.ETH_SIGN_TRANSACTION, - ].includes(payload.method as EthereumMethodType) - } - - async fn(context: Context, next: () => Promise) { - const { hasNativeAPI, shiftUnconfirmedRequest, pushUnconfirmedRequest, openPopupWindow, closePopupWindow } = - SharedContextSettings.value - - if (context.providerType !== ProviderType.MaskWallet || hasNativeAPI || !context.requestOptions?.popupsWindow) { - await next() - return - } - - switch (context.method) { - case EthereumMethodType.MASK_CONFIRM_TRANSACTION: - case EthereumMethodType.MASK_REJECT_TRANSACTION: - const payload = await shiftUnconfirmedRequest() - const previousRequest = this.previousRequests.shift() - - if (!payload) { - context.abort(new Error('No unconfirmed request.')) - break - } - - await closePopupWindow() - - if (context.method === EthereumMethodType.MASK_CONFIRM_TRANSACTION) { - if (previousRequest) { - previousRequest.resume() - } else { - const config = getPayloadConfig(payload) - - if (!config) { - context.abort(new Error('Failed to read transaction config.')) - break - } - - // re-send the previous request - await context.connection.sendTransaction(config, context.requestOptions) - } - } else { - if (previousRequest) { - previousRequest.context.abort(new Error('The user rejected the request.')) - previousRequest.resume() - } - } - context.end() - break - default: - if (!this.isRiskPayload(context.request)) break - - try { - await pushUnconfirmedRequest(context.request) - await openPopupWindow() - - const [promise, resume] = defer() - - // for now, we only support one request per time. - this.previousRequests = [ - { - context, - resume, - }, - ] - - // the context is holding until the user confirms or rejects it. - await promise - } catch (error) { - context.abort(error, 'Failed to add request.') - break - } - } - - await next() - } -} diff --git a/packages/plugins/EVM/src/state/Connection/providers/MaskWallet.ts b/packages/plugins/EVM/src/state/Connection/providers/MaskWallet.ts index 14c3647dc7ee..9f154f238ffb 100644 --- a/packages/plugins/EVM/src/state/Connection/providers/MaskWallet.ts +++ b/packages/plugins/EVM/src/state/Connection/providers/MaskWallet.ts @@ -1,23 +1,15 @@ -import Web3 from 'web3' import { toHex } from 'web3-utils' -import type { HttpProvider, RequestArguments } from 'web3-core' -import type { JsonRpcResponse } from 'web3-core-helpers' -import { ChainId, createWeb3Provider, createPayload, getRPCConstants, chainResolver } from '@masknet/web3-shared-evm' +import type { RequestArguments } from 'web3-core' +import { ChainId, createPayload, chainResolver } from '@masknet/web3-shared-evm' import { BaseProvider } from './Base' import type { EVM_Provider } from '../types' import { SharedContextSettings, Web3StateSettings } from '../../../settings' - -const { RPC_WEIGHTS = [] } = getRPCConstants(ChainId.Mainnet) +import { ExtensionSite, getSiteType, isEnhanceableSiteType, PopupRoutes } from '@masknet/shared-base' export class MaskWalletProvider extends BaseProvider implements EVM_Provider { - private id = 0 - private seed = Math.floor(Math.random() * RPC_WEIGHTS.length) - private providerPool = new Map() - private instancePool = new Map() - constructor() { super() - Web3StateSettings.readyPromise.then(this.addShareContextListeners.bind(this)) + Web3StateSettings.readyPromise.then(this.addSharedContextListeners.bind(this)) } /** @@ -42,7 +34,7 @@ export class MaskWalletProvider extends BaseProvider implements EVM_Provider { }) } - private addShareContextListeners() { + private addSharedContextListeners() { const sharedContext = SharedContextSettings.value sharedContext.chainId.subscribe(() => { @@ -53,72 +45,29 @@ export class MaskWalletProvider extends BaseProvider implements EVM_Provider { }) } - private createProviderInstance(url: string) { - const instance = this.providerPool.get(url) - if (instance) return instance - - const newInstance = new Web3.providers.HttpProvider(url, { - timeout: 30 * 1000, // ms - // @ts-ignore - clientConfig: { - keepalive: true, - keepaliveInterval: 1, // ms + override async request(requestArguments: RequestArguments): Promise { + const response = await SharedContextSettings.value.send( + createPayload(0, requestArguments.method, requestArguments.params), + { + popupsWindow: getSiteType() === ExtensionSite.Dashboard || isEnhanceableSiteType(), }, - reconnect: { - auto: true, - delay: 5000, // ms - maxAttempts: Number.MAX_SAFE_INTEGER, - onTimeout: true, - }, - }) - this.providerPool.set(url, newInstance) - return newInstance + ) + return response?.result as T } - private async createProvider(chainId = ChainId.Mainnet) { - await this.readyPromise - - const { RPC_URLS = [], RPC_WEIGHTS = [] } = getRPCConstants(chainId) - const url = RPC_URLS[RPC_WEIGHTS[this.seed]] - if (!url) throw new Error('Failed to create provider.') - return this.createProviderInstance(url) - } - - private createWeb3Instance(provider: HttpProvider) { - const instance = this.instancePool.get(provider.host) - if (instance) return instance - - const newInstance = new Web3(provider) - this.instancePool.set(provider.host, newInstance) - return newInstance - } - - override async createWeb3(chainId?: ChainId) { - return this.createWeb3Instance(await this.createProvider(chainId)) - } - - override async createWeb3Provider(chainId?: ChainId) { - const provider = await this.createProvider(chainId) - const request = (requestArguments: RequestArguments) => { - return new Promise((resolve, reject) => { - this.id += 1 - const requestId = this.id - provider?.send( - createPayload(requestId, requestArguments.method, requestArguments.params), - (error: Error | null, response?: JsonRpcResponse) => { - if (error) reject(error) - else resolve(response?.result as T) - }, - ) - }) + override async connect(chainId: ChainId, popupsWindow = true) { + const { account, chainId: actualChainId, getWallets, updateAccount } = SharedContextSettings.value + const wallets = await getWallets() + if (popupsWindow) { + SharedContextSettings.value.openPopupWindow( + wallets.length ? PopupRoutes.SelectWallet : PopupRoutes.Wallet, + { + chainId, + }, + ) } - return createWeb3Provider(request) - } - - override async connect(chainId: ChainId) { - const { account, chainId: actualChainId, updateAccount } = SharedContextSettings.value - - if (!account.getCurrentValue()) throw new Error(`Failed to connect to ${chainResolver.chainFullName(chainId)}.`) + const accounts = await SharedContextSettings.value.selectAccount() + if (!accounts.length) throw new Error(`Failed to connect to ${chainResolver.chainFullName(chainId)}.`) // switch chain if (actualChainId.getCurrentValue() !== chainId) { diff --git a/packages/plugins/Flow/src/state/Connection/connection.ts b/packages/plugins/Flow/src/state/Connection/connection.ts index 418a443d0978..7e8b42d030ab 100644 --- a/packages/plugins/Flow/src/state/Connection/connection.ts +++ b/packages/plugins/Flow/src/state/Connection/connection.ts @@ -205,8 +205,6 @@ class Connection implements BaseConnection { sendSignedTransaction(signature: never, options?: FlowConnectionOptions): Promise { throw new Error('Method not implemented.') } - confirmRequest?: ((options?: FlowConnectionOptions) => Promise) | undefined - rejectRequest?: ((options?: FlowConnectionOptions) => Promise) | undefined replaceRequest(hash: string, config: MutateOptions, options?: FlowConnectionOptions): Promise { throw new Error('Method not implemented.') } diff --git a/packages/plugins/Solana/src/state/Connection/connection.ts b/packages/plugins/Solana/src/state/Connection/connection.ts index 1eab84f2eae0..04175820a471 100644 --- a/packages/plugins/Solana/src/state/Connection/connection.ts +++ b/packages/plugins/Solana/src/state/Connection/connection.ts @@ -219,12 +219,6 @@ class Connection implements BaseConnection { ): Promise> { throw new Error('Method not implemented.') } - confirmRequest?: - | ((options?: ConnectionOptions | undefined) => Promise) - | undefined - rejectRequest?: - | ((options?: ConnectionOptions | undefined) => Promise) - | undefined replaceRequest( hash: string, config: Transaction, diff --git a/packages/shared-base/src/Site/index.ts b/packages/shared-base/src/Site/index.ts index 438764cda7b4..d5c92e292ae8 100644 --- a/packages/shared-base/src/Site/index.ts +++ b/packages/shared-base/src/Site/index.ts @@ -17,7 +17,7 @@ const matchExtensionSitePathname: Record = { } function getEnhanceableSiteType() { - const { host, pathname } = location + const { host } = location for (const [type, regexp] of Object.entries(matchEnhanceableSiteHost)) { if (host.match(regexp)) return type as EnhanceableSite continue @@ -26,7 +26,7 @@ function getEnhanceableSiteType() { } function getExtensionSiteType() { - const { host, pathname } = location + const { pathname } = location for (const [type, regexp] of Object.entries(matchExtensionSitePathname)) { if (pathname.match(regexp)) return type as ExtensionSite continue @@ -38,6 +38,10 @@ export function getSiteType() { return getEnhanceableSiteType() ?? getExtensionSiteType() } +export function isEnhanceableSiteType() { + return !!getEnhanceableSiteType() +} + export function isExtensionSiteType() { return !!getExtensionSiteType() } diff --git a/packages/web3-providers/src/index.ts b/packages/web3-providers/src/index.ts index 405ac9f2b718..a5d61bd26d26 100644 --- a/packages/web3-providers/src/index.ts +++ b/packages/web3-providers/src/index.ts @@ -10,7 +10,6 @@ import { KeyValueAPI } from './kv' import { TwitterAPI } from './twitter' import { TokenListAPI } from './token-list' import { InstagramAPI } from './instagram' -import { MaskAPI } from './mask' import { DeBankAPI } from './debank' import { ZerionAPI } from './zerion' import { MetaSwapAPI } from './metaswap' @@ -34,7 +33,6 @@ export const Twitter = new TwitterAPI() export const Instagram = new InstagramAPI() export const GoPlusLabs = new GoPlusLabsAPI() export const TokenList = new TokenListAPI() -export const Mask = new MaskAPI() export const DeBank = new DeBankAPI() export const Zerion = new ZerionAPI() export const MetaSwap = new MetaSwapAPI() diff --git a/packages/web3-shared/base/src/specs/index.ts b/packages/web3-shared/base/src/specs/index.ts index f7a8cbd018d3..1fed42f7de32 100644 --- a/packages/web3-shared/base/src/specs/index.ts +++ b/packages/web3-shared/base/src/specs/index.ts @@ -644,10 +644,6 @@ export interface Connection< connect(options?: Web3ConnectionOptions): Promise> /** Break connection */ disconnect(options?: Web3ConnectionOptions): Promise - /** Confirm request */ - confirmRequest?: (options?: Web3ConnectionOptions) => Promise - /** Reject request */ - rejectRequest?: (options?: Web3ConnectionOptions) => Promise /** Replace request */ replaceRequest(hash: string, config: Transaction, options?: Web3ConnectionOptions): Promise /** Cancel request */ @@ -882,7 +878,7 @@ export interface ProviderState { /** Wait until a provider ready */ untilReady: (providerType: ProviderType) => Promise /** Connect with the provider and set chain id. */ - connect: (chainId: ChainId, providerType: ProviderType) => Promise> + connect: (chainId: ChainId, providerType: ProviderType, popupsWindow?: boolean) => Promise> /** Disconnect with the provider. */ disconnect: (providerType: ProviderType) => Promise } diff --git a/packages/web3-shared/evm/types/index.ts b/packages/web3-shared/evm/types/index.ts index 5c1660d23d63..7864e505c88f 100644 --- a/packages/web3-shared/evm/types/index.ts +++ b/packages/web3-shared/evm/types/index.ts @@ -152,8 +152,6 @@ export enum EthereumMethodType { MASK_LOGIN = 'MASK_LOGIN', MASK_LOGOUT = 'MASK_LOGOUT', MASK_REPLACE_TRANSACTION = 'mask_replaceTransaction', - MASK_CONFIRM_TRANSACTION = 'mask_confirmTransaction', - MASK_REJECT_TRANSACTION = 'mask_rejectTransaction', } export enum TransactionEventType {