diff --git a/README.md b/README.md index ff2ec83..c9ef252 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,21 @@ You can adjust the log level by changing `info` to `debug`, `warn`, or `error` a Log messages will appear in the terminal where you run the command. +## Hardware Integration Notes + +Keeper Desktop uses a single hardware integration path: + +1. **Rust HWI sidecar path** (Trezor / Ledger / BitBox02 / OneKey / etc.) + - Routed through Tauri commands in `src-tauri/src/main.rs` (`hwi_*` commands). + - Backed by the `hwi` sidecar binary configured in `src-tauri/tauri.conf.json`. + - No WebUSB fallback path in the desktop frontend. + +### Current OneKey capability scope + +- Supported: connect, share xpubs, health check, sign transaction, verify address +- Not supported: register multisig +- Not supported (for now): complex miniscript policy address verification (timelock / nested thresh) + ## Code Quality and CI We use several tools to maintain code quality and consistency: diff --git a/package-lock.json b/package-lock.json index 97e735d..db32e95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@tanstack/react-query": "^5.52.1", "@tauri-apps/api": "^1", + "lodash": "^4.17.23", "qrcode.react": "^3.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -2058,7 +2059,8 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/available-typed-arrays": { "version": "1.0.7", @@ -2076,13 +2078,14 @@ } }, "node_modules/axios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "dev": true, + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -2171,6 +2174,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2214,6 +2231,7 @@ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -2366,6 +2384,7 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -2382,6 +2401,21 @@ "node": ">=6.0.0" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2461,13 +2495,11 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -2507,10 +2539,11 @@ } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -2519,14 +2552,16 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -3148,13 +3183,16 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dev": true, + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -3227,16 +3265,22 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -3245,6 +3289,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-symbol-description": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", @@ -3411,12 +3469,13 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3462,10 +3521,11 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4074,6 +4134,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4100,6 +4166,16 @@ "yallist": "^3.0.2" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4127,6 +4203,7 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -4136,6 +4213,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, diff --git a/package.json b/package.json index e175ebc..1e25bdb 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "dependencies": { "@tanstack/react-query": "^5.52.1", "@tauri-apps/api": "^1", + "lodash": "^4.17.23", "qrcode.react": "^3.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/assets/hww/icons-modal/onekey.svg b/src/assets/hww/icons-modal/onekey.svg new file mode 100644 index 0000000..f06e410 --- /dev/null +++ b/src/assets/hww/icons-modal/onekey.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/assets/hww/icons/onekey.svg b/src/assets/hww/icons/onekey.svg new file mode 100644 index 0000000..9069067 --- /dev/null +++ b/src/assets/hww/icons/onekey.svg @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/src/helpers/devices.ts b/src/helpers/devices.ts index 83bb241..03eede2 100644 --- a/src/helpers/devices.ts +++ b/src/helpers/devices.ts @@ -1,11 +1,13 @@ import bitboxIcon from "../assets/hww/icons/bitbox.svg"; import trezorIcon from "../assets/hww/icons/trezor.svg"; import ledgerIcon from "../assets/hww/icons/ledger.svg"; +import onekeyIcon from "../assets/hww/icons/onekey.svg"; import coldcardIcon from "../assets/hww/icons/coldcard.svg"; import jadeIcon from "../assets/hww/icons/jade.svg"; import ledgerIconModal from "../assets/hww/icons-modal/ledger.svg"; import trezorIconModal from "../assets/hww/icons-modal/trezor.svg"; import bitboxIconModal from "../assets/hww/icons-modal/bitbox.svg"; +import onekeyIconModal from "../assets/hww/icons-modal/onekey.svg"; import coldcardIconModal from "../assets/hww/icons-modal/coldcard.svg"; import jadeIconModal from "../assets/hww/icons-modal/jade.svg"; @@ -27,6 +29,10 @@ const HWI_DEVICES = { icon: ledgerIcon, name: "Ledger", }, + onekey: { + icon: onekeyIcon, + name: "OneKey", + }, trezor: { icon: trezorIcon, name: "Trezor", @@ -56,11 +62,54 @@ interface HWIDevice { interface DeviceContent { icon: string; - content: Record; + content: Partial>; } type HWIDeviceType = keyof typeof HWI_DEVICES; +type PinInteractionType = "host" | "device"; + +const ONEKEY_HOST_PIN_MODELS = new Set([ + "classic", + "classic1s", + "classicpure", + "onekey1", + "onekeyclassic", + "onekeyclassic1s", + "onekeyclassicpure", +]); + +const ONEKEY_DEVICE_PIN_MODELS = new Set([ + "touch", + "pro", + "t", + "onekeyt", + "onekeytouch", + "onekeypro", +]); + +const normalizeDeviceModel = (model: string | null | undefined) => + (model ?? "").toLowerCase().replace(/[^a-z0-9]/g, ""); + +const getPinInteractionType = ( + deviceType: HWIDeviceType, + model: string | null | undefined, +): PinInteractionType => { + if (deviceType !== "onekey") { + return "host"; + } + + const normalizedModel = normalizeDeviceModel(model); + if (ONEKEY_DEVICE_PIN_MODELS.has(normalizedModel)) { + return "device"; + } + if (ONEKEY_HOST_PIN_MODELS.has(normalizedModel)) { + return "host"; + } + + return "host"; +}; + const deviceContent: Record = { ledger: { icon: ledgerIconModal, @@ -101,6 +150,39 @@ const deviceContent: Record = { }, }, }, + onekey: { + icon: onekeyIconModal, + content: { + connect: { + text: "Your mobile app is trying to connect to your OneKey. Please connect your OneKey to your computer via USB, unlock it, and open the Bitcoin app on the device.", + list: [], + }, + shareXpubs: { + text: "Keep your OneKey connected to the computer until setup is completed.", + list: [], + }, + healthCheck: { + text: "Your Mobile app is trying to perform a health check. Keep your OneKey connected to the computer until the operation is completed.", + list: [ + "Health check ensures the device holds the keys registered in the mobile app", + ], + }, + signTx: { + text: "Please sign the transaction by approving it on your OneKey.", + list: [ + "Make sure to verify the address and amount shown on your OneKey screen.", + "Only approve the request if the OneKey screen matches the expected details in Keeper.", + ], + }, + verifyAddress: { + text: "Clicking below will display the address on your OneKey device, make sure to read it carefully and verify that it matches the address on your Keeper mobile app.", + list: [ + "Only use the address from Keeper mobile app if it matches the address displayed on your OneKey.", + "In case the address on your OneKey is different than the address on the Keeper mobile app please contact support immediately.", + ], + }, + }, + }, trezor: { icon: trezorIconModal, content: { @@ -259,7 +341,9 @@ export { HWI_DEVICES, HWI_ACTIONS, deviceContent, + getPinInteractionType, type HWI_ACTION, + type PinInteractionType, type HWIDeviceType, type HWIDevice, type NetworkType, diff --git a/src/hooks/useDeviceActions.tsx b/src/hooks/useDeviceActions.tsx index bf8ab94..b45a308 100644 --- a/src/hooks/useDeviceActions.tsx +++ b/src/hooks/useDeviceActions.tsx @@ -72,6 +72,10 @@ export const useDeviceActions = ({ onActionSuccess(); break; case "registerMultisig": + if (deviceType === "onekey") { + onError("Register multisig is not supported on OneKey"); + return; + } if (!descriptor && !miniscriptPolicy) { onError("Descriptor or miniscript policy is required"); return; diff --git a/src/hooks/useModalState.tsx b/src/hooks/useModalState.tsx index 9c12977..f5fbd6a 100644 --- a/src/hooks/useModalState.tsx +++ b/src/hooks/useModalState.tsx @@ -7,6 +7,7 @@ export type ModalType = | "multipleDevices" | "error" | "pin" + | "onekeyPin" | null; const useModalState = () => { diff --git a/src/modals/DeviceActionModal/DeviceActionModal.tsx b/src/modals/DeviceActionModal/DeviceActionModal.tsx index 756a8a5..af629e0 100644 --- a/src/modals/DeviceActionModal/DeviceActionModal.tsx +++ b/src/modals/DeviceActionModal/DeviceActionModal.tsx @@ -34,14 +34,26 @@ interface DeviceActionModalProps { onError: (error: string) => void; } -const actionTitle = (deviceType: HWIDeviceType) => ({ - connect: `Connect ${HWI_DEVICES[deviceType].name}`, - shareXpubs: `Setting up ${HWI_DEVICES[deviceType].name}`, - healthCheck: `${HWI_DEVICES[deviceType].name} Health Check`, - signTx: "Sign Transaction", - registerMultisig: `Register Multisig on ${HWI_DEVICES[deviceType].name}`, - verifyAddress: `Verify Address on your ${HWI_DEVICES[deviceType].name}`, -}); +const getActionTitle = (deviceType: HWIDeviceType, actionType: HWI_ACTION) => { + if (actionType === "registerMultisig" && deviceType === "onekey") { + return "Unsupported Action"; + } + + switch (actionType) { + case "connect": + return `Connect ${HWI_DEVICES[deviceType].name}`; + case "shareXpubs": + return `Setting up ${HWI_DEVICES[deviceType].name}`; + case "healthCheck": + return `${HWI_DEVICES[deviceType].name} Health Check`; + case "signTx": + return "Sign Transaction"; + case "registerMultisig": + return `Register Multisig on ${HWI_DEVICES[deviceType].name}`; + case "verifyAddress": + return `Verify Address on your ${HWI_DEVICES[deviceType].name}`; + } +}; const DeviceActionModal = ({ isOpen, @@ -84,6 +96,10 @@ const DeviceActionModal = ({ const iconSrc = isVerifyAddress ? verifyAddressIcon : content.icon; const modalContent = useMemo(() => { + const actionContent = content.content[actionType] ?? { + text: "Operation not supported on this device", + list: [], + }; const iconStyle = isVerifyAddress ? { width: "173px", height: "137px", marginBottom: "-20px" } : {}; @@ -93,7 +109,7 @@ const DeviceActionModal = ({ deviceType === "coldcard" && actionType === "registerMultisig" ? "Please approve the registration of the wallet on the connected Coldcard device" - : content.content[actionType].text; + : actionContent.text; const listContent = miniscriptPolicy && @@ -102,7 +118,7 @@ const DeviceActionModal = ({ ? [ "Make sure to verify the public keys and wallet details shown on the Coldcard screen match the expected public keys of your cosigners and wallet details.", ] - : content.content[actionType].list; + : actionContent.list; const hasListItems = listContent.length > 0; @@ -123,7 +139,7 @@ const DeviceActionModal = ({ marginLeft: hasListItems ? "25px" : "0px", }} > - {actionTitle(deviceType)[actionType]} + {getActionTitle(deviceType, actionType)} ), content: ( diff --git a/src/modals/ModalManager.tsx b/src/modals/ModalManager.tsx index a6058c7..657945f 100644 --- a/src/modals/ModalManager.tsx +++ b/src/modals/ModalManager.tsx @@ -4,6 +4,7 @@ import { DeviceNotFoundModal, MultipleDevicesModal, ErrorModal, + OneKeyPinModal, TrezorPinModal, } from "./index"; import { @@ -30,6 +31,7 @@ interface ModalsManagerProps { hmac: string | null; expectedAddress: string | null; pairingCode: string | null; + currentDevice: HWIDevice | null; errorMessage: string; handleConnectResult: (devices: HWIDevice[]) => Promise; handleActionSuccess: () => void; @@ -54,6 +56,7 @@ const ModalsManager = ({ hmac, expectedAddress, pairingCode, + currentDevice, errorMessage, handleConnectResult, handleActionSuccess, @@ -125,7 +128,19 @@ const ModalsManager = ({ /> { + openModalHandler("deviceActionSuccess"); + }} + /> + + { openModalHandler("deviceActionSuccess"); diff --git a/src/modals/OneKeyPinModal/OneKeyPinModal.module.css b/src/modals/OneKeyPinModal/OneKeyPinModal.module.css new file mode 100644 index 0000000..68c5b65 --- /dev/null +++ b/src/modals/OneKeyPinModal/OneKeyPinModal.module.css @@ -0,0 +1,71 @@ +.errorContainer { + position: absolute; + top: 0; + left: 0; + right: 0; + display: flex; + padding: 10px; + min-width: 180px; + max-width: 250px; + opacity: 0; + transition: opacity 0.3s ease-in-out; + pointer-events: none; +} + +.errorContainer.show { + opacity: 1; +} + +.error { + background-color: #e54545; + color: white; + font-size: 12px; + padding: 10px 20px; + border-radius: 6px; + display: flex; + align-items: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + width: 100%; +} + +.errorIcon { + width: 21px; + height: 21px; + margin-right: 10px; +} + +.text { + width: 220px; + margin: 0 auto; + text-align: center; +} + +.model { + margin-top: 12px; + font-size: 12px; + color: #3e524d; +} + +.continueButton { + margin-top: 12px; +} + +.continueButton:disabled { + opacity: 0.5; +} + +.loadingSpinner { + width: 24px; + height: 24px; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} diff --git a/src/modals/OneKeyPinModal/OneKeyPinModal.tsx b/src/modals/OneKeyPinModal/OneKeyPinModal.tsx new file mode 100644 index 0000000..f0f08ba --- /dev/null +++ b/src/modals/OneKeyPinModal/OneKeyPinModal.tsx @@ -0,0 +1,134 @@ +import { useState } from "react"; +import BaseModal from "../BaseModal/BaseModal"; +import styles from "./OneKeyPinModal.module.css"; +import baseStyles from "../BaseModal/BaseModal.module.css"; +import loader from "../../assets/loader.svg"; +import ErrorIcon from "../../assets/error-popup-icon.svg"; +import hwiService from "../../services/hwiService"; +import { + deviceContent, + HWI_DEVICES, + HWIDevice, + HWIDeviceType, + NetworkType, +} from "../../helpers/devices"; + +interface OneKeyPinModalProps { + isOpen: boolean; + deviceType: HWIDeviceType; + network: NetworkType | null; + model: string | null; + onClose: () => void; + onSuccess: () => void; +} + +const OneKeyPinModal = ({ + isOpen, + deviceType, + network, + model, + onClose, + onSuccess, +}: OneKeyPinModalProps) => { + const [error, setError] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + const showError = (message: string) => { + setError(message); + const timer = setTimeout(() => { + setError(""); + }, 4000); + return () => clearTimeout(timer); + }; + + const handleUnlocked = async () => { + if (!network) { + return showError("Network is not set"); + } + + setIsLoading(true); + await new Promise((resolve) => setTimeout(resolve, 100)); + + try { + const devices = await hwiService.fetchDevices( + deviceType, + network.toLowerCase(), + ); + const unlockedDevice = devices.find( + (device: HWIDevice) => !device.needs_pin_sent, + ); + + if (!unlockedDevice) { + return showError( + "Device is still locked. Please finish PIN entry on your device and retry.", + ); + } + + await hwiService.setHWIClient( + unlockedDevice.fingerprint, + deviceType, + network.toLowerCase(), + ); + onSuccess(); + } catch { + return showError("Failed to verify device unlock. Please try again."); + } finally { + setIsLoading(false); + } + }; + + const deviceName = HWI_DEVICES[deviceType].name; + + const modalContent = { + image: ( + {deviceName} + ), + title: ( +

