diff --git a/_raw/locales/en/messages.json b/_raw/locales/en/messages.json index bc984655fb7..3f8467dd8e6 100644 --- a/_raw/locales/en/messages.json +++ b/_raw/locales/en/messages.json @@ -2546,6 +2546,11 @@ "fast": "Fast", "normal": "Normal", "doNotReserve": "Don't reserve Gas" + }, + "OpenExternalWebsiteModal": { + "title": "You're Leaving Rabby Wallet", + "content": "You're about to visit an external website. Rabby Wallet is not responsible for the content or security of this site.", + "button": "Continue" } }, "global": { diff --git a/package.json b/package.json index cedb4ccb0be..129c14f8bc6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rabby", - "version": "0.92.99", + "version": "0.92.100", "description": "A browser plugin for DeFi users", "scripts": { "clean": "mkdir -p dist && rm -rf dist/*", @@ -60,7 +60,7 @@ "@rabby-wallet/eth-watch-keyring": "1.0.0", "@rabby-wallet/gnosis-sdk": "1.3.9", "@rabby-wallet/page-provider": "0.4.2", - "@rabby-wallet/rabby-action": "0.1.4", + "@rabby-wallet/rabby-action": "0.1.8", "@rabby-wallet/rabby-api": "0.8.5", "@rabby-wallet/rabby-security-engine": "2.0.7", "@rabby-wallet/rabby-swap": "0.0.42", diff --git a/patches/@ledgerhq+hw-app-eth+6.39.0.patch b/patches/@ledgerhq+hw-app-eth+6.39.0.patch new file mode 100644 index 00000000000..8dbf7674b02 --- /dev/null +++ b/patches/@ledgerhq+hw-app-eth+6.39.0.patch @@ -0,0 +1,26 @@ +diff --git a/node_modules/@ledgerhq/hw-app-eth/lib-es/services/ledger/contracts.js b/node_modules/@ledgerhq/hw-app-eth/lib-es/services/ledger/contracts.js +index 806a539..b488b25 100644 +--- a/node_modules/@ledgerhq/hw-app-eth/lib-es/services/ledger/contracts.js ++++ b/node_modules/@ledgerhq/hw-app-eth/lib-es/services/ledger/contracts.js +@@ -19,7 +19,7 @@ export const loadInfosForContractMethod = (contractAddress, selector, chainId, u + if (pluginBaseURL) { + const url = `${pluginBaseURL}/plugins/ethereum.json`; + data = yield axios +- .get(`${pluginBaseURL}/plugins/ethereum.json`) ++ .get(`${pluginBaseURL}/plugins/ethereum.json`, { timeout: 5000 }) + .then(r => r.data) + .catch(e => { + log("error", "could not fetch from " + url + ": " + String(e)); +diff --git a/node_modules/@ledgerhq/hw-app-eth/lib-es/services/ledger/erc20.js b/node_modules/@ledgerhq/hw-app-eth/lib-es/services/ledger/erc20.js +index 16a933e..a2a0f1a 100644 +--- a/node_modules/@ledgerhq/hw-app-eth/lib-es/services/ledger/erc20.js ++++ b/node_modules/@ledgerhq/hw-app-eth/lib-es/services/ledger/erc20.js +@@ -21,7 +21,7 @@ export const findERC20SignaturesInfo = (userLoadConfig, chainId) => __awaiter(vo + return null; + const url = `${cryptoassetsBaseURL}/evm/${chainId}/erc20-signatures.json`; + const blob = yield axios +- .get(url) ++ .get(url, { timeout: 5000 }) + .then(({ data }) => { + if (!data || typeof data !== "string") { + throw new Error(`ERC20 signatures for chainId ${chainId} file is malformed ${url}`); diff --git a/patches/@metamask+eth-sig-util+5.1.0.patch b/patches/@metamask+eth-sig-util+5.1.0.patch index 6e24543cc59..38062ce9957 100644 --- a/patches/@metamask+eth-sig-util+5.1.0.patch +++ b/patches/@metamask+eth-sig-util+5.1.0.patch @@ -1,3 +1,25 @@ +diff --git a/node_modules/@metamask/eth-sig-util/dist/ethereumjs-abi-utils.d.ts b/node_modules/@metamask/eth-sig-util/dist/ethereumjs-abi-utils.d.ts +index 45a660e..9b3f105 100644 +--- a/node_modules/@metamask/eth-sig-util/dist/ethereumjs-abi-utils.d.ts ++++ b/node_modules/@metamask/eth-sig-util/dist/ethereumjs-abi-utils.d.ts +@@ -20,3 +20,5 @@ export declare function parseNumber(arg: string | number | BN): BN; + * @param values + */ + export declare function rawEncode(types: string[], values: (BN | Buffer | string | number | string[] | number[])[]): Buffer; ++ ++export declare function encodeSingle(type: string, arg: BN | Buffer | string | number | string[] | number[]): Buffer; +diff --git a/node_modules/@metamask/eth-sig-util/dist/ethereumjs-abi-utils.js b/node_modules/@metamask/eth-sig-util/dist/ethereumjs-abi-utils.js +index 1a11fec..7c78143 100644 +--- a/node_modules/@metamask/eth-sig-util/dist/ethereumjs-abi-utils.js ++++ b/node_modules/@metamask/eth-sig-util/dist/ethereumjs-abi-utils.js +@@ -352,6 +352,7 @@ function encodeSingle(type, arg) { + } + throw new Error(`Unsupported or invalid type: ${JSON.stringify(type)}`); + } ++exports.encodeSingle = encodeSingle; + // Is a type dynamic? + /** + * @param type diff --git a/node_modules/@metamask/eth-sig-util/dist/index.d.ts b/node_modules/@metamask/eth-sig-util/dist/index.d.ts index 48ddf59..58c9ae7 100644 --- a/node_modules/@metamask/eth-sig-util/dist/index.d.ts diff --git a/src/background/service/keyring/eth-ledger-keyring.ts b/src/background/service/keyring/eth-ledger-keyring.ts index c7d4b178e3c..cffc3f56959 100644 --- a/src/background/service/keyring/eth-ledger-keyring.ts +++ b/src/background/service/keyring/eth-ledger-keyring.ts @@ -15,6 +15,12 @@ import { SignHelper, LedgerHDPathType } from './helper'; const type = 'Ledger Hardware'; import HDPathType = LedgerHDPathType; +import Browser from 'webextension-polyfill'; +import { + LedgerAction, + OffscreenCommunicationTarget, +} from '@/constant/offscreen-communication'; +import { isManifestV3 } from '@/utils/env'; const HD_PATH_BASE = { [HDPathType.BIP44]: "m/44'/60'/0'/0", @@ -70,6 +76,17 @@ class LedgerBridgeKeyring { this.app = null; this.usedHDPathTypeList = {}; this.deserialize(opts); + + if (isManifestV3) { + Browser.runtime.onMessage.addListener((request) => { + if ( + request.target === OffscreenCommunicationTarget.extension && + request.event === LedgerAction.ledgerDeviceDisconnect + ) { + this.cleanUp(); + } + }); + } } serialize() { @@ -145,7 +162,9 @@ class LedgerBridgeKeyring { async cleanUp() { this.app = null; - if (this.transport) this.transport.close(); + if (this.transport) { + await this.transport.close(); + } this.transport = null; } diff --git a/src/constant/offscreen-communication.ts b/src/constant/offscreen-communication.ts index ac0f369fdd1..03ae7bcc7df 100644 --- a/src/constant/offscreen-communication.ts +++ b/src/constant/offscreen-communication.ts @@ -49,3 +49,7 @@ export enum LatticeAction { export enum KnownOrigins { lattice = 'https://lattice.gridplus.io', } + +export enum LedgerAction { + ledgerDeviceDisconnect = 'ledger-device-disconnect', +} diff --git a/src/manifest/mv2/manifest.json b/src/manifest/mv2/manifest.json index 464958690bd..cc7f9aefaaf 100644 --- a/src/manifest/mv2/manifest.json +++ b/src/manifest/mv2/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_appName__", "short_name": "__MSG_appName__", - "version": "0.92.99", + "version": "0.92.100", "default_locale": "en", "description": "__MSG_appDescription__", "icons": { diff --git a/src/manifest/mv3/manifest.json b/src/manifest/mv3/manifest.json index 61e581f918f..5292b86322c 100644 --- a/src/manifest/mv3/manifest.json +++ b/src/manifest/mv3/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "__MSG_appName__", "short_name": "__MSG_appName__", - "version": "0.92.99", + "version": "0.92.100", "default_locale": "en", "description": "__MSG_appDescription__", "icons": { diff --git a/src/offscreen/scripts/ledger.ts b/src/offscreen/scripts/ledger.ts new file mode 100644 index 00000000000..54f5f01b813 --- /dev/null +++ b/src/offscreen/scripts/ledger.ts @@ -0,0 +1,17 @@ +import { + LedgerAction, + OffscreenCommunicationTarget, +} from '@/constant/offscreen-communication'; +import { ledgerUSBVendorId } from '@ledgerhq/devices'; +import Browser from 'webextension-polyfill'; + +export function initLedger() { + navigator.hid.addEventListener('disconnect', ({ device }) => { + if (device.vendorId === ledgerUSBVendorId) { + Browser.runtime.sendMessage({ + target: OffscreenCommunicationTarget.extension, + event: LedgerAction.ledgerDeviceDisconnect, + }); + } + }); +} diff --git a/src/offscreen/scripts/offscreen.ts b/src/offscreen/scripts/offscreen.ts index f846b4a4885..3b6b7cc27fe 100644 --- a/src/offscreen/scripts/offscreen.ts +++ b/src/offscreen/scripts/offscreen.ts @@ -1,12 +1,14 @@ import { initBitBox02 } from './bitbox02'; import { initImKey } from './imkey'; import initLattice from './lattice'; +import { initLedger } from './ledger'; import { initOneKey } from './onekey'; initImKey(); initOneKey(); initBitBox02(); initLattice(); +initLedger(); // keep alive when ui page is open let pageCount = 0; diff --git a/src/ui/assets/component/external-link-alert.svg b/src/ui/assets/component/external-link-alert.svg new file mode 100644 index 00000000000..5efb37d4344 --- /dev/null +++ b/src/ui/assets/component/external-link-alert.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ui/component/Ecology/EcologyNavBar.tsx b/src/ui/component/Ecology/EcologyNavBar.tsx index 703bf680af8..6695afcf490 100644 --- a/src/ui/component/Ecology/EcologyNavBar.tsx +++ b/src/ui/component/Ecology/EcologyNavBar.tsx @@ -1,23 +1,22 @@ -import { findChain } from '@/utils/chain'; import clsx from 'clsx'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router-dom'; import { ReactComponent as RcIconBack } from 'ui/assets/icon-back-cc.svg'; +import { EcoChains } from 'ui/views/Ecology/constants'; interface Props { className?: string; style?: React.CSSProperties; chainId: number; } + +const findChain = (id: number) => { + return EcoChains.find((chain) => chain.id === id); +}; + export const EcologyNavBar = ({ className, style, chainId }: Props) => { - const chain = useMemo( - () => - findChain({ - id: chainId, - }), - [chainId] - ); + const chain = useMemo(() => findChain(chainId), [chainId]); const history = useHistory(); const { t } = useTranslation(); diff --git a/src/ui/style/index.less b/src/ui/style/index.less index 9c59a8b3f60..af9c3e9b038 100644 --- a/src/ui/style/index.less +++ b/src/ui/style/index.less @@ -423,3 +423,19 @@ button:focus { background-clip: text; color: transparent; } + +.external-link-alert-modal { + background: var(--r-neutral-bg-1); + border-radius: 16px; + overflow: hidden; + padding-bottom: 0; + .ant-modal-confirm-content { + padding: 0; + } + .ant-modal-confirm-content, .ant-modal-content { + background: var(--r-neutral-bg-1); + } + .ant-modal-content { + box-shadow: none; + } +} \ No newline at end of file diff --git a/src/ui/utils/webapi.ts b/src/ui/utils/webapi.ts deleted file mode 100644 index 34cb1733902..00000000000 --- a/src/ui/utils/webapi.ts +++ /dev/null @@ -1,55 +0,0 @@ -import browser, { Tabs, Windows } from 'webextension-polyfill'; -import { WalletController, WalletControllerType } from './index'; -import { getOriginFromUrl } from '@/utils'; - -export const getCurrentTab = async (): Promise => { - const tabs = await browser.tabs.query({ active: true, currentWindow: true }); - - return tabs[0]; -}; - -export const getCurrentConnectSite = async ( - wallet: WalletController | WalletControllerType -) => { - const { id, url } = await getCurrentTab(); - if (!id || !url) return null; - - return (wallet as WalletControllerType).getCurrentConnectedSite( - id, - getOriginFromUrl(url) - ); -}; - -export const openInTab = async ( - url?: string, - needClose = true -): Promise => { - const tab = await browser.tabs.create({ - active: true, - url, - }); - - if (needClose) window.close(); - - return tab; -}; - -export const getCurrentWindow = async (): Promise => { - const { id } = await browser.windows.getCurrent({ - windowTypes: ['popup'], - } as Windows.GetInfo); - - return id; -}; - -export const openInternalPageInTab = ( - path: string, - useWebapi = true, - needClose = true -) => { - if (useWebapi) { - openInTab(`./index.html#/${path}`, needClose); - } else { - window.open(`./index.html#/${path}`); - } -}; diff --git a/src/ui/utils/webapi.tsx b/src/ui/utils/webapi.tsx new file mode 100644 index 00000000000..0edaf29cd17 --- /dev/null +++ b/src/ui/utils/webapi.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import browser, { Tabs, Windows } from 'webextension-polyfill'; +import { t } from 'i18next'; +import { Button } from 'antd'; +import { WalletController, WalletControllerType } from './index'; +import { getOriginFromUrl } from '@/utils'; +import Modal from '../component/Modal'; +import { ReactComponent as ExternalLinkAlert } from 'ui/assets/component/external-link-alert.svg'; + +export const getCurrentTab = async (): Promise => { + const tabs = await browser.tabs.query({ active: true, currentWindow: true }); + + return tabs[0]; +}; + +export const getCurrentConnectSite = async ( + wallet: WalletController | WalletControllerType +) => { + const { id, url } = await getCurrentTab(); + if (!id || !url) return null; + + return (wallet as WalletControllerType).getCurrentConnectedSite( + id, + getOriginFromUrl(url) + ); +}; + +export const openInTab = async ( + url?: string, + needClose = true +): Promise => { + const tab = await browser.tabs.create({ + active: true, + url, + }); + + if (needClose) window.close(); + + return tab; +}; + +export const getCurrentWindow = async (): Promise => { + const { id } = await browser.windows.getCurrent({ + windowTypes: ['popup'], + } as Windows.GetInfo); + + return id; +}; + +export const openInternalPageInTab = ( + path: string, + useWebapi = true, + needClose = true +) => { + if (useWebapi) { + openInTab(`./index.html#/${path}`, needClose); + } else { + window.open(`./index.html#/${path}`); + } +}; + +export const openExternalWebsiteInTab = async ( + url?: string, + needClose = true +) => { + return new Promise((resolve, reject) => { + const modal = Modal.info({ + closable: true, + className: 'external-link-alert-modal', + content: ( +
+
+ +
+

