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"}
+ />
+
+
+
+
+
+
+ );
+});
+
+const StepView: FunctionComponent<{
+ step: number;
+ paragraph: string;
+ icon?: React.ReactNode;
+
+ focused: boolean;
+ completed: boolean;
+}> = ({ step, paragraph, icon, focused, completed }) => {
+ const theme = useTheme();
+
+ return (
+
+
+ {icon}
+
+
+
+
+
+
+ {completed ? (
+
+
+
+
+ ) : null}
+
+
+
+ {paragraph}
+
+
+
+
+ );
+};
+
+const CheckIcon: FunctionComponent<{
+ color: string;
+}> = ({ color }) => {
+ return (
+
+ );
+};
+
+const CosmosIcon: FunctionComponent = () => {
+ return (
+
+ );
+};
diff --git a/apps/extension/src/pages/register/finalize-key/index.tsx b/apps/extension/src/pages/register/finalize-key/index.tsx
index 8b24428fe9..b319d34803 100644
--- a/apps/extension/src/pages/register/finalize-key/index.tsx
+++ b/apps/extension/src/pages/register/finalize-key/index.tsx
@@ -54,7 +54,14 @@ export const FinalizeKeyScene: FunctionComponent<{
addressIndex: number;
};
};
- keystone?: MultiAccounts;
+ keystone?: MultiAccounts & {
+ bip44Path: {
+ account: number;
+ change: number;
+ addressIndex: number;
+ };
+ connectionType?: "USB" | "QR";
+ };
stepPrevious: number;
stepTotal: number;
}> = observer(
diff --git a/apps/extension/src/pages/register/index.tsx b/apps/extension/src/pages/register/index.tsx
index b0c91ab2d3..deabc41dba 100644
--- a/apps/extension/src/pages/register/index.tsx
+++ b/apps/extension/src/pages/register/index.tsx
@@ -35,7 +35,10 @@ import { useStore } from "../../stores";
import { useSearchParams } from "react-router-dom";
import * as KeplrWalletPrivate from "keplr-wallet-private";
import { BackUpPrivateKeyScene } from "./back-up-private-key";
-import { ConnectKeystoneScene } from "./connect-keystone";
+import {
+ ConnectKeystoneQRScene,
+ ConnectKeystoneUSBScene,
+} from "./connect-keystone";
import { ScanKeystoneScene } from "./connect-keystone/scan";
const Container = styled.div`
@@ -230,8 +233,13 @@ const RegisterPageImpl: FunctionComponent = observer(() => {
width: "40rem",
},
{
- name: "connect-keystone",
- element: ConnectKeystoneScene,
+ name: "connect-keystone-qr",
+ element: ConnectKeystoneQRScene,
+ width: "40rem",
+ },
+ {
+ name: "connect-keystone-usb",
+ element: ConnectKeystoneUSBScene,
width: "40rem",
},
{
diff --git a/apps/extension/src/pages/register/name-password-hardware/index.tsx b/apps/extension/src/pages/register/name-password-hardware/index.tsx
index 8a93d0550b..c25959d2e4 100644
--- a/apps/extension/src/pages/register/name-password-hardware/index.tsx
+++ b/apps/extension/src/pages/register/name-password-hardware/index.tsx
@@ -1,4 +1,4 @@
-import React, { FunctionComponent, useState } from "react";
+import React, { FunctionComponent, useEffect, useState } from "react";
import { RegisterSceneBox } from "../components/register-scene-box";
import { Box } from "../../../components/box";
import { FormNamePassword, useFormNamePassword } from "../components/form";
@@ -22,6 +22,7 @@ export const RegisterNamePasswordHardwareScene: FunctionComponent<{
const sceneTransition = useSceneTransition();
const intl = useIntl();
+ const [headerHasSet, setHeaderHasSet] = useState(false);
const header = useRegisterHeader();
useSceneEvents({
onWillVisible: () => {
@@ -33,14 +34,30 @@ export const RegisterNamePasswordHardwareScene: FunctionComponent<{
stepCurrent: 1,
stepTotal: type === "keystone" ? 4 : 3,
});
+ setHeaderHasSet(true);
},
});
+ const [keystoneWay, setKeystoneWay] = useState("USB");
+ const [isKeystoneUSB, setIsKeystoneUSB] = useState(true);
+ useEffect(() => {
+ if (headerHasSet && type === "keystone") {
+ const prev = header.header;
+ if ("stepTotal" in prev) {
+ if (isKeystoneUSB) {
+ // USB에서는 step 3개밖에 안 필요하더라...
+ header.setHeader({ ...prev, stepTotal: 3 });
+ } else {
+ header.setHeader({ ...prev, stepTotal: 4 });
+ }
+ }
+ }
+ }, [header, headerHasSet, isKeystoneUSB, type]);
+
const form = useFormNamePassword();
const [connectTo, setConnectTo] = useState("Cosmos");
-
- const bip44PathState = useBIP44PathState(type === "ledger");
+ const bip44PathState = useBIP44PathState(true);
const [isBIP44CardOpen, setIsBIP44CardOpen] = useState(false);
return (
@@ -57,7 +74,16 @@ export const RegisterNamePasswordHardwareScene: FunctionComponent<{
stepTotal: 3,
});
} else if (type === "keystone") {
- sceneTransition.push("connect-keystone", {
+ if (isKeystoneUSB) {
+ return sceneTransition.push("connect-keystone-usb", {
+ name: data.name,
+ password: data.password,
+ bip44Path: isKeystoneUSB ? bip44PathState.getPath() : undefined,
+ stepPrevious: 1,
+ stepTotal: 3,
+ });
+ }
+ return sceneTransition.push("connect-keystone-qr", {
name: data.name,
password: data.password,
stepPrevious: 1,
@@ -134,6 +160,70 @@ export const RegisterNamePasswordHardwareScene: FunctionComponent<{
) : undefined}
+ {type === "keystone" ? (
+
+
+
+ {
+ setKeystoneWay(key);
+ setIsKeystoneUSB(key === "USB");
+ }}
+ />
+
+ {isKeystoneUSB && (
+
+
+
+
+
+
+ {
+ setIsBIP44CardOpen(false);
+ }}
+ />
+
+
+ )}
+
+
+ ) : undefined}
diff --git a/apps/extension/src/pages/sign/components/keystone-usb-box.tsx b/apps/extension/src/pages/sign/components/keystone-usb-box.tsx
new file mode 100644
index 0000000000..31b662e39c
--- /dev/null
+++ b/apps/extension/src/pages/sign/components/keystone-usb-box.tsx
@@ -0,0 +1,48 @@
+import React, { FunctionComponent } from "react";
+import { VerticalCollapseTransition } from "../../../components/transition/vertical-collapse";
+import { Gutter } from "../../../components/gutter";
+import { GuideBox } from "../../../components/guide-box";
+import { useIntl } from "react-intl";
+
+export const KeystoneUSBBox: FunctionComponent<{
+ isKeystoneInteracting: boolean;
+ KeystoneInteractingError: Error | undefined;
+}> = ({ isKeystoneInteracting, KeystoneInteractingError }) => {
+ const intl = useIntl();
+ return (
+
+
+ {(() => {
+ if (KeystoneInteractingError) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ })()}
+
+ );
+};
diff --git a/apps/extension/src/pages/sign/cosmos/adr36.tsx b/apps/extension/src/pages/sign/cosmos/adr36.tsx
index 888be84607..375b016631 100644
--- a/apps/extension/src/pages/sign/cosmos/adr36.tsx
+++ b/apps/extension/src/pages/sign/cosmos/adr36.tsx
@@ -24,6 +24,7 @@ import { KeyRingService } from "@keplr-wallet/background";
import { handleExternalInteractionWithNoProceedNext } from "../../../utils";
import { MessageAdr36Icon } from "../../../components/icon";
import { ItemLogo } from "../../main/token-detail/msg-items/logo";
+import { KeystoneUSBBox } from "../components/keystone-usb-box";
export const SignCosmosADR36Page: FunctionComponent = observer(() => {
const { chainStore, signInteractionStore, uiConfigStore } = useStore();
@@ -108,6 +109,11 @@ export const SignCosmosADR36Page: FunctionComponent = observer(() => {
Error | undefined
>(undefined);
+ const isKeystoneUSB =
+ signInteractionStore.waitingData?.data.keyType === "keystone" &&
+ signInteractionStore.waitingData?.data.keyInsensitive["connectionType"] ===
+ "USB";
+
const [isKeystoneInteracting, setIsKeystoneInteracting] = useState(false);
const [keystoneUR, setKeystoneUR] = useState();
const keystoneScanResolve = useRef<(ur: KeystoneUR) => void>();
@@ -134,7 +140,9 @@ export const SignCosmosADR36Page: FunctionComponent = observer(() => {
isLoading:
signInteractionStore.isObsoleteInteraction(
signInteractionStore.waitingData?.id
- ) || isLedgerInteracting,
+ ) ||
+ isLedgerInteracting ||
+ isKeystoneInteracting,
onClick: async () => {
if (signInteractionStore.waitingData) {
const signDocWrapper =
@@ -163,6 +171,7 @@ export const SignCosmosADR36Page: FunctionComponent = observer(() => {
signInteractionStore.waitingData.data.keyType === "keystone"
) {
setIsKeystoneInteracting(true);
+ setKeystoneInteractingError(undefined);
const isEthSigning = KeyRingService.isEthermintLike(
chainStore.getChain(
signInteractionStore.waitingData.data.chainId
@@ -220,6 +229,7 @@ export const SignCosmosADR36Page: FunctionComponent = observer(() => {
}
} finally {
setIsLedgerInteracting(false);
+ setIsKeystoneInteracting(false);
}
}
},
@@ -414,27 +424,35 @@ export const SignCosmosADR36Page: FunctionComponent = observer(() => {
isInternal={signInteractionStore.waitingData.isInternal}
/>
) : null}
+ {isKeystoneUSB && (
+
+ )}
- {
- setIsKeystoneInteracting(false);
- }}
- onScan={(ur) => {
- if (keystoneScanResolve.current === undefined) {
- throw new Error("Keystone Scan Error");
- }
- keystoneScanResolve.current(ur);
- }}
- error={keystoneInteractingError}
- onCloseError={() => {
- if (keystoneInteractingError) {
+ {!isKeystoneUSB && (
+ {
setIsKeystoneInteracting(false);
- }
- setKeystoneInteractingError(undefined);
- }}
- />
+ }}
+ onScan={(ur) => {
+ if (keystoneScanResolve.current === undefined) {
+ throw new Error("Keystone Scan Error");
+ }
+ keystoneScanResolve.current(ur);
+ }}
+ error={keystoneInteractingError}
+ onCloseError={() => {
+ if (keystoneInteractingError) {
+ setIsKeystoneInteracting(false);
+ }
+ setKeystoneInteractingError(undefined);
+ }}
+ />
+ )}
);
});
diff --git a/apps/extension/src/pages/sign/cosmos/tx/view.tsx b/apps/extension/src/pages/sign/cosmos/tx/view.tsx
index 75ef21984b..9a4f701bef 100644
--- a/apps/extension/src/pages/sign/cosmos/tx/view.tsx
+++ b/apps/extension/src/pages/sign/cosmos/tx/view.tsx
@@ -30,6 +30,7 @@ import { handleCosmosPreSign } from "../../utils/handle-cosmos-sign";
import { KeplrError } from "@keplr-wallet/router";
import { ErrModuleLedgerSign } from "../../utils/ledger-types";
import { LedgerGuideBox } from "../../components/ledger-guide-box";
+import { KeystoneUSBBox } from "../../components/keystone-usb-box";
import { Gutter } from "../../../../components/gutter";
import { GuideBox } from "../../../../components/guide-box";
import { FormattedMessage, useIntl } from "react-intl";
@@ -338,6 +339,10 @@ export const CosmosTxView: FunctionComponent<{
Error | undefined
>(undefined);
+ const isKeystonUSB =
+ interactionData.data.keyType === "keystone" &&
+ interactionData.data.keyInsensitive["connectionType"] === "USB";
+
const [isKeystoneInteracting, setIsKeystoneInteracting] = useState(false);
const [keystoneUR, setKeystoneUR] = useState();
const keystoneScanResolve = useRef<(ur: KeystoneUR) => void>();
@@ -386,6 +391,7 @@ export const CosmosTxView: FunctionComponent<{
};
} else if (interactionData.data.keyType === "keystone") {
setIsKeystoneInteracting(true);
+ setKeystoneInteractingError(undefined);
const isEthSigning = KeyRingService.isEthermintLike(
chainStore.getChain(chainId)
);
@@ -460,6 +466,7 @@ export const CosmosTxView: FunctionComponent<{
}
} finally {
setIsLedgerInteracting(false);
+ setIsKeystoneInteracting(false);
}
}
};
@@ -499,7 +506,8 @@ export const CosmosTxView: FunctionComponent<{
disabled: buttonDisabled,
isLoading:
signInteractionStore.isObsoleteInteraction(interactionData.id) ||
- isLedgerInteracting,
+ isLedgerInteracting ||
+ isKeystoneInteracting,
onClick: approve,
}}
>
@@ -735,27 +743,35 @@ export const CosmosTxView: FunctionComponent<{
ledgerInteractingError={ledgerInteractingError}
isInternal={interactionData.isInternal}
/>
+ {isKeystonUSB && (
+
+ )}
- {
- setIsKeystoneInteracting(false);
- }}
- onScan={(ur) => {
- if (keystoneScanResolve.current === undefined) {
- throw new Error("Keystone Scan Error");
- }
- keystoneScanResolve.current(ur);
- }}
- error={keystoneInteractingError}
- onCloseError={() => {
- if (keystoneInteractingError) {
+ {!isKeystonUSB && (
+ {
setIsKeystoneInteracting(false);
- }
- setKeystoneInteractingError(undefined);
- }}
- />
+ }}
+ onScan={(ur) => {
+ if (keystoneScanResolve.current === undefined) {
+ throw new Error("Keystone Scan Error");
+ }
+ keystoneScanResolve.current(ur);
+ }}
+ error={keystoneInteractingError}
+ onCloseError={() => {
+ if (keystoneInteractingError) {
+ setIsKeystoneInteracting(false);
+ }
+ setKeystoneInteractingError(undefined);
+ }}
+ />
+ )}
);
});
diff --git a/apps/extension/src/pages/sign/ethereum/view.tsx b/apps/extension/src/pages/sign/ethereum/view.tsx
index ef9002a555..8db7762ec9 100644
--- a/apps/extension/src/pages/sign/ethereum/view.tsx
+++ b/apps/extension/src/pages/sign/ethereum/view.tsx
@@ -19,6 +19,7 @@ import { KeplrError } from "@keplr-wallet/router";
import { ErrModuleLedgerSign } from "../utils/ledger-types";
import { Buffer } from "buffer/";
import { LedgerGuideBox } from "../components/ledger-guide-box";
+import { KeystoneUSBBox } from "../components/keystone-usb-box";
import { EthSignType } from "@keplr-wallet/types";
import {
handleEthereumPreSignByKeystone,
@@ -309,6 +310,10 @@ export const EthereumSigningView: FunctionComponent<{
Error | undefined
>(undefined);
+ const isKeystonUSB =
+ interactionData.data.keyType === "keystone" &&
+ interactionData.data.keyInsensitive["connectionType"] === "USB";
+
const [isKeystoneInteracting, setIsKeystoneInteracting] = useState(false);
const [keystoneUR, setKeystoneUR] = useState();
const keystoneScanResolve = useRef<(ur: KeystoneUR) => void>();
@@ -355,7 +360,9 @@ export const EthereumSigningView: FunctionComponent<{
isLoading:
signEthereumInteractionStore.isObsoleteInteraction(
interactionData.id
- ) || isLedgerInteracting,
+ ) ||
+ isLedgerInteracting ||
+ isKeystoneInteracting,
onClick: async () => {
try {
let signature;
@@ -371,6 +378,7 @@ export const EthereumSigningView: FunctionComponent<{
);
} else if (interactionData.data.keyType === "keystone") {
setIsKeystoneInteracting(true);
+ setKeystoneInteractingError(undefined);
signature = await handleEthereumPreSignByKeystone(
interactionData,
Buffer.from(signingDataText),
@@ -434,6 +442,7 @@ export const EthereumSigningView: FunctionComponent<{
}
} finally {
setIsLedgerInteracting(false);
+ setIsKeystoneInteracting(false);
}
},
}}
@@ -687,27 +696,35 @@ export const EthereumSigningView: FunctionComponent<{
ledgerInteractingError={ledgerInteractingError}
isInternal={interactionData.isInternal}
/>
+ {isKeystonUSB && (
+
+ )}
- {
- setIsKeystoneInteracting(false);
- }}
- onScan={(ur) => {
- if (keystoneScanResolve.current === undefined) {
- throw new Error("Keystone Scan Error");
- }
- keystoneScanResolve.current(ur);
- }}
- error={keystoneInteractingError}
- onCloseError={() => {
- if (keystoneInteractingError) {
+ {!isKeystonUSB && (
+ {
setIsKeystoneInteracting(false);
- }
- setKeystoneInteractingError(undefined);
- }}
- />
+ }}
+ onScan={(ur) => {
+ if (keystoneScanResolve.current === undefined) {
+ throw new Error("Keystone Scan Error");
+ }
+ keystoneScanResolve.current(ur);
+ }}
+ error={keystoneInteractingError}
+ onCloseError={() => {
+ if (keystoneInteractingError) {
+ setIsKeystoneInteracting(false);
+ }
+ setKeystoneInteractingError(undefined);
+ }}
+ />
+ )}
);
});
diff --git a/apps/extension/src/pages/sign/utils/handle-cosmos-sign.ts b/apps/extension/src/pages/sign/utils/handle-cosmos-sign.ts
index b3ce1d3d4f..6d1e6abc1d 100644
--- a/apps/extension/src/pages/sign/utils/handle-cosmos-sign.ts
+++ b/apps/extension/src/pages/sign/utils/handle-cosmos-sign.ts
@@ -14,6 +14,7 @@ import KeystoneSDK, {
KeystoneEvmSDK,
UR,
utils,
+ UREncoder,
} from "@keystonehq/keystone-sdk";
import {
ErrInvalidPublicKey,
@@ -21,10 +22,16 @@ import {
ErrInvalidSignature,
ErrInvalidSigner,
ErrModuleKeystoneSign,
+ ErrKeystoneUSBCommunication,
KeystoneKeys,
KeystoneUR,
getPathFromPubKey,
} from "./keystone";
+import {
+ createKeystoneTransport,
+ handleKeystoneUSBError,
+} from "../../../utils/keystone";
+import Base from "@keystonehq/hw-app-base";
import { PlainObject } from "@keplr-wallet/background";
import { KeplrError } from "@keplr-wallet/router";
@@ -141,6 +148,8 @@ export const handleCosmosPreSign = async (
: serializeSignDoc(signDocWrapper.aminoSignDoc)
).toString("hex");
const xfp = interactionData.data.keyInsensitive["xfp"] as string;
+ const isUSB =
+ interactionData.data.keyInsensitive["connectionType"] === "USB";
if (isEthSigning) {
ur = keystoneSDK.evm.generateSignRequest({
requestId,
@@ -181,17 +190,40 @@ export const handleCosmosPreSign = async (
],
});
}
- await keystoneOptions.displayQRCode({
- type: ur.type,
- cbor: ur.cbor.toString("hex"),
- });
- const scanResult = await keystoneOptions.scanQRCode();
+
+ // Keystone usb signing
+ let urResult: KeystoneUR;
+ if (isUSB) {
+ try {
+ const transport = await createKeystoneTransport();
+ const URString = new UREncoder(ur, Infinity).nextPart().toUpperCase();
+ const baseApp = new Base(transport as any);
+ const response = await baseApp.sendURRequest(URString);
+ urResult = {
+ type: response.type,
+ cbor: response.cbor.toString("hex"),
+ } as KeystoneUR;
+ } catch (e) {
+ throw new KeplrError(
+ ErrModuleKeystoneSign,
+ ErrKeystoneUSBCommunication,
+ handleKeystoneUSBError(e)
+ );
+ }
+ } else {
+ await keystoneOptions.displayQRCode({
+ type: ur.type,
+ cbor: ur.cbor.toString("hex"),
+ });
+ urResult = await keystoneOptions.scanQRCode();
+ }
+
let signResult;
try {
signResult = keystoneSDK[
isEthSigning ? "evm" : "cosmos"
].parseSignature(
- new UR(Buffer.from(scanResult.cbor, "hex"), scanResult.type)
+ new UR(Buffer.from(urResult.cbor, "hex"), urResult.type)
);
} catch (e) {
throw new KeplrError(
diff --git a/apps/extension/src/pages/sign/utils/handle-eth-sign.ts b/apps/extension/src/pages/sign/utils/handle-eth-sign.ts
index b04f2de4d2..a343589036 100644
--- a/apps/extension/src/pages/sign/utils/handle-eth-sign.ts
+++ b/apps/extension/src/pages/sign/utils/handle-eth-sign.ts
@@ -2,6 +2,8 @@ import { SignEthereumInteractionStore } from "@keplr-wallet/stores-core";
import { EthSignType } from "@keplr-wallet/types";
import Transport from "@ledgerhq/hw-transport";
import TransportWebUSB from "@ledgerhq/hw-transport-webusb";
+import { UREncoder } from "@keystonehq/keystone-sdk";
+import Base from "@keystonehq/hw-app-base";
import { KeplrError } from "@keplr-wallet/router";
import {
ErrCodeDeviceLocked,
@@ -28,9 +30,15 @@ import {
encodeEthMessage,
getEthDataTypeFromSignType,
getPathFromPubKey,
+ ErrModuleKeystoneSign,
+ ErrKeystoneUSBCommunication,
} from "./keystone";
import KeystoneSDK, { UR, utils } from "@keystonehq/keystone-sdk";
import { EthermintChainIdHelper } from "@keplr-wallet/cosmos";
+import {
+ createKeystoneTransport,
+ handleKeystoneUSBError,
+} from "../../../utils/keystone";
export interface LedgerOptions {
useWebHID: boolean;
@@ -130,13 +138,37 @@ export const handleEthereumPreSignByKeystone = async (
chainId: evmChainId,
address,
});
- await options.displayQRCode({
- type: ur.type,
- cbor: ur.cbor.toString("hex"),
- });
- const scanResult = await options.scanQRCode();
+
+ const isUSB = interactionData.data.keyInsensitive["connectionType"] === "USB";
+
+ let urResult: KeystoneUR;
+ if (isUSB) {
+ try {
+ const transport = await createKeystoneTransport();
+ const URString = new UREncoder(ur, Infinity).nextPart().toUpperCase();
+ const baseApp = new Base(transport as any);
+ const response = await baseApp.sendURRequest(URString);
+ urResult = {
+ type: response.type,
+ cbor: response.cbor.toString("hex"),
+ } as KeystoneUR;
+ } catch (e) {
+ throw new KeplrError(
+ ErrModuleKeystoneSign,
+ ErrKeystoneUSBCommunication,
+ handleKeystoneUSBError(e)
+ );
+ }
+ } else {
+ await options.displayQRCode({
+ type: ur.type,
+ cbor: ur.cbor.toString("hex"),
+ });
+ urResult = await options.scanQRCode();
+ }
+
const signResult = keystoneSDK.eth.parseSignature(
- new UR(Buffer.from(scanResult.cbor, "hex"), scanResult.type)
+ new UR(Buffer.from(urResult.cbor, "hex"), urResult.type)
);
if (signResult.requestId !== requestId) {
throw new Error("Invalid request id");
diff --git a/apps/extension/src/pages/sign/utils/keystone.ts b/apps/extension/src/pages/sign/utils/keystone.ts
index 25bba14369..1821f0f7df 100644
--- a/apps/extension/src/pages/sign/utils/keystone.ts
+++ b/apps/extension/src/pages/sign/utils/keystone.ts
@@ -81,3 +81,4 @@ export const ErrInvalidSigner = 1;
export const ErrInvalidRequestId = 2;
export const ErrInvalidPublicKey = 3;
export const ErrInvalidSignature = 4;
+export const ErrKeystoneUSBCommunication = 5;
diff --git a/apps/extension/src/utils/keystone.ts b/apps/extension/src/utils/keystone.ts
new file mode 100644
index 0000000000..0ddbfb2923
--- /dev/null
+++ b/apps/extension/src/utils/keystone.ts
@@ -0,0 +1,39 @@
+import {
+ TransportWebUSB,
+ getKeystoneDevices,
+ StatusCode,
+} from "@keystonehq/hw-transport-webusb";
+
+export async function createKeystoneTransport() {
+ if ((await getKeystoneDevices()).length <= 0) {
+ try {
+ await TransportWebUSB.requestPermission();
+ } catch (e) {
+ throw new Error("USB_PERMISSIONS_NOT_AVAILABLE");
+ }
+ }
+
+ const transport = await TransportWebUSB.connect({
+ timeout: 100000,
+ });
+ await transport.close();
+ return transport;
+}
+
+export function handleKeystoneUSBError(error: {
+ message: string;
+ transportErrorCode: StatusCode;
+}) {
+ if (error.message === "USB_PERMISSIONS_NOT_AVAILABLE") {
+ return "Missing browser permissions";
+ }
+
+ if (error.transportErrorCode === StatusCode.PRS_PARSING_REJECTED) {
+ return `Please reconnect the Keystone hardware wallet and reauthorize.`;
+ }
+ if (error.transportErrorCode === StatusCode.PRS_PARSING_DISALLOWED) {
+ return "Please reconnect the Keystone on home screen and reauthorize.";
+ }
+
+ return "Communication with the Keystone device failed. Please ensure the Keystone is connected, and try again.";
+}
diff --git a/packages/background/src/keyring-keystone/service.ts b/packages/background/src/keyring-keystone/service.ts
index 1592925616..81ea1632ff 100644
--- a/packages/background/src/keyring-keystone/service.ts
+++ b/packages/background/src/keyring-keystone/service.ts
@@ -31,6 +31,8 @@ export class KeyRingKeystoneService {
device: multiAccounts.device,
deviceId: multiAccounts.deviceId,
keys,
+ bip44Path: multiAccounts.bip44Path,
+ connectionType: multiAccounts.connectionType,
},
sensitive: {},
});
diff --git a/packages/background/src/keyring-keystone/types.ts b/packages/background/src/keyring-keystone/types.ts
index 4ccd8a8b94..ce63ddbaaf 100644
--- a/packages/background/src/keyring-keystone/types.ts
+++ b/packages/background/src/keyring-keystone/types.ts
@@ -1,8 +1,14 @@
export interface MultiAccounts {
masterFingerprint: string;
keys: Account[];
+ bip44Path: {
+ account: number;
+ change: number;
+ addressIndex: number;
+ };
device?: string;
deviceId?: string;
+ connectionType?: "USB" | "QR";
}
export interface Account {
diff --git a/yarn.lock b/yarn.lock
index 4c19db6d62..8bdd94c3c0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8753,6 +8753,8 @@ __metadata:
"@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
@@ -9483,6 +9485,17 @@ __metadata:
languageName: node
linkType: hard
+"@keystonehq/bc-ur-registry@npm:0.7.0":
+ version: 0.7.0
+ resolution: "@keystonehq/bc-ur-registry@npm:0.7.0"
+ dependencies:
+ "@ngraveio/bc-ur": ^1.1.5
+ bs58check: ^2.1.2
+ tslib: ^2.3.0
+ checksum: d6017e8fda67fc01e28aa1c047b20cce8f07b026f110a5771920879fbd658b845f529b054d1dce2fbabadcfd8da47a2160ab50c73f0bd56678aab4d83899ffcc
+ languageName: node
+ linkType: hard
+
"@keystonehq/bc-ur-registry@npm:^0.4.4":
version: 0.4.4
resolution: "@keystonehq/bc-ur-registry@npm:0.4.4"
@@ -9516,6 +9529,46 @@ __metadata:
languageName: node
linkType: hard
+"@keystonehq/hw-app-base@npm:0.1.1":
+ version: 0.1.1
+ resolution: "@keystonehq/hw-app-base@npm:0.1.1"
+ dependencies:
+ "@keystonehq/bc-ur-registry": 0.7.0
+ "@keystonehq/hw-transport-error": ^0.0.2
+ "@keystonehq/hw-transport-webusb": ^0.4.0-beta.0
+ "@ngraveio/bc-ur": ^1.1.6
+ uuid: ^8.3.2
+ checksum: 8f8b51f473d8f0998e0124dd73cfa2c212952e1e814369137e4788e2985a2fdd0f1d84e48ddafd70132e60c06ef2276a3637324c265a40eae9369b8d525a0fe7
+ languageName: node
+ linkType: hard
+
+"@keystonehq/hw-transport-error@npm:^0.0.2":
+ version: 0.0.2
+ resolution: "@keystonehq/hw-transport-error@npm:0.0.2"
+ checksum: 681c3344fdd5e59b63ee522a4539fff926c0c050dc645070dfaa4ac73b5c913312667aa491ec2174252e553d83f4c7e90e2dad2a80e9cbcdb4fa40e86565ccf2
+ languageName: node
+ linkType: hard
+
+"@keystonehq/hw-transport-webusb@npm:0.4.0":
+ version: 0.4.0
+ resolution: "@keystonehq/hw-transport-webusb@npm:0.4.0"
+ dependencies:
+ "@keystonehq/hw-transport-error": ^0.0.2
+ buffer: ^6.0.3
+ checksum: 7c56d7b9b73596306be50aae6b5661905a88fb018385b6074c0f18a04efbf5871f7fe93b957c6fe8f966f7c829ff085df444c9fbd23967c0ef2df4a7f9f7e4ae
+ languageName: node
+ linkType: hard
+
+"@keystonehq/hw-transport-webusb@npm:^0.4.0-beta.0":
+ version: 0.4.0-beta.0
+ resolution: "@keystonehq/hw-transport-webusb@npm:0.4.0-beta.0"
+ dependencies:
+ "@keystonehq/hw-transport-error": ^0.0.2
+ buffer: ^6.0.3
+ checksum: 70c85917bb234ae6bd420ecb0744647ca8358f67b1eb043814f45567a0eb1b73ac6c8699cbdc877ad77f9d0a70d8ec5859b73aa4bfdfff8b29fad7dee2fce216
+ languageName: node
+ linkType: hard
+
"@keystonehq/keystone-sdk@npm:^0.2.3":
version: 0.2.3
resolution: "@keystonehq/keystone-sdk@npm:0.2.3"