+ Unlock {deviceName} on Device +

+ ), + content: ( + <> +
+
+ Error + {error} +
+
+

+ Please enter your PIN directly on the {deviceName} screen. After the + device is unlocked, click the button below. +

+ {model &&

Model: {model}

} + + ), + button: ( + + ), + }; + + return ( + + ); +}; + +export default OneKeyPinModal; diff --git a/src/modals/TrezorPinModal/TrezorPinModal.tsx b/src/modals/TrezorPinModal/TrezorPinModal.tsx index 9e11a05..0658c85 100644 --- a/src/modals/TrezorPinModal/TrezorPinModal.tsx +++ b/src/modals/TrezorPinModal/TrezorPinModal.tsx @@ -4,12 +4,14 @@ import styles from "./TrezorPinModal.module.css"; import baseStyles from "../BaseModal/BaseModal.module.css"; import loader from "../../assets/loader.svg"; import TrezorIcon from "../../assets/hww/icons-modal/trezor.svg"; +import OneKeyIcon from "../../assets/hww/icons-modal/onekey.svg"; import ErrorIcon from "../../assets/error-popup-icon.svg"; import hwiService from "../../services/hwiService"; -import { NetworkType } from "../../helpers/devices"; +import { HWIDeviceType, NetworkType } from "../../helpers/devices"; interface TrezorPinModalProps { isOpen: boolean; + deviceType: HWIDeviceType; network: NetworkType | null; onClose: () => void; onSuccess: () => void; @@ -17,6 +19,7 @@ interface TrezorPinModalProps { const TrezorPinModal = ({ isOpen, + deviceType, network, onClose, onSuccess, @@ -30,6 +33,9 @@ const TrezorPinModal = ({ }; const handlePinSubmit = async () => { + if (!network) { + return showError("Network is not set"); + } if (pin.length < 4) { return showError("PIN must be at least 4 digits"); } @@ -38,13 +44,17 @@ const TrezorPinModal = ({ try { await hwiService.sendPin(pin); const devices = await hwiService.fetchDevices( - "trezor", + deviceType, network?.toLowerCase(), ); + const unlockedDevice = devices.find((device) => !device.needs_pin_sent); + if (!unlockedDevice) { + throw new Error("Device is still locked"); + } await hwiService.setHWIClient( - devices[0].fingerprint, - "trezor", - network!.toLowerCase(), + unlockedDevice.fingerprint, + deviceType, + network.toLowerCase(), ); onSuccess(); } catch { @@ -65,16 +75,20 @@ const TrezorPinModal = ({ return () => clearTimeout(timer); } + const isOneKey = deviceType === "onekey"; + const icon = isOneKey ? OneKeyIcon : TrezorIcon; + const deviceName = isOneKey ? "OneKey" : "Trezor"; + const modalContent = { image: ( Trezor ), title: ( -

Enter the pin

+

Enter the PIN

), content: ( <> @@ -86,7 +100,7 @@ const TrezorPinModal = ({

- Follow the keypad layout on your Trezor + Follow the keypad layout on your {deviceName}

@@ -115,7 +129,7 @@ const TrezorPinModal = ({ className={styles.loadingSpinner} /> ) : ( - "Enter Pin" + "Enter PIN" )} ), diff --git a/src/modals/index.ts b/src/modals/index.ts index 7ef0f5d..c20f014 100644 --- a/src/modals/index.ts +++ b/src/modals/index.ts @@ -4,4 +4,5 @@ export { default as DeviceNotFoundModal } from "./DeviceNotFoundModal/DeviceNotF export { default as MultipleDevicesModal } from "./MultipleDevicesModal/MultipleDevicesModal"; export { default as ErrorModal } from "./ErrorModal/ErrorModal"; export { default as TrezorPinModal } from "./TrezorPinModal/TrezorPinModal"; +export { default as OneKeyPinModal } from "./OneKeyPinModal/OneKeyPinModal"; export { default as SubscriptionsModal } from "./SubscriptionsModal/SubscriptionsModal"; diff --git a/src/screens/ConnectScreen/ConnectScreen.tsx b/src/screens/ConnectScreen/ConnectScreen.tsx index 38d4ae5..d6bddda 100644 --- a/src/screens/ConnectScreen/ConnectScreen.tsx +++ b/src/screens/ConnectScreen/ConnectScreen.tsx @@ -10,6 +10,7 @@ import { listen } from "@tauri-apps/api/event"; import QRCode from "qrcode.react"; import useModalState from "../../hooks/useModalState"; import { + getPinInteractionType, HWI_ACTION, HWIDevice, HWIDeviceType, @@ -57,6 +58,7 @@ const ConnectScreen = () => { const [expectedAddress, setExpectedAddress] = useState(null); const [errorMessage, setErrorMessage] = useState(""); const [pairingCode, setPairingCode] = useState(null); + const [currentDevice, setCurrentDevice] = useState(null); // Subscriptions state variables const [isSubscriptionsModalOpen, setSubscriptionsModalOpen] = useState(false); @@ -70,14 +72,22 @@ const ConnectScreen = () => { openModalHandler("notFound"); } else { if (deviceType && network) { + const device = devices[0]; + setCurrentDevice(device); await hwiService.setHWIClient( - devices[0].fingerprint, + device.fingerprint, deviceType, network.toLowerCase(), ); - if (devices[0].needs_pin_sent) { + if (device.needs_pin_sent) { await hwiService.promptPin(); - openModalHandler("pin"); + const pinInteractionType = getPinInteractionType( + deviceType, + device.model, + ); + openModalHandler( + pinInteractionType === "device" ? "onekeyPin" : "pin", + ); } else { openModalHandler("deviceActionSuccess"); } @@ -155,6 +165,10 @@ const ConnectScreen = () => { } break; case "REGISTER_MULTISIG": + if (data.signerType?.toLowerCase() === "onekey") { + handleError("Register multisig is not supported on OneKey"); + return; + } setActionType("registerMultisig"); if (data.descriptorString) { setDescriptor(data.descriptorString.replace(/\*\*/g, "0/0")); @@ -329,6 +343,7 @@ const ConnectScreen = () => { setCurrentAction={setCurrentAction} openModalHandler={openModalHandler} pairingCode={pairingCode} + currentDevice={currentDevice} /> )}
Version {version}
diff --git a/src/services/hwiService.ts b/src/services/hwiService.ts index c094b92..be1002c 100644 --- a/src/services/hwiService.ts +++ b/src/services/hwiService.ts @@ -15,6 +15,12 @@ const emptyTrezorDevice: HWIDevice = { fingerprint: null, }; +let currentDeviceType: string | null = null; + +const emitToChannel = async (eventData: unknown) => { + await invoke("emit_to_channel", { eventData }); +}; + const hwiService = { fetchDevices: async ( deviceType: HWIDeviceType | null = null, @@ -50,6 +56,8 @@ const hwiService = { deviceType: string, network: string, ): Promise => { + currentDeviceType = deviceType; + if (network === "mainnet") { network = "bitcoin"; } @@ -58,12 +66,12 @@ const hwiService = { shareXpubs: async (account: number): Promise => { const eventData = await invoke("hwi_get_xpubs", { account }); - await invoke("emit_to_channel", { eventData }); + await emitToChannel(eventData); }, performHealthCheck: async (account: number): Promise => { const eventData = await invoke("hwi_healthcheck", { account }); - await invoke("emit_to_channel", { eventData }); + await emitToChannel(eventData); }, signTx: async ( @@ -78,7 +86,7 @@ const hwiService = { walletName, hmac, }); - await invoke("emit_to_channel", { eventData }); + await emitToChannel(eventData); }, registerMultisig: async ( @@ -87,13 +95,18 @@ const hwiService = { walletName: string | null, expectedAddress: string, ): Promise => { + if (currentDeviceType === "onekey") { + throw new Error("Operation not supported on OneKey"); + } + const eventData = await invoke("hwi_register_multisig", { descriptor, policy, walletName, expectedAddress, }); - await invoke("emit_to_channel", { eventData }); + + await emitToChannel(eventData); }, verifyAddress: async ( @@ -112,7 +125,7 @@ const hwiService = { hmac, expectedAddress, }); - await invoke("emit_to_channel", { eventData }); + await emitToChannel(eventData); }, promptPin: async (): Promise => { diff --git a/vite.config.ts b/vite.config.ts index 0c9adff..2ae2470 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,6 +4,9 @@ import react from "@vitejs/plugin-react"; // https://vitejs.dev/config/ export default defineConfig(async () => ({ plugins: [react()], + define: { + global: "globalThis", + }, // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` //