+ {t('component.OpenExternalWebsiteModal.title')} +

+

+ {t('component.OpenExternalWebsiteModal.content')} +

+
+ +
+
+ ), + onCancel() { + reject('user cancel'); + }, + }); + }); +}; diff --git a/src/ui/views/AddressBackup/Mnemonics.tsx b/src/ui/views/AddressBackup/Mnemonics.tsx index 32a8b92d1f7..9931ee94315 100644 --- a/src/ui/views/AddressBackup/Mnemonics.tsx +++ b/src/ui/views/AddressBackup/Mnemonics.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useWallet } from 'ui/utils'; import './style.less'; import { InfoCircleOutlined } from '@ant-design/icons'; import { Button } from 'antd'; @@ -20,7 +19,6 @@ import QRCode from 'qrcode.react'; import { ReactComponent as RcIconQrCode } from 'ui/assets/qrcode-cc.svg'; const AddressBackup = () => { - const wallet = useWallet(); const { t } = useTranslation(); const history = useHistory(); @@ -91,11 +89,11 @@ const AddressBackup = () => { )} {t('page.backupSeedPhrase.title')} -
+
{t('page.backupSeedPhrase.alert')}
-
+
setMasked(false)} @@ -106,6 +104,34 @@ const AddressBackup = () => { {t('page.backupSeedPhrase.clickToShow')}

+
+
+ + {t('page.backupSeedPhrase.showQrCode')} +
+
+ + {t('page.backupSeedPhrase.copySeedPhrase')} +
+
{ )}
-
-
- - {t('page.backupSeedPhrase.showQrCode')} -
-
- - {t('page.backupSeedPhrase.copySeedPhrase')} -
-
-
+