diff --git a/apps/extension/package.json b/apps/extension/package.json index 4eebc7ef62..f47e71114c 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -52,6 +52,8 @@ "@keplr-wallet/types": "0.12.138", "@keplr-wallet/unit": "0.12.138", "@keystonehq/animated-qr": "^0.8.6", + "@keystonehq/hw-app-base": "0.1.1", + "@keystonehq/hw-transport-webusb": "0.4.0", "@keystonehq/keystone-sdk": "^0.2.3", "@ledgerhq/devices": "^6.20.0", "@react-spring/web": "^9.6.1", diff --git a/apps/extension/src/languages/en.json b/apps/extension/src/languages/en.json index 41df93cc7a..f868025211 100644 --- a/apps/extension/src/languages/en.json +++ b/apps/extension/src/languages/en.json @@ -105,6 +105,10 @@ "pages.register.name-password-hardware.connect-to-terra": "Terra app", "pages.register.name-password-hardware.connect-to-secret": "Secret app", + "pages.register.name-password-hardware.connect-keystone-way": "Connect Keystone via", + "pages.register.name-password-hardware.connect-to-keystone-USB": "USB", + "pages.register.name-password-hardware.connect-to-keystone-QR": "QR", + "pages.register.recover-mnemonic.title": "Import Existing Wallet", "pages.register.recover-mnemonic.paragraph-1": "Enter your recovery phrase here to restore your wallet. Or click on any blank and paste the entire phrase.", "pages.register.recover-mnemonic.paragraph-2": "Enter the phrase in the right order without capitalization, punctuation symbols, or spaces.", @@ -128,7 +132,7 @@ "pages.register.connect-ledger.use-hid-confirm-title": "Unable to use Web HID", "pages.register.connect-ledger.use-hid-confirm-paragraph": "Please enable ‘experimental web platform features’ to use Web HID", - "pages.register.connect-keystone.title": "Please Connect Your Hardware Wallet", + "pages.register.connect-keystone.paragraph": "Please unlock your Keystone device and connect it to your computer via USB.", "pages.register.connect-keystone.step-text": "Step {num}", "pages.register.connect-keystone.step-1": "Tap “Connect Software Wallet” at the bottom left corner on the Keystone device.", "pages.register.connect-keystone.step-2": "Select “Keplr Wallet”.", @@ -145,6 +149,10 @@ "pages.register.connect-keystone.tutorial": "Tutorial", "pages.register.connect-keystone.unsupported": "Unsupported", + "pages.register.connect-keystone.usb.title": "Please Connect Your Hardware Wallet", + "pages.register.connect-keystone.usb.step-1": "Connect and unlock your Keystone.", + "pages.register.connect-keystone.usb.step-2": "Ensure your Keystone 3 Pro is on the homepage.", + "pages.register.enable-chains.title": "Select Chains", "pages.register.enable-chains.paragraph": "Don’t worry, you can change your selections anytime in the Manage Chain Visibility in the sidebar menu.", "pages.register.enable-chains.search-input-placeholder": "Search networks", @@ -505,6 +513,10 @@ "page.sign.keystone.invalid-qrcode": "Invalid QR code", "page.sign.keystone.select-valid-qrcode": "Please ensure you have selected a valid QR code from your Keystone device.", + "page.sign.keystone.usb.box.unknown-error-title": "Unknown error", + "page.sign.keystone.usb.box.sign-on-keystone-title": "Sign on Keystone", + "page.sign.keystone.usb.box.sign-on-keystone-paragraph": "To proceed, please review and approve the transaction on your Keystone device.", + "page.send.select-asset.title": "Select Asset", "page.send.select-asset.search-placeholder": "Search for asset or chain", "page.send.select-asset.hide-ibc-token": "Hide IBC token", diff --git a/apps/extension/src/languages/ko.json b/apps/extension/src/languages/ko.json index 2e6f6837b2..494e8563ec 100644 --- a/apps/extension/src/languages/ko.json +++ b/apps/extension/src/languages/ko.json @@ -105,6 +105,10 @@ "pages.register.name-password-hardware.connect-to-terra": "테라 앱", "pages.register.name-password-hardware.connect-to-secret": "시크릿 앱", + "pages.register.name-password-hardware.connect-keystone-way": "Keystone를 통해 연결하세요", + "pages.register.name-password-hardware.connect-to-keystone-USB": "USB", + "pages.register.name-password-hardware.connect-to-keystone-QR": "QR", + "pages.register.recover-mnemonic.title": "기존 지갑 불러오기", "pages.register.recover-mnemonic.paragraph-1": "복구 문구를 통해 지갑을 불러올 수 있습니다. 아무 칸이나 선택해서 전체 복구 문구를 붙여 넣어보세요.", "pages.register.recover-mnemonic.paragraph-2": "복구 문구의 단어를 올바른 순서로, 정확한 대소문자, 구두점 기호 혹은 공백으로 기입해야만 지갑을 불러올 수 있습니다.", @@ -145,6 +149,10 @@ "pages.register.connect-keystone.tutorial": "튜토리얼", "pages.register.connect-keystone.unsupported": "미지원", + "pages.register.connect-keystone.usb.title": "하드웨어 지갑을 연결해 주세요.", + "pages.register.connect-keystone.usb.step-1": "키스톤을 연결하고 잠금을 해제하세요.", + "pages.register.connect-keystone.usb.step-2": "귀하의 Keystone 3 Pro가 홈페이지에 있도록 하십시오.", + "pages.register.enable-chains.title": "체인 선택하기", "pages.register.enable-chains.paragraph": "걱정마세요! 언제든지 사이드바 메뉴에 있는 ‘체인 표시’를 방문해서 설정을 변경할 수 있습니다.", "pages.register.enable-chains.search-input-placeholder": "네트워크 검색하기", @@ -488,6 +496,10 @@ "page.sign.keystone.invalid-qrcode": "QR 코드가 유효하지 않습니다", "page.sign.keystone.select-valid-qrcode": "키스톤 장치에서 유효한 QR 코드를 선택했는지 확인해주세요.", + "page.sign.keystone.usb.box.unknown-error-title": "알 수 없는 오류", + "page.sign.keystone.usb.box.sign-on-keystone-title": "키스톤에 있는 표지판", + "page.sign.keystone.usb.box.sign-on-keystone-paragraph": "진행하려면, 귀하의 Keystone 기기에서 거래를 검토하고 승인해 주시기 바랍니다.", + "page.send.select-asset.title": "자산 선택", "page.send.select-asset.search-placeholder": "자산이나 체인 이름 검색", "page.send.select-asset.hide-ibc-token": "IBC 토큰 숨기기", diff --git a/apps/extension/src/languages/zh-cn.json b/apps/extension/src/languages/zh-cn.json index ec49dec8c2..92ecdbfc2a 100644 --- a/apps/extension/src/languages/zh-cn.json +++ b/apps/extension/src/languages/zh-cn.json @@ -99,6 +99,10 @@ "pages.register.name-password-hardware.connect-to-terra": "Terra应用", "pages.register.name-password-hardware.connect-to-secret": "Secret应用", + "pages.register.name-password-hardware.connect-keystone-way": "选择Keystone连接方式", + "pages.register.name-password-hardware.connect-to-keystone-USB": "USB", + "pages.register.name-password-hardware.connect-to-keystone-QR": "QR", + "pages.register.recover-mnemonic.title": "导入已有钱包", "pages.register.recover-mnemonic.paragraph-1": "输入助记词,或点击任意输入框并粘贴整个助记词。", "pages.register.recover-mnemonic.paragraph-2": "按正确的顺序输入助记词,无需大写、标点符号或空格。", @@ -139,6 +143,10 @@ "pages.register.connect-keystone.tutorial": "教程", "pages.register.connect-keystone.unsupported": "暂不支持", + "pages.register.connect-keystone.usb.title": "请连接您的硬件钱包", + "pages.register.connect-keystone.usb.step-1": "连接并解锁您的Keystone.", + "pages.register.connect-keystone.usb.step-2": "确保你的 Keystone 3 Pro 在主页上。", + "pages.register.enable-chains.title": "选择链", "pages.register.enable-chains.paragraph": "不用担心,你可以随时在侧边栏菜单中的“管理链可见性”中更改你的选择。", "pages.register.enable-chains.search-input-placeholder": "搜索网络", @@ -473,6 +481,10 @@ "page.sign.keystone.invalid-qrcode": "无效的二维码", "page.sign.keystone.select-valid-qrcode": "请确保你从Keystone设备中选择了正确的二维码。", + "page.sign.keystone.usb.box.unknown-error-title": "未知错误", + "page.sign.keystone.usb.box.sign-on-keystone-title": "Keystone 签名", + "page.sign.keystone.usb.box.sign-on-keystone-paragraph": "要继续,请在你的Keystone设备上查看并确认交易。", + "page.send.select-asset.title": "选择资产", "page.send.select-asset.search-placeholder": "搜索资产或链", "page.send.select-asset.hide-ibc-token": "隐藏IBC币种", diff --git a/apps/extension/src/pages/register/connect-keystone/index.tsx b/apps/extension/src/pages/register/connect-keystone/index.tsx index 324a651a1e..4de4e439f2 100644 --- a/apps/extension/src/pages/register/connect-keystone/index.tsx +++ b/apps/extension/src/pages/register/connect-keystone/index.tsx @@ -12,8 +12,9 @@ import { Button } from "../../../components/button"; import { ColorPalette } from "../../../styles"; import { useTheme } from "styled-components"; import { FormattedMessage, useIntl } from "react-intl"; +export { ConnectKeystoneUSBScene } from "./usb"; -export const ConnectKeystoneScene: FunctionComponent<{ +export const ConnectKeystoneQRScene: FunctionComponent<{ name: string; password: string; stepPrevious: number; diff --git a/apps/extension/src/pages/register/connect-keystone/scan.tsx b/apps/extension/src/pages/register/connect-keystone/scan.tsx index 59bb2c3126..b2eb1c5771 100644 --- a/apps/extension/src/pages/register/connect-keystone/scan.tsx +++ b/apps/extension/src/pages/register/connect-keystone/scan.tsx @@ -57,10 +57,11 @@ export const ScanKeystoneScene: FunctionComponent<{ const accounts = sdk.parseMultiAccounts( new UR(Buffer.from(ur.cbor, "hex"), ur.type) ); + const enhancedAccounts = { ...accounts, connectionType: "QR" }; sceneTransition.replaceAll("finalize-key", { name, password, - keystone: accounts, + keystone: enhancedAccounts, stepPrevious: stepPrevious + 1, stepTotal, }); diff --git a/apps/extension/src/pages/register/connect-keystone/usb.tsx b/apps/extension/src/pages/register/connect-keystone/usb.tsx new file mode 100644 index 0000000000..3a0156862b --- /dev/null +++ b/apps/extension/src/pages/register/connect-keystone/usb.tsx @@ -0,0 +1,353 @@ +import React, { FunctionComponent, useState } from "react"; +import { RegisterSceneBox } from "../components/register-scene-box"; +import { + useSceneEvents, + useSceneTransition, +} from "../../../components/transition"; +import { useRegisterHeader } from "../components/header"; +import { Gutter } from "../../../components/gutter"; +import { Box } from "../../../components/box"; +import { XAxis, YAxis } from "../../../components/axis"; +import { Body1, H2 } from "../../../components/typography"; +import { ColorPalette } from "../../../styles"; +import { Stack } from "../../../components/stack"; +import { Button } from "../../../components/button"; +import { TransportWebUSB } from "@keystonehq/hw-transport-webusb"; +import Base from "@keystonehq/hw-app-base"; +import { createKeystoneTransport } from "../../../utils/keystone"; +import { observer } from "mobx-react-lite"; +import { FormattedMessage, useIntl } from "react-intl"; +import { useTheme } from "styled-components"; +import KeystoneSDK, { + Curve, + DerivationAlgorithm, +} from "@keystonehq/keystone-sdk"; +import { KeystoneIcon } from "../../../components/icon/keystone"; + +type Step = "unknown" | "connected" | "app"; + +const DEFAULT_KEYSTONE_PATHS = [ + "m/44'/118'/0'/0/0", // cosmos path + "m/44'/60'/0'/0/0", // ethereum path + "m/44'/529'/0'/0/0", + "m/44'/394'/0'/0/0", + "m/44'/234'/0'/0/0", + "m/44'/564'/0'/0/0", + "m/44'/459'/0'/0/0", + "m/44'/330'/0'/0/0", +]; + +const DEFAULT_COIN_TYPE = [118, 60]; + +const isDefaultPath = (bip44Path: { + account: number; + change: number; + addressIndex: number; +}) => { + return ( + bip44Path.account === 0 && + bip44Path.change === 0 && + bip44Path.addressIndex === 0 + ); +}; + +const fetchRequiredAccountsFromKeystone = async ( + baseApp: Base, + paths: string[] +) => { + const keys = []; + let device; + let deviceId; + let masterFingerprint; + for (const path of paths) { + const res = await baseApp.getURAccount( + path, + Curve.secp256k1, + DerivationAlgorithm.slip10 + ); + const sdk = new KeystoneSDK({ + origin: "Keplr Extension", + }); + const account = sdk.parseMultiAccounts(res.toUR()); + keys.push(account.keys[0]); + device = account.device; + deviceId = account.deviceId; + masterFingerprint = account.masterFingerprint; + } + return { + keys, + device, + deviceId, + masterFingerprint, + }; +}; + +export const ConnectKeystoneUSBScene: FunctionComponent<{ + name: string; + password: string; + bip44Path: { + account: number; + change: number; + addressIndex: number; + }; + stepPrevious: number; + stepTotal: number; +}> = observer(({ name, password, bip44Path, stepPrevious, stepTotal }) => { + const intl = useIntl(); + + const sceneTransition = useSceneTransition(); + + const header = useRegisterHeader(); + useSceneEvents({ + onWillVisible: () => { + header.setHeader({ + mode: "step", + title: intl.formatMessage({ + id: "pages.register.connect-keystone.usb.title", + }), + paragraphs: [], + stepCurrent: stepPrevious + 1, + stepTotal: stepTotal, + }); + }, + }); + + const [step, setStep] = useState("unknown"); + const [isLoading, setIsLoading] = useState(false); + + const connectKeystone = async () => { + setIsLoading(true); + + let transport: TransportWebUSB; + + try { + transport = await createKeystoneTransport(); + } catch { + setStep("unknown"); + setIsLoading(false); + return; + } + + const baseApp = new Base(transport as any); + + // step1: make sure the device is connected and unlocked + try { + await baseApp.getAppConfig(); + setStep("connected"); + } catch (e) { + console.log(e); + setStep("unknown"); + setIsLoading(false); + return; + } + + // step2: get the keystone accounts + let accounts; + let paths = DEFAULT_KEYSTONE_PATHS; + + try { + if (!isDefaultPath(bip44Path)) { + paths = DEFAULT_COIN_TYPE.map( + (each) => + `m/44'/${each}'/${bip44Path.account}'/${bip44Path.change}/${bip44Path.addressIndex}` + ); + } + + const result = await fetchRequiredAccountsFromKeystone(baseApp, paths); + accounts = { + device: result?.device, + deviceId: result?.deviceId, + masterFingerprint: result?.masterFingerprint, + keys: result?.keys, + bip44Path: { + account: bip44Path.account ?? 0, + change: bip44Path.change ?? 0, + addressIndex: bip44Path.addressIndex ?? 0, + }, + connectionType: "USB", + }; + + setStep("app"); + // + sceneTransition.replaceAll("finalize-key", { + name, + password, + keystone: accounts, + stepPrevious: stepPrevious + 1, + stepTotal, + }); + } catch (e) { + console.log(e); + setStep("unknown"); + } + setIsLoading(false); + + return; + }; + + return ( + + + + + + } + focused={step === "unknown"} + completed={step !== "unknown"} + /> + + + + } + focused={step === "connected"} + completed={step === "app"} + /> + + + +