diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f0ac2de0d4..829e706eba 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -24,6 +24,12 @@ + + + + + + 2.2.3) - MMKVCore (2.2.3) + - MultiplatformBleAdapter (0.2.0) - nanopb (2.30908.0): - nanopb/decode (= 2.30908.0) - nanopb/encode (= 2.30908.0) @@ -1301,6 +1323,28 @@ PODS: - React-Core - react-native-biometrics (2.2.0): - React-Core + - react-native-ble-plx (3.5.0): + - DoubleConversion + - glog + - hermes-engine + - MultiplatformBleAdapter (= 0.2.0) + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - react-native-blob-util (0.18.3): - React-Core - react-native-change-icon (5.0.0): @@ -1922,6 +1966,7 @@ PODS: - ZXingObjC/All (3.6.9) DEPENDENCIES: + - "BleUtils (from `../node_modules/@onekeyfe/react-native-ble-utils`)" - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - BVLinearGradient (from `../node_modules/react-native-linear-gradient`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) @@ -1967,6 +2012,7 @@ DEPENDENCIES: - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) - react-native-background-timer (from `../node_modules/react-native-background-timer`) - react-native-biometrics (from `../node_modules/react-native-biometrics`) + - react-native-ble-plx (from `../node_modules/react-native-ble-plx`) - react-native-blob-util (from `../node_modules/react-native-blob-util`) - react-native-change-icon (from `../node_modules/react-native-change-icon`) - react-native-config (from `../node_modules/react-native-config`) @@ -2046,6 +2092,7 @@ SPEC REPOS: - libwebp - MMKV - MMKVCore + - MultiplatformBleAdapter - nanopb - PDFGenerator - PromisesObjC @@ -2057,6 +2104,8 @@ SPEC REPOS: - ZXingObjC EXTERNAL SOURCES: + BleUtils: + :path: "../node_modules/@onekeyfe/react-native-ble-utils" boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" BVLinearGradient: @@ -2140,6 +2189,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-background-timer" react-native-biometrics: :path: "../node_modules/react-native-biometrics" + react-native-ble-plx: + :path: "../node_modules/react-native-ble-plx" react-native-blob-util: :path: "../node_modules/react-native-blob-util" react-native-change-icon: @@ -2277,6 +2328,7 @@ CHECKOUT OPTIONS: :git: https://github.com/bithyve/libportal-ios.git SPEC CHECKSUMS: + BleUtils: 7837f24770460d054aa9acf20c7c147a75f17057 boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 BVLinearGradient: cb006ba232a1f3e4f341bb62c42d1098c284da70 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 @@ -2298,6 +2350,7 @@ SPEC CHECKSUMS: libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 MMKV: 941e8774da0e6fdf12c6b3fcc833ca687ae5a42d MMKVCore: 6d5cc1bacce539f4c974985dfe646fb65a5d27d2 + MultiplatformBleAdapter: b1fddd0d499b96b607e00f0faa8e60648343dc1d nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 PDFGenerator: 17d6bc525db0a3fcd156fbf00f9d1b8d5cc75d1e PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 @@ -2333,6 +2386,7 @@ SPEC CHECKSUMS: React-microtasksnativemodule: 3b784cf40f7c3c9500b9a5f9e4eec5c9bbfbef8f react-native-background-timer: 4638ae3bee00320753647900b21260b10587b6f7 react-native-biometrics: ecca256a9e1c8b430f78c4e3dcb16b57ea1418e0 + react-native-ble-plx: 39e6b732ac5bce0a7a40c1fc33ae58a92b444656 react-native-blob-util: 4be49d669870f01645b63d33342718e95d0d48b1 react-native-change-icon: cf7be79da0c88778d626d50d3a78912b10143521 react-native-config: 8e425892a531627c52db765be3088185cb871e19 diff --git a/ios/hexa_keeper/Info.plist b/ios/hexa_keeper/Info.plist index 86c03c42f7..350e74b896 100644 --- a/ios/hexa_keeper/Info.plist +++ b/ios/hexa_keeper/Info.plist @@ -53,6 +53,10 @@ NSAllowsLocalNetworking + NSBluetoothAlwaysUsageDescription + Allow Keeper to scan and connect to your OneKey hardware wallet over Bluetooth + NSBluetoothPeripheralUsageDescription + Allow Keeper to scan and connect to your OneKey hardware wallet over Bluetooth NSCameraUsageDescription Please allow camera access for QR scanner NSContactsUsageDescription diff --git a/ios/hexa_keeper_dev-Info.plist b/ios/hexa_keeper_dev-Info.plist index 3413ca61a8..d5057c47fc 100644 --- a/ios/hexa_keeper_dev-Info.plist +++ b/ios/hexa_keeper_dev-Info.plist @@ -46,6 +46,10 @@ NFCReaderUsageDescription Allow $(PRODUCT_NAME) to interact with nearby NFC devices + NSBluetoothAlwaysUsageDescription + Allow $(PRODUCT_NAME) to scan and connect to your OneKey hardware wallet over Bluetooth + NSBluetoothPeripheralUsageDescription + Allow $(PRODUCT_NAME) to scan and connect to your OneKey hardware wallet over Bluetooth NSAppTransportSecurity diff --git a/metro.config.js b/metro.config.js index 1748c03ea2..1e11c8b303 100644 --- a/metro.config.js +++ b/metro.config.js @@ -16,6 +16,8 @@ const config = { resolver: { assetExts: assetExts.filter((ext) => ext !== 'svg'), sourceExts: [...sourceExts, 'svg'], + // OneKey hd-core uses package.json "exports" field + unstable_enablePackageExports: true, }, }; diff --git a/package.json b/package.json index 35db7552d3..a4a7fbd7f6 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,11 @@ "dependencies": { "@bitcoinerlab/miniscript": "1.4.0", "@ngraveio/bc-ur": "1.1.6", + "@noble/hashes": "^1.3.3", "@noble/secp256k1": "1.6.3", + "@onekeyfe/hd-ble-sdk": "1.1.16", + "@onekeyfe/hd-core": "1.1.16", + "@onekeyfe/react-native-ble-utils": "^0.1.4", "@react-native-clipboard/clipboard": "1.16.2", "@react-native-community/netinfo": "11.4.1", "@react-native-firebase/app": "14.11.1", @@ -74,6 +78,7 @@ "react-localization": "1.0.19", "react-native": "0.77.3", "react-native-background-timer": "2.4.1", + "react-native-ble-plx": "3.5.0", "react-native-biometrics": "2.2.0", "react-native-blob-util": "0.18.3", "react-native-change-icon": "5.0.0", @@ -118,6 +123,7 @@ "redux-persist": "6.0.0", "redux-saga": "1.1.3", "rn-qr-generator": "^1.4.4", + "ripple-keypairs": "^1.3.1", "satochip-react-native": "git+https://github.com/Toporin/satochip-react-native.git#1076e7c097571d561b5b903f31c6337455def19e", "semver": "7.3.8", "socket.io-client": "4.5.4", diff --git a/src/assets/images/flag-hongkong.svg b/src/assets/images/flag-hongkong.svg new file mode 100644 index 0000000000..e1a7ee6868 --- /dev/null +++ b/src/assets/images/flag-hongkong.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/flag-japan.svg b/src/assets/images/flag-japan.svg new file mode 100644 index 0000000000..3c980d2058 --- /dev/null +++ b/src/assets/images/flag-japan.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/assets/images/onekey-devices/classic-pure.png b/src/assets/images/onekey-devices/classic-pure.png new file mode 100644 index 0000000000..de31d59dc5 Binary files /dev/null and b/src/assets/images/onekey-devices/classic-pure.png differ diff --git a/src/assets/images/onekey-devices/classic.png b/src/assets/images/onekey-devices/classic.png new file mode 100644 index 0000000000..aaf787a4f0 Binary files /dev/null and b/src/assets/images/onekey-devices/classic.png differ diff --git a/src/assets/images/onekey-devices/pro-black.png b/src/assets/images/onekey-devices/pro-black.png new file mode 100644 index 0000000000..3057f8bc51 Binary files /dev/null and b/src/assets/images/onekey-devices/pro-black.png differ diff --git a/src/assets/images/onekey-devices/touch.png b/src/assets/images/onekey-devices/touch.png new file mode 100644 index 0000000000..fb85963c15 Binary files /dev/null and b/src/assets/images/onekey-devices/touch.png differ diff --git a/src/assets/images/onekey-green-dark.svg b/src/assets/images/onekey-green-dark.svg new file mode 100644 index 0000000000..7eb3c742a1 --- /dev/null +++ b/src/assets/images/onekey-green-dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/onekey-green-light.svg b/src/assets/images/onekey-green-light.svg new file mode 100644 index 0000000000..a9166d0064 --- /dev/null +++ b/src/assets/images/onekey-green-light.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/onekey-shop-device.png b/src/assets/images/onekey-shop-device.png new file mode 100644 index 0000000000..e57d06cfab Binary files /dev/null and b/src/assets/images/onekey-shop-device.png differ diff --git a/src/assets/images/onekey_icon.svg b/src/assets/images/onekey_icon.svg new file mode 100644 index 0000000000..afa35e0aac --- /dev/null +++ b/src/assets/images/onekey_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/onekey_icon_light.svg b/src/assets/images/onekey_icon_light.svg new file mode 100644 index 0000000000..afa35e0aac --- /dev/null +++ b/src/assets/images/onekey_icon_light.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/onekey_illustration.svg b/src/assets/images/onekey_illustration.svg new file mode 100644 index 0000000000..1b36bb43f3 --- /dev/null +++ b/src/assets/images/onekey_illustration.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/assets/images/onekey_logo.svg b/src/assets/images/onekey_logo.svg new file mode 100644 index 0000000000..51b2596ff4 --- /dev/null +++ b/src/assets/images/onekey_logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/images/onekey_logo_white.svg b/src/assets/images/onekey_logo_white.svg new file mode 100644 index 0000000000..4d3d1cd7db --- /dev/null +++ b/src/assets/images/onekey_logo_white.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/components/OneKeyBleModal.tsx b/src/components/OneKeyBleModal.tsx new file mode 100644 index 0000000000..6b4d516c09 --- /dev/null +++ b/src/components/OneKeyBleModal.tsx @@ -0,0 +1,545 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { ActivityIndicator, FlatList, Image, StyleSheet, TouchableOpacity } from 'react-native'; +import { Box, useColorMode } from 'native-base'; +import { useDispatch } from 'react-redux'; +import KeeperModal from 'src/components/KeeperModal'; +import Text from 'src/components/KeeperText'; +import useToastMessage from 'src/hooks/useToastMessage'; +import ToastErrorIcon from 'src/assets/images/toast_error.svg'; +import TickIcon from 'src/assets/images/icon_tick.svg'; +import { useAppSelector } from 'src/store/hooks'; +import { NetworkType, SignerType } from 'src/services/wallets/enums'; +import { UI_REQUEST } from '@onekeyfe/hd-core'; +import { + ensureOneKeyBLEReady, + fetchOneKeySignerData, + getOneKeyDeviceInfo, + onekeyUIEmitter, + ONEKEY_UI_EVENT, + searchOneKeyDevices, + verifyAddressOnOneKey, + type OneKeyUIEvent, +} from 'src/services/onekeyBle'; +import { + getDeviceImage, + getDeviceDisplayName, + getDeviceTypeName, +} from 'src/services/onekeyBle/deviceConstants'; +import { setupUSBSigner } from 'src/hardware/signerSetup'; +import { addSigningDevice, updateKeyDetails } from 'src/store/sagaActions/vaults'; +import { healthCheckStatusUpdate } from 'src/store/sagaActions/bhr'; +import { hcStatusType } from 'src/models/interfaces/HeathCheckTypes'; +import type { VaultSigner } from 'src/services/wallets/interfaces/vault'; +import { captureError } from 'src/services/sentry'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import type { Signer } from 'src/services/wallets/interfaces/vault'; +import type { SearchDevice } from '@onekeyfe/hd-core'; + +// ─── SDK UI event descriptions ────────────────────────────────────────────── + +const UI_PROMPTS: Record = { + [UI_REQUEST.REQUEST_PIN]: 'Please enter PIN on your OneKey device', + [UI_REQUEST.REQUEST_BUTTON]: 'Please confirm on your OneKey device', + [UI_REQUEST.REQUEST_PASSPHRASE]: 'Please enter passphrase on your OneKey device', + idle: '', +}; + +// ─── Types ────────────────────────────────────────────────────────────────── + +type ModalMode = 'setup' | 'health-check' | 'verify-address'; +type ModalPhase = 'scan' | 'connecting' | 'sdk-prompt' | 'done'; + +const BLE_OPERATION_TIMEOUT_MS = 30_000; + +type Props = { + visible: boolean; + close: () => void; + mode: ModalMode; + signer?: Signer; + isMultisig?: boolean; + addSignerFlow?: boolean; + accountNumber?: number; + onSignerAdded?: (signer: Signer) => void; + // verify-address mode props + vaultKey?: VaultSigner; + vaultId?: string; + receiveAddressIndex?: number; + receivingAddress?: string; +}; + +// ─── Component ────────────────────────────────────────────────────────────── + +function OneKeyBleModal({ + visible, + close, + mode, + signer, + isMultisig = true, + addSignerFlow = false, + accountNumber = 0, + onSignerAdded, + vaultKey, + vaultId, + receiveAddressIndex, + receivingAddress, +}: Props) { + const { colorMode } = useColorMode(); + const dispatch = useDispatch(); + const { showToast } = useToastMessage(); + const { translations } = useContext(LocalizationContext); + const { common } = translations; + + const { bitcoinNetworkType } = useAppSelector((state) => state.settings); + const networkType = + bitcoinNetworkType === NetworkType.TESTNET ? NetworkType.TESTNET : NetworkType.MAINNET; + + const [phase, setPhase] = useState('scan'); + const [devices, setDevices] = useState([]); + const [scanning, setScanning] = useState(false); + const [statusMessage, setStatusMessage] = useState(''); + const [sdkPrompt, setSdkPrompt] = useState('idle'); + + // Listen to SDK UI events + useEffect(() => { + const handler = (event: OneKeyUIEvent) => { + if (event === 'idle') { + setSdkPrompt('idle'); + if (phase === 'sdk-prompt') setPhase('connecting'); + } else { + setSdkPrompt(event); + setPhase('sdk-prompt'); + } + }; + const subscription = onekeyUIEmitter.addListener(ONEKEY_UI_EVENT, handler); + return () => { subscription.remove(); }; + }, [phase]); + + // Reset state when modal opens + useEffect(() => { + if (visible) { + setDevices([]); + setScanning(false); + setStatusMessage(''); + setSdkPrompt('idle'); + + if (mode === 'health-check' || mode === 'verify-address') { + // Direct connect modes: skip scan + setPhase('connecting'); + setTimeout(() => mode === 'verify-address' ? runVerifyAddress() : runHealthCheck(), 300); + } else { + // Setup: show scan UI + setPhase('scan'); + setTimeout(() => scanDevices(), 300); + } + } + }, [visible]); + + // ─── Scan ────────────────────────────────────────────────────────────────── + + const scanDevices = async () => { + if (scanning) return; + try { + setScanning(true); + setDevices([]); + const bleReady = await ensureOneKeyBLEReady(); + if (!bleReady.ready) { + showToast( + bleReady.reason === 'MISSING_PERMISSION' + ? 'Please grant Bluetooth permissions' + : 'Please turn on Bluetooth and try again', + + ); + return; + } + const found = await searchOneKeyDevices(); + setDevices(found || []); + } catch (error) { + captureError(error); + showToast(error?.message || common.somethingWrong, ); + } finally { + setScanning(false); + } + }; + + // ─── Setup: tap device → connect → import keys ───────────────────────────── + + const handleSetupTap = async (device: SearchDevice) => { + if (!device?.connectId) return; + try { + setPhase('connecting'); + setStatusMessage('Connecting to device...'); + + const deviceInfo = await getOneKeyDeviceInfo(device.connectId); + + // Clear any SDK prompt after device info is fetched + setPhase('connecting'); + setStatusMessage('Importing keys...'); + const signerData = await fetchOneKeySignerData({ + connectId: device.connectId, + deviceId: deviceInfo.deviceId, + networkType, + accountNumber, + }); + + // Clear any SDK prompt after keys imported + setPhase('connecting'); + setStatusMessage('Finalizing...'); + + const { signer: newSigner } = setupUSBSigner(SignerType.ONEKEY, signerData, isMultisig); + + // Title: "OneKey Pro" / "OneKey Classic", Subtitle: BLE name (e.g. "Pro 04DD") + newSigner.signerName = getDeviceTypeName(device); + const bleName = device?.name; + if (bleName && bleName !== 'Unknown') { + newSigner.signerDescription = bleName; + } + newSigner.extraData = { ...newSigner.extraData, bleConnectId: deviceInfo.connectId }; + + dispatch(addSigningDevice([newSigner])); + setPhase('done'); + showToast('OneKey added successfully', ); + onSignerAdded?.(newSigner); + close(); + } catch (error) { + captureError(error); + showToast(error?.message || common.somethingWrong, ); + setPhase('scan'); // Back to scan so user can retry + } + }; + + // ─── Health Check: direct connect via stored connectId ───────────────────── + + const runHealthCheck = async () => { + if (!signer) return; + try { + setPhase('connecting'); + + const bleReady = await ensureOneKeyBLEReady(); + if (!bleReady.ready) { + showToast('Please turn on Bluetooth and try again', ); + close(); + return; + } + + const storedConnectId = signer?.extraData?.bleConnectId; + if (!storedConnectId) { + showToast('No stored connection info. Please re-add this device.', ); + close(); + return; + } + + // BLE needs a brief scan to discover peripherals before connecting + setStatusMessage('Connecting to device...'); + await searchOneKeyDevices(); + + setStatusMessage('Verifying device...'); + const deviceInfo = await Promise.race([ + getOneKeyDeviceInfo(storedConnectId), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Operation timed out. Device not found.')), BLE_OPERATION_TIMEOUT_MS) + ), + ]); + + // Clear any SDK prompt after verification + setPhase('connecting'); + setStatusMessage('Checking result...'); + + if (signer.masterFingerprint === deviceInfo.masterFingerprint) { + dispatch( + healthCheckStatusUpdate([ + { signerId: signer.masterFingerprint, status: hcStatusType.HEALTH_CHECK_SUCCESSFULL }, + ]) + ); + setPhase('done'); + showToast('OneKey verification successful', ); + close(); + } else { + showToast('Fingerprint mismatch. Wrong device connected.', ); + close(); + } + } catch (error) { + captureError(error); + showToast(error?.message || common.somethingWrong, ); + close(); + } + }; + + // ─── Verify Address: direct connect → show address on device ───────────── + + const runVerifyAddress = async () => { + if (!signer || !vaultKey || !receivingAddress) return; + try { + setPhase('connecting'); + + const bleReady = await ensureOneKeyBLEReady(); + if (!bleReady.ready) { + showToast('Please turn on Bluetooth and try again', ); + close(); + return; + } + + const storedConnectId = signer?.extraData?.bleConnectId; + if (!storedConnectId) { + showToast('No stored connection info. Please re-add this device.', ); + close(); + return; + } + + setStatusMessage('Connecting to device...'); + await searchOneKeyDevices(); + + const withTimeout = (promise: Promise, ms = BLE_OPERATION_TIMEOUT_MS): Promise => + Promise.race([ + promise, + new Promise((_, reject) => + setTimeout(() => reject(new Error('Operation timed out. Device not found.')), ms) + ), + ]); + + setStatusMessage('Reading device info...'); + const deviceInfo = await withTimeout(getOneKeyDeviceInfo(storedConnectId)); + + setPhase('connecting'); + setStatusMessage('Verifying address on device...'); + const addressPath = `${vaultKey.derivationPath}/0/${receiveAddressIndex}`; + const deviceAddress = await withTimeout(verifyAddressOnOneKey({ + connectId: storedConnectId, + deviceId: deviceInfo.deviceId, + path: addressPath, + networkType, + })); + + setPhase('connecting'); + + if (deviceAddress === receivingAddress) { + dispatch(updateKeyDetails(vaultKey, 'registered', { registered: true, vaultId })); + dispatch( + healthCheckStatusUpdate([ + { signerId: signer.masterFingerprint, status: hcStatusType.HEALTH_CHECK_VERIFICATION }, + ]) + ); + showToast('Address verified successfully on OneKey', ); + } else { + showToast('Address mismatch! The address on device does not match.', ); + } + close(); + } catch (error) { + captureError(error); + showToast(error?.message || common.somethingWrong, ); + close(); + } + }; + + const handleDeviceTap = handleSetupTap; + + // ─── Render helpers ──────────────────────────────────────────────────────── + + const renderDevice = ({ item: device }: { item: SearchDevice }) => { + const img = getDeviceImage(device?.deviceType); + return ( + handleDeviceTap(device)} + activeOpacity={0.7} + > + + {img ? ( + + ) : ( + + OK + + )} + + + {getDeviceDisplayName(device)} + + + + + + ); + }; + + const ModalContent = () => { + // SDK prompt phase — show device interaction prompt + if (phase === 'sdk-prompt' && sdkPrompt !== 'idle') { + return ( + + + + {UI_PROMPTS[sdkPrompt]} + + + ); + } + + // Connecting phase + if (phase === 'connecting') { + return ( + + + + {statusMessage || 'Connecting...'} + + + ); + } + + // Scan phase — show device list or loading + if (scanning && devices.length === 0) { + return ( + + + + Looking for devices... + + + ); + } + + if (!scanning && devices.length === 0) { + return ( + + + No devices found. Make sure your OneKey is unlocked and nearby. + + + + Rescan + + + + ); + } + + // Devices found + return ( + + + + Select your device + + + + Rescan + + + + `${d?.uuid || ''}-${d?.connectId || ''}`} + contentContainerStyle={styles.listContent} + showsVerticalScrollIndicator={false} + style={styles.list} + /> + + ); + }; + + if (!visible) return null; + + const title = + mode === 'setup' ? 'Setting up OneKey' + : mode === 'verify-address' ? 'Verify Address' + : 'Verify OneKey'; + const subTitle = + mode === 'setup' ? 'Connect OneKey hardware wallet via Bluetooth' + : mode === 'verify-address' ? 'Confirm the address matches on your OneKey device' + : 'Verify your OneKey device is accessible'; + + return ( + + ); +} + +const styles = StyleSheet.create({ + centerContent: { + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 40, + gap: 16, + }, + promptText: { + fontSize: 16, + fontWeight: '600', + textAlign: 'center', + paddingHorizontal: 20, + }, + statusText: { + fontSize: 14, + textAlign: 'center', + paddingHorizontal: 20, + }, + rescanText: { + fontSize: 14, + fontWeight: '600', + }, + listHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 12, + }, + listHeaderText: { + fontSize: 13, + }, + list: { + maxHeight: 300, + }, + listContent: { + gap: 8, + }, + deviceItem: { + borderWidth: 1, + borderRadius: 10, + paddingHorizontal: 16, + paddingVertical: 12, + }, + deviceRow: { + flexDirection: 'row', + alignItems: 'center', + }, + deviceImage: { + width: 40, + height: 40, + borderRadius: 8, + marginRight: 12, + }, + fallbackIcon: { + width: 40, + height: 40, + borderRadius: 8, + backgroundColor: '#44D62C', + alignItems: 'center', + justifyContent: 'center', + marginRight: 12, + }, + fallbackText: { + fontSize: 14, + fontWeight: '700', + color: '#000', + }, + deviceName: { + fontSize: 15, + fontWeight: '500', + }, + arrow: { + fontSize: 22, + fontWeight: '300', + marginLeft: 8, + }, +}); + +export default OneKeyBleModal; diff --git a/src/components/ThemedSvg.tsx/ThemedIcons.js b/src/components/ThemedSvg.tsx/ThemedIcons.js index c5ac8bedeb..69a9c37b3c 100644 --- a/src/components/ThemedSvg.tsx/ThemedIcons.js +++ b/src/components/ThemedSvg.tsx/ThemedIcons.js @@ -150,6 +150,7 @@ import SatochipSetupSVG from 'src/assets/images/SatochipSetup.svg'; import PrivateSatochipIllustration from 'src/assets/privateImages/satochip-illustration.svg'; import PortalIllustration from 'src/assets/images/portal_illustration.svg'; import PrivatePortalIllustration from 'src/assets/privateImages/portal-illustration.svg'; +import OneKeyIllustration from 'src/assets/images/onekey_illustration.svg'; import WalletRecoveryIcon from 'src/assets/images/walletRecoveryIcon.svg'; import PrivateWalletRecovery from 'src/assets/privateImages/wallet-recovery-illlustration.svg'; import OrganizationIcon from 'src/assets/images/organizationIcon.svg'; @@ -767,6 +768,12 @@ const themeIcons = { PRIVATE: PrivatePortalIllustration, PRIVATE_LIGHT: PrivatePortalIllustration, }, + onekey_illustration: { + DARK: OneKeyIllustration, + LIGHT: OneKeyIllustration, + PRIVATE: OneKeyIllustration, + PRIVATE_LIGHT: OneKeyIllustration, + }, wallet_Recovery_icon: { DARK: WalletRecoveryIcon, diff --git a/src/constants/HardwareReferralLinks.ts b/src/constants/HardwareReferralLinks.ts index d402333bee..32a2576b1e 100644 --- a/src/constants/HardwareReferralLinks.ts +++ b/src/constants/HardwareReferralLinks.ts @@ -39,6 +39,11 @@ export const sellers = [ link: 'https://affil.trezor.io/aff_c?offer_id=134&aff_id=35017', id: '67befce7bb95d55d985d844a', }, + { + identifier: 'onekey', + link: 'https://onekey.so/zh_CN/products/onekey-pro/', + id: 'onekey_pro', + }, ]; export const resellers = [ { diff --git a/src/hardware/index.ts b/src/hardware/index.ts index d1a5e65662..31cad2a02a 100644 --- a/src/hardware/index.ts +++ b/src/hardware/index.ts @@ -183,6 +183,9 @@ export const getSignerNameFromType = (type: SignerType, isMock = false, isAmf = case SignerType.LEDGER: name = 'Ledger'; break; + case SignerType.ONEKEY: + name = 'OneKey'; + break; case SignerType.MOBILE_KEY: name = 'Recovery Key'; break; @@ -476,6 +479,9 @@ export const getSDMessage = ({ type }: { type: SignerType }) => { case SignerType.TREZOR: { return 'Trusted signers from SatoshiLabs'; } + case SignerType.ONEKEY: { + return 'OneKey hardware wallet over Bluetooth'; + } case SignerType.OTHER_SD: { return 'Varies with different signer'; } diff --git a/src/navigation/Navigator.tsx b/src/navigation/Navigator.tsx index e88bcaa427..1888935421 100644 --- a/src/navigation/Navigator.tsx +++ b/src/navigation/Navigator.tsx @@ -47,8 +47,10 @@ import WalletSettings from 'src/screens/WalletDetails/WalletSettings'; import Colors from 'src/theme/Colors'; import NodeSettings from 'src/screens/AppSettings/Node/NodeSettings'; import ConnectChannel from 'src/screens/Channel/ConnectChannel'; +import SignMessageOneKeyBle from 'src/screens/OneKey/SignMessageOneKeyBle'; import RegisterWithChannel from 'src/screens/QRScreens/RegisterWithChannel'; import SignWithChannel from 'src/screens/QRScreens/SignWithChannel'; +import SignWithOneKeyBle from 'src/screens/SignTransaction/SignWithOneKeyBle'; import UTXOLabeling from 'src/screens/UTXOManagement/UTXOLabeling'; import UTXOManagement from 'src/screens/UTXOManagement/UTXOManagement'; import ImportWalletDetailsScreen from 'src/screens/ImportWalletDetailsScreen/ImportWalletDetailsScreen'; @@ -207,6 +209,7 @@ function LoginStack() { {/* Channel Based SDs */} + {/* Mobile Key, Seed Key */} @@ -318,9 +321,11 @@ function AppStack() { + + diff --git a/src/navigation/types.ts b/src/navigation/types.ts index 021dd59a55..f14743a9bf 100644 --- a/src/navigation/types.ts +++ b/src/navigation/types.ts @@ -95,9 +95,11 @@ export type AppStackParams = { ScanNode: undefined; PrivacyAndDisplay: undefined; ConnectChannel: undefined; + SignMessageOneKeyBle: undefined; RegisterWithChannel: undefined; SetupOtherSDScreen: undefined; SignWithChannel: undefined; + SignWithOneKeyBle: undefined; CosignerDetails: { signer: Signer }; AdditionalDetails: { signer: Signer }; KeyHistory: undefined; diff --git a/src/screens/Hardware/components/DeviceCard.tsx b/src/screens/Hardware/components/DeviceCard.tsx index 4017c6c0d4..2ca15edc92 100644 --- a/src/screens/Hardware/components/DeviceCard.tsx +++ b/src/screens/Hardware/components/DeviceCard.tsx @@ -146,7 +146,9 @@ const styles = StyleSheet.create({ flagContainer: { flexDirection: 'row', alignItems: 'center', - gap: wp(8), + gap: wp(4), + flexWrap: 'wrap', + flex: 1, }, subText: { marginTop: hp(10), diff --git a/src/screens/Hardware/components/HardwareDevices.tsx b/src/screens/Hardware/components/HardwareDevices.tsx index f5eb453382..e18d46cc00 100644 --- a/src/screens/Hardware/components/HardwareDevices.tsx +++ b/src/screens/Hardware/components/HardwareDevices.tsx @@ -8,6 +8,13 @@ import ColdCard from 'src/assets/images/coinkite-image.svg'; import Passport from 'src/assets/images/foundation-passport-icon.svg'; import Legder from 'src/assets/images/Ledger-icon.svg'; import Trezor from 'src/assets/images/TREZOR-icon.svg'; +import { Image, ImageStyle } from 'react-native'; + +const OnekeyDevice = ({ width, height }: { width: number; height: number }) => ( + +); +import FlagHongKong from 'src/assets/images/flag-hongkong.svg'; +import FlagJapan from 'src/assets/images/flag-japan.svg'; import FlagCanada from 'src/assets/images/flag-canada.svg'; import FlagUSA from 'src/assets/images/flag-usa.svg'; import FlagSwizerland from 'src/assets/images/flag-swizerland.svg'; @@ -83,6 +90,16 @@ const HardwareDevices = ({ sellers }) => { subscribeText: '', unSubscribeText: '', }, + { + id: 6, + title: 'OneKey', + image: , + flagIcon: <>, + country: 'HK & Japan', + link: getSellerLink('onekey'), + subscribeText: '', + unSubscribeText: '', + }, ]; return ( diff --git a/src/screens/Home/components/Keys/SignerContent.tsx b/src/screens/Home/components/Keys/SignerContent.tsx index 12f4737228..41db531b4e 100644 --- a/src/screens/Home/components/Keys/SignerContent.tsx +++ b/src/screens/Home/components/Keys/SignerContent.tsx @@ -44,6 +44,7 @@ const SignerContent = ({ navigation, handleModalClose }) => { background: 'headerWhite', isTrue: false, }, + { type: SignerType.ONEKEY, background: 'pantoneGreen', isTrue: true }, ]; const hardwareSnippet = hardwareSigners.map(({ type, background, isTrue }) => ({ diff --git a/src/screens/OneKey/SignMessageOneKeyBle.tsx b/src/screens/OneKey/SignMessageOneKeyBle.tsx new file mode 100644 index 0000000000..37b4cece58 --- /dev/null +++ b/src/screens/OneKey/SignMessageOneKeyBle.tsx @@ -0,0 +1,158 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { ActivityIndicator, StyleSheet } from 'react-native'; +import { Box, useColorMode } from 'native-base'; +import { CommonActions, useNavigation, useRoute } from '@react-navigation/native'; +import ScreenWrapper from 'src/components/ScreenWrapper'; +import WalletHeader from 'src/components/WalletHeader'; +import Text from 'src/components/KeeperText'; +import useToastMessage from 'src/hooks/useToastMessage'; +import ToastErrorIcon from 'src/assets/images/toast_error.svg'; +import TickIcon from 'src/assets/images/icon_tick.svg'; +import { useAppSelector } from 'src/store/hooks'; +import { NetworkType } from 'src/services/wallets/enums'; +import { + ensureOneKeyBLEReady, + getOneKeyDeviceInfo, + searchOneKeyDevices, + signMessageWithOneKey, + onekeyUIEmitter, + ONEKEY_UI_EVENT, + type OneKeyUIEvent, +} from 'src/services/onekeyBle'; +import { UI_REQUEST } from '@onekeyfe/hd-core'; +import { captureError } from 'src/services/sentry'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import type { Signer } from 'src/services/wallets/interfaces/vault'; + +const UI_PROMPTS: Record = { + [UI_REQUEST.REQUEST_PIN]: 'Please enter PIN on your OneKey device', + [UI_REQUEST.REQUEST_BUTTON]: 'Please confirm on your OneKey device', + [UI_REQUEST.REQUEST_PASSPHRASE]: 'Please enter passphrase on your OneKey device', +}; + +const BLE_TIMEOUT_MS = 30_000; + +type Params = { + message: string; + address: string; + derivationPath: string; + signer: Signer; + onSignatureReceived: (signature: string, address: string) => void; +}; + +function SignMessageOneKeyBle() { + const { colorMode } = useColorMode(); + const { params } = useRoute(); + const navigation = useNavigation(); + const { showToast } = useToastMessage(); + const { translations } = useContext(LocalizationContext); + const { common } = translations; + + const { message, address, derivationPath, signer, onSignatureReceived } = params as Params; + + const { bitcoinNetworkType } = useAppSelector((state) => state.settings); + const networkType = + bitcoinNetworkType === NetworkType.TESTNET ? NetworkType.TESTNET : NetworkType.MAINNET; + + const [statusMessage, setStatusMessage] = useState('Preparing...'); + const [sdkPrompt, setSdkPrompt] = useState(''); + + // Listen to SDK UI events + useEffect(() => { + const sub = onekeyUIEmitter.addListener(ONEKEY_UI_EVENT, (event: OneKeyUIEvent) => { + if (UI_PROMPTS[event]) { + setSdkPrompt(UI_PROMPTS[event]); + } + }); + return () => sub.remove(); + }, []); + + // Auto-run on mount + useEffect(() => { + const timer = setTimeout(() => runSignMessage(), 300); + return () => clearTimeout(timer); + }, []); + + const withTimeout = (promise: Promise): Promise => + Promise.race([ + promise, + new Promise((_, reject) => + setTimeout(() => reject(new Error('Operation timed out. Device not found.')), BLE_TIMEOUT_MS) + ), + ]); + + const runSignMessage = async () => { + try { + const bleReady = await ensureOneKeyBLEReady(); + if (!bleReady.ready) { + showToast('Please turn on Bluetooth and try again', ); + navigation.dispatch(CommonActions.goBack()); + return; + } + + const storedConnectId = signer?.extraData?.bleConnectId; + if (!storedConnectId) { + showToast('No stored connection info. Please re-add this device.', ); + navigation.dispatch(CommonActions.goBack()); + return; + } + + // BLE needs a brief scan to discover peripherals + setStatusMessage('Connecting to device...'); + await searchOneKeyDevices(); + + setStatusMessage('Reading device info...'); + setSdkPrompt(''); + const deviceInfo = await withTimeout(getOneKeyDeviceInfo(storedConnectId)); + + setStatusMessage('Signing message...'); + setSdkPrompt(''); + const result = await withTimeout(signMessageWithOneKey({ + connectId: storedConnectId, + deviceId: deviceInfo.deviceId, + path: derivationPath, + message, + networkType, + })); + + setSdkPrompt(''); + showToast('Message signed successfully', ); + onSignatureReceived?.(result.signature, result.address); + navigation.dispatch(CommonActions.goBack()); + } catch (error) { + captureError(error); + showToast(error?.message || common.somethingWrong, ); + navigation.dispatch(CommonActions.goBack()); + } + }; + + const displayText = sdkPrompt || statusMessage; + + return ( + + + + + + {displayText} + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + gap: 16, + paddingHorizontal: 20, + }, + statusText: { + fontSize: 15, + textAlign: 'center', + }, +}); + +export default SignMessageOneKeyBle; diff --git a/src/screens/Recieve/ReceiveScreen.tsx b/src/screens/Recieve/ReceiveScreen.tsx index 55580525ad..4aad421dc8 100644 --- a/src/screens/Recieve/ReceiveScreen.tsx +++ b/src/screens/Recieve/ReceiveScreen.tsx @@ -45,6 +45,7 @@ import useExchangeRates from 'src/hooks/useExchangeRates'; import useCurrencyCode from 'src/store/hooks/state-selectors/useCurrencyCode'; import { SATOSHIS_IN_BTC } from 'src/constants/Bitcoin'; import { InteracationMode } from '../Vault/HardwareModalMap'; +import OneKeyBleModal from 'src/components/OneKeyBleModal'; import { Vault } from 'src/services/wallets/interfaces/vault'; import KeyPadView from 'src/components/AppNumPad/KeyPadView'; import AmountDetailsInput from '../Send/AmountDetailsInput'; @@ -58,6 +59,7 @@ const AddressVerifiableSigners = [ SignerType.COLDCARD, SignerType.JADE, SignerType.PORTAL, + SignerType.ONEKEY, ]; const SignerTypesNeedingRegistration = [ @@ -81,6 +83,7 @@ function ReceiveScreen({ route }: { route }) { // const amount = route?.params?.amount; const [receivingAddress, setReceivingAddress] = useState(null); const [paymentURI, setPaymentURI] = useState(null); + const [onekeyVerifyState, setOnekeyVerifyState] = useState({ visible: false }); const { translations } = useContext(LocalizationContext); const { common, home, wallet: walletTranslation, vault: vaultTranslations } = translations; @@ -281,6 +284,18 @@ function ReceiveScreen({ route }: { route }) { receiveAddressIndex: currentAddressIdx - 1, }) ); + } else if (signer.type === SignerType.ONEKEY) { + const vKey = vaultSigners?.find( + (vs) => vs.signer?.masterFingerprint === signer.masterFingerprint + )?.vaultSigner; + setOnekeyVerifyState({ + visible: true, + signer, + vaultKey: vKey, + vaultId: wallet.id, + receiveAddressIndex: currentAddressIdx - 1, + receivingAddress, + }); } else { navigation.dispatch( CommonActions.navigate('ConnectChannel', { @@ -348,6 +363,7 @@ function ReceiveScreen({ route }: { route }) { }; return ( + <> )} + setOnekeyVerifyState({ visible: false })} + mode="verify-address" + signer={onekeyVerifyState.signer} + vaultKey={onekeyVerifyState.vaultKey} + vaultId={onekeyVerifyState.vaultId} + receiveAddressIndex={onekeyVerifyState.receiveAddressIndex} + receivingAddress={onekeyVerifyState.receivingAddress} + /> + ); } diff --git a/src/screens/SignTransaction/SignTransactionScreen.tsx b/src/screens/SignTransaction/SignTransactionScreen.tsx index 082f7110c0..ab6c9c2ec0 100644 --- a/src/screens/SignTransaction/SignTransactionScreen.tsx +++ b/src/screens/SignTransaction/SignTransactionScreen.tsx @@ -144,6 +144,7 @@ function SignTransactionScreen() { const [trezorModal, setTrezorModal] = useState(false); const [bitbox02Modal, setBitbox02Modal] = useState(false); const [otherSDModal, setOtherSDModal] = useState(false); + const [oneKeyModal, setOneKeyModal] = useState(false); const [otpModal, showOTPModal] = useState(false); const [passwordModal, setPasswordModal] = useState(false); const [confirmPassVisible, setConfirmPassVisible] = useState(false); @@ -673,6 +674,9 @@ function SignTransactionScreen() { case SignerType.KRUX: setKruxModal(true); break; + case SignerType.ONEKEY: + setOneKeyModal(true); + break; default: showToast(`action not set for ${signer.type}`); break; @@ -837,10 +841,12 @@ function SignTransactionScreen() { trezorModal={trezorModal} bitbox02Modal={bitbox02Modal} otherSDModal={otherSDModal} + oneKeyModal={oneKeyModal} specterModal={specterModal} kruxModal={kruxModal} setSpecterModal={setSpecterModal} setOtherSDModal={setOtherSDModal} + setOneKeyModal={setOneKeyModal} setTrezorModal={setTrezorModal} setBitbox02Modal={setBitbox02Modal} setJadeModal={setJadeModal} diff --git a/src/screens/SignTransaction/SignWithOneKeyBle.tsx b/src/screens/SignTransaction/SignWithOneKeyBle.tsx new file mode 100644 index 0000000000..b6d1088105 --- /dev/null +++ b/src/screens/SignTransaction/SignWithOneKeyBle.tsx @@ -0,0 +1,195 @@ +import React, { useContext, useEffect, useMemo, useState } from 'react'; +import { ActivityIndicator, StyleSheet } from 'react-native'; +import { Box, useColorMode } from 'native-base'; +import { CommonActions, useNavigation, useRoute } from '@react-navigation/native'; +import ScreenWrapper from 'src/components/ScreenWrapper'; +import WalletHeader from 'src/components/WalletHeader'; +import Text from 'src/components/KeeperText'; +import { VaultSigner } from 'src/services/wallets/interfaces/vault'; +import { useDispatch } from 'react-redux'; +import { useAppSelector } from 'src/store/hooks'; +import { SerializedPSBTEnvelop } from 'src/services/wallets/interfaces'; +import { updatePSBTEnvelops } from 'src/store/reducers/send_and_receive'; +import { captureError } from 'src/services/sentry'; +import { LocalizationContext } from 'src/context/Localization/LocContext'; +import useToastMessage from 'src/hooks/useToastMessage'; +import ToastErrorIcon from 'src/assets/images/toast_error.svg'; +import { + ensureOneKeyBLEReady, + getOneKeyDeviceInfo, + searchOneKeyDevices, + signPsbtWithOneKey, + onekeyUIEmitter, + ONEKEY_UI_EVENT, + type OneKeyUIEvent, +} from 'src/services/onekeyBle'; +import { UI_REQUEST } from '@onekeyfe/hd-core'; +import useSignerFromKey from 'src/hooks/useSignerFromKey'; +import { healthCheckStatusUpdate } from 'src/store/sagaActions/bhr'; +import { hcStatusType } from 'src/models/interfaces/HeathCheckTypes'; +import { validatePSBT } from 'src/utils/utilities'; +import { NetworkType } from 'src/services/wallets/enums'; + +const UI_PROMPTS: Record = { + [UI_REQUEST.REQUEST_PIN]: 'Please enter PIN on your OneKey device', + [UI_REQUEST.REQUEST_BUTTON]: 'Please confirm on your OneKey device', + [UI_REQUEST.REQUEST_PASSPHRASE]: 'Please enter passphrase on your OneKey device', +}; + +const BLE_TIMEOUT_MS = 30_000; + +type SignWithOneKeyBleParams = { + vaultKey: VaultSigner; + isRemoteKey?: boolean; + serializedPSBTEnvelopFromProps?: SerializedPSBTEnvelop; + signTransaction: (args: { signedSerializedPSBT: string }) => void; +}; + +function SignWithOneKeyBle() { + const { colorMode } = useColorMode(); + const { params } = useRoute(); + const navigation = useNavigation(); + const dispatch = useDispatch(); + const { showToast } = useToastMessage(); + const { translations } = useContext(LocalizationContext); + const { common, error: errorText, signer: signerText } = translations; + + const { + vaultKey, + isRemoteKey = false, + serializedPSBTEnvelopFromProps, + signTransaction, + } = params as SignWithOneKeyBleParams; + + const { signer } = useSignerFromKey(vaultKey); + const { bitcoinNetworkType } = useAppSelector((state) => state.settings); + const serializedPSBTEnvelops: SerializedPSBTEnvelop[] = useAppSelector( + (state) => state.sendAndReceive.sendPhaseTwo.serializedPSBTEnvelops + ); + + const serializedPSBTEnvelop = useMemo(() => { + if (isRemoteKey) return serializedPSBTEnvelopFromProps; + return serializedPSBTEnvelops?.find((envelop) => envelop.xfp === vaultKey.xfp); + }, [isRemoteKey, serializedPSBTEnvelopFromProps, serializedPSBTEnvelops, vaultKey.xfp]); + + const networkType = + bitcoinNetworkType === NetworkType.TESTNET ? NetworkType.TESTNET : NetworkType.MAINNET; + + const [statusMessage, setStatusMessage] = useState('Preparing...'); + const [sdkPrompt, setSdkPrompt] = useState(''); + + // Listen to SDK UI events + useEffect(() => { + const sub = onekeyUIEmitter.addListener(ONEKEY_UI_EVENT, (event: OneKeyUIEvent) => { + if (UI_PROMPTS[event]) { + setSdkPrompt(UI_PROMPTS[event]); + } + }); + return () => sub.remove(); + }, []); + + // Auto-run on mount + useEffect(() => { + const timer = setTimeout(() => runAutoSign(), 300); + return () => clearTimeout(timer); + }, []); + + const withTimeout = (promise: Promise): Promise => + Promise.race([ + promise, + new Promise((_, reject) => + setTimeout(() => reject(new Error('Operation timed out. Device not found.')), BLE_TIMEOUT_MS) + ), + ]); + + const runAutoSign = async () => { + if (!serializedPSBTEnvelop?.serializedPSBT) { + showToast('No PSBT found to sign', ); + return; + } + + try { + setStatusMessage('Checking Bluetooth...'); + const bleReady = await ensureOneKeyBLEReady(); + if (!bleReady.ready) { + showToast('Please turn on Bluetooth and try again', ); + return; + } + + // Use stored connectId for direct connection + const storedConnectId = signer?.extraData?.bleConnectId; + if (!storedConnectId) { + showToast('No stored connection info. Please re-add this device.', ); + return; + } + + // BLE needs a brief scan to discover peripherals + setStatusMessage('Connecting to device...'); + await searchOneKeyDevices(); + + setSdkPrompt(''); + setStatusMessage('Reading device info...'); + const deviceInfo = await withTimeout(getOneKeyDeviceInfo(storedConnectId)); + + setSdkPrompt(''); + setStatusMessage('Signing transaction on device...'); + const signedSerializedPSBT = await withTimeout(signPsbtWithOneKey({ + connectId: storedConnectId, + deviceId: deviceInfo.deviceId, + networkType, + serializedPSBT: serializedPSBTEnvelop.serializedPSBT, + })); + + setSdkPrompt(''); + validatePSBT(serializedPSBTEnvelop.serializedPSBT, signedSerializedPSBT, signer, errorText); + + dispatch( + healthCheckStatusUpdate([ + { signerId: signer.masterFingerprint, status: hcStatusType.HEALTH_CHECK_SIGNING }, + ]) + ); + + if (isRemoteKey) { + signTransaction({ signedSerializedPSBT }); + navigation.dispatch(CommonActions.goBack()); + return; + } + + dispatch(updatePSBTEnvelops({ signedSerializedPSBT, xfp: vaultKey.xfp })); + navigation.dispatch(CommonActions.navigate({ name: 'SignTransactionScreen', merge: true })); + } catch (error) { + captureError(error); + showToast(error?.message || common.somethingWrong, ); + } + }; + + const displayText = sdkPrompt || statusMessage; + + return ( + + + + + + {displayText} + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + gap: 16, + paddingHorizontal: 20, + }, + statusText: { + fontSize: 15, + textAlign: 'center', + }, +}); + +export default SignWithOneKeyBle; diff --git a/src/screens/SignTransaction/SignerModals.tsx b/src/screens/SignTransaction/SignerModals.tsx index 680ea5c175..762dc8c7e1 100644 --- a/src/screens/SignTransaction/SignerModals.tsx +++ b/src/screens/SignTransaction/SignerModals.tsx @@ -593,6 +593,22 @@ const getSupportedSigningOptions = (signerType: SignerType, colorMode) => { }, ], }; + case SignerType.ONEKEY: + return { + supportedSigningOptions: [ + { + title: 'Bluetooth', + icon: ( + } + backgroundColor={`${colorMode}.pantoneGreen`} + width={35} + /> + ), + name: SigningMode.USB, + }, + ], + }; default: return { supportedSigningOptions: [], @@ -618,8 +634,10 @@ function SignerModals({ bitbox02Modal, portalModal, otherSDModal, + oneKeyModal, kruxModal, setOtherSDModal, + setOneKeyModal, setTrezorModal, setBitbox02Modal, setJadeModal, @@ -665,8 +683,10 @@ function SignerModals({ bitbox02Modal: boolean; portalModal: boolean; otherSDModal: boolean; + oneKeyModal: boolean; kruxModal: boolean; setOtherSDModal: any; + setOneKeyModal: any; setTrezorModal: any; setBitbox02Modal: any; setJadeModal: any; @@ -759,6 +779,18 @@ function SignerModals({ }) ); }; + + const navigateToOneKeySigning = (vaultKey: VaultSigner) => { + setOneKeyModal(false); + navigation.dispatch( + CommonActions.navigate('SignWithOneKeyBle', { + signTransaction, + vaultKey, + isRemoteKey, + serializedPSBTEnvelopFromProps, + }) + ); + }; const [registeredSigner, setRegisteredSigner] = useState(null); const [registeredVaultKey, setRegisteredVaultKey] = useState(null); const [registerActiveVault, setRegisterActiveVault] = useState(null); @@ -1664,6 +1696,23 @@ function SignerModals({ ); } + if (signer.type === SignerType.ONEKEY) { + return ( + setOneKeyModal(false)} + title="Connect OneKey" + subTitle="Connect your OneKey via Bluetooth to sign the transaction" + modalBackground={`${colorMode}.modalWhiteBackground`} + textColor={`${colorMode}.textGreen`} + subTitleColor={`${colorMode}.modalSubtitleBlack`} + buttonText={common.proceed} + buttonCallback={() => navigateToOneKeySigning(vaultKey)} + Content={() => null} + /> + ); + } return null; })} diff --git a/src/screens/Vault/HardwareModalMap.tsx b/src/screens/Vault/HardwareModalMap.tsx index fd92704fee..9f6aec8525 100644 --- a/src/screens/Vault/HardwareModalMap.tsx +++ b/src/screens/Vault/HardwareModalMap.tsx @@ -29,6 +29,7 @@ import CVVInputsView from 'src/components/HealthCheck/CVVInputsView'; import DeleteIcon from 'src/assets/images/deleteBlack.svg'; import RecoverImage from 'src/assets/images/recover_white.svg'; import KeeperModal from 'src/components/KeeperModal'; +import OneKeyBleModal from 'src/components/OneKeyBleModal'; import KeyPadView from 'src/components/AppNumPad/KeyPadView'; import { LocalizationContext } from 'src/context/Localization/LocContext'; import { Signer, VaultSigner } from 'src/services/wallets/interfaces/vault'; @@ -542,6 +543,18 @@ const getSignerContent = ( subTitle: ledger.SetupDescription, options: [], }; + case SignerType.ONEKEY: + return { + type: SignerType.ONEKEY, + Illustration: , + Instructions: [ + 'Unlock your OneKey device and enable Bluetooth.', + 'Keep the device nearby and tap Proceed to connect.', + ], + title: isHealthcheck ? `${common.verify} OneKey` : `${signerText.settingUp} OneKey`, + subTitle: 'Connect OneKey hardware wallet via Bluetooth', + options: [], + }; case SignerType.SEED_WORDS: return { type: SignerType.SEED_WORDS, @@ -1052,6 +1065,7 @@ function HardwareModalMap({ const data = useQuery(RealmSchema.BackupHistory); const [backupModalVisible, setBackupModalVisible] = useState(false); const [openSetup, setOpenSetup] = useState(false); + const [onekeyBleModalVisible, setOnekeyBleModalVisible] = useState(false); const getNfcSupport = async () => { const isSupported = await NFC.isNFCSupported(); @@ -1333,6 +1347,7 @@ function HardwareModalMap({ ); }; + const importSeedWordsBasedKey = (mnemonic, remember = false) => { try { const { signer, key } = setupSeedWordsBasedKey(mnemonic, isMultisig, remember); @@ -2021,7 +2036,8 @@ function HardwareModalMap({ signerType === SignerType.PASSPORT || signerType === SignerType.SEED_WORDS || signerType === SignerType.KEEPER || - signerType === SignerType.KRUX + signerType === SignerType.KRUX || + signerType === SignerType.ONEKEY ) { return ( @@ -2165,6 +2181,10 @@ function HardwareModalMap({ return navigateToSetupWithOtherSD(); case SignerType.PORTAL: return navigateToPortalSetup(); + case SignerType.ONEKEY: + close(); + setOnekeyBleModalVisible(true); + return; default: return null; } @@ -2447,6 +2467,22 @@ function HardwareModalMap({ /> {inProgress && } + setOnekeyBleModalVisible(false)} + mode={isHealthcheck ? 'health-check' : 'setup'} + signer={signer} + isMultisig={isMultisig} + addSignerFlow={addSignerFlow} + accountNumber={accountNumber} + onSignerAdded={(addedSigner) => { + setOnekeyBleModalVisible(false); + const navigationState = addSignerFlow + ? { name: 'Home', params: { selectedOption: 'Keys', addedSigner } } + : { name: 'AddSigningDevice', merge: true, params: { addedSigner } }; + navigation.dispatch(CommonActions.navigate(navigationState)); + }} + /> ); } diff --git a/src/screens/Vault/SignerCategoryList.tsx b/src/screens/Vault/SignerCategoryList.tsx index 768ab5688d..7e1edef9ac 100644 --- a/src/screens/Vault/SignerCategoryList.tsx +++ b/src/screens/Vault/SignerCategoryList.tsx @@ -61,6 +61,7 @@ function SignerCategoryList() { { type: SignerType.SPECTER, background: 'pantoneGreen', isTrue: false }, { type: SignerType.KEYSTONE, background: 'brownBackground', isTrue: false }, { type: SignerType.LEDGER, background: 'headerWhite', isTrue: false }, + { type: SignerType.ONEKEY, background: 'headerWhite', isTrue: false }, { type: SignerType.PORTAL, background: 'pantoneGreen', isTrue: false }, { type: SignerType.TREZOR, background: 'brownBackground', isTrue: false }, { type: SignerType.BITBOX02, background: 'headerWhite', isTrue: false }, diff --git a/src/screens/Vault/SigningDeviceIcons.tsx b/src/screens/Vault/SigningDeviceIcons.tsx index 8d4908c215..c750b92660 100644 --- a/src/screens/Vault/SigningDeviceIcons.tsx +++ b/src/screens/Vault/SigningDeviceIcons.tsx @@ -1,6 +1,10 @@ import React from 'react'; import { SignerStorage, SignerType } from 'src/services/wallets/enums'; +import ONEKEYICON from 'src/assets/images/onekey_icon.svg'; +import ONEKEYICONLIGHT from 'src/assets/images/onekey_icon_light.svg'; +import ONEKEYLOGO from 'src/assets/images/onekey_logo.svg'; +import ONEKEYLOGOWHITE from 'src/assets/images/onekey_logo_white.svg'; import COLDCARDICON from 'src/assets/images/coldcard_icon.svg'; import COLDCARDICONLIGHT from 'src/assets/images/coldcard_light.svg'; import COLDCARDLOGO from 'src/assets/images/coldcard_logo.svg'; @@ -84,6 +88,8 @@ import BITBOXGREENLIGHT from 'src/assets/images/bitbox-green-light.svg'; import BITBOXGREENDARK from 'src/assets/images/bitbox-green-dark.svg'; import TREZORGREENLIGHT from 'src/assets/images/trezor-green-light.svg'; import TREZORGREENDARK from 'src/assets/images/trezor-green-dark.svg'; +import ONEKEYGREENLIGHT from 'src/assets/images/onekey-green-light.svg'; +import ONEKEYGREENDARK from 'src/assets/images/onekey-green-dark.svg'; import PortalLogo from 'src/assets/images/portalLogo.svg'; import PortalLogoLight from 'src/assets/images/PortalLogoLight.svg'; import PortalIcon from 'src/assets/images/portalIcon.svg'; @@ -156,6 +162,12 @@ export const SDIcons = ({ type, light = true, width = 20, height = 20 }: SDIconO Logo: colorMode === 'dark' ? : , type: SignerStorage.COLD, }; + case SignerType.ONEKEY: + return { + Icon: getColouredIcon(, , light, width, height), + Logo: colorMode === 'dark' ? : , + type: SignerStorage.COLD, + }; case SignerType.MOBILE_KEY: return { Icon: getColouredIcon(, , light, width, height), @@ -312,6 +324,11 @@ export const SDColoredIcons = (type: SignerType, light = true, width = 20, heigh Icon: getColouredIcon(, , light, width, height), type: SignerStorage.COLD, }; + case SignerType.ONEKEY: + return { + Icon: getColouredIcon(, , light, width, height), + type: SignerStorage.COLD, + }; case SignerType.MOBILE_KEY: return { Icon: getColouredIcon( diff --git a/src/screens/Vault/SigningDeviceList.tsx b/src/screens/Vault/SigningDeviceList.tsx index 2cbdc1f0fd..678125962d 100644 --- a/src/screens/Vault/SigningDeviceList.tsx +++ b/src/screens/Vault/SigningDeviceList.tsx @@ -89,6 +89,7 @@ const SigningDeviceList = () => { SignerType.KEYSTONE, SignerType.KRUX, SignerType.LEDGER, + SignerType.ONEKEY, SignerType.PASSPORT, SignerType.PORTAL, SignerType.SATOCHIP, diff --git a/src/screens/WalletDetails/SignMessageScreen.tsx b/src/screens/WalletDetails/SignMessageScreen.tsx index c3838b8e08..e115b282e2 100644 --- a/src/screens/WalletDetails/SignMessageScreen.tsx +++ b/src/screens/WalletDetails/SignMessageScreen.tsx @@ -18,10 +18,12 @@ import useWallets from 'src/hooks/useWallets'; import { useDispatch } from 'react-redux'; import { refreshWallets } from 'src/store/sagaActions/wallets'; import useVault from 'src/hooks/useVault'; -import { EntityKind, KeyGenerationMode } from 'src/services/wallets/enums'; +import useSigners from 'src/hooks/useSigners'; +import { EntityKind, KeyGenerationMode, SignerType } from 'src/services/wallets/enums'; import ShowXPub from 'src/components/XPub/ShowXPub'; -import { CommonActions } from '@react-navigation/native'; +import { CommonActions, useNavigation } from '@react-navigation/native'; import { InteracationMode } from '../Vault/HardwareModalMap'; +import BLEIcon from 'src/assets/images/usb_white.svg'; import CircleIconWrapper from 'src/components/CircleIconWrapper'; import QRComms from 'src/assets/images/qr_comms.svg'; import ImportIcon from 'src/assets/images/import.svg'; @@ -38,7 +40,8 @@ export const SignMessageScreen = ({ route, navigation }) => { const { walletId = null, vaultId = null, type } = route.params; const wallet = useWallets({ walletIds: [walletId] }).wallets[0]; const { activeVault } = useVault({ vaultId: vaultId ?? '' }); - const { xpriv, addresses } = wallet.specs; + const { vaultSigners } = useSigners(vaultId ?? ''); + const { xpriv, addresses } = wallet?.specs ?? {}; const receiveAddressCache = addresses?.external; const { colorMode } = useColorMode(); const [message, setMessage] = useState(''); @@ -114,8 +117,45 @@ export const SignMessageScreen = ({ route, navigation }) => { navigation.pop(); }; + const hasOneKeySigner = + activeVault?.signers?.some((s) => { + const signerInfo = vaultSigners?.find( + (vs) => vs.masterFingerprint === s.masterFingerprint + ); + return signerInfo?.type === SignerType.ONEKEY; + }) ?? false; + const onSigningMediumSelection = (medium) => { setMediumModal(false); + + // OneKey BLE direct signing + if (medium === 'BLE') { + const onekeyVaultKey = activeVault?.signers?.find((s) => { + const signerInfo = vaultSigners?.find( + (vs) => vs.masterFingerprint === s.masterFingerprint + ); + return signerInfo?.type === SignerType.ONEKEY; + }); + const oneKeySigner = vaultSigners?.find( + (vs) => vs.type === SignerType.ONEKEY + ); + if (onekeyVaultKey && oneKeySigner) { + navigation.dispatch( + CommonActions.navigate('SignMessageOneKeyBle', { + message: message.trim(), + address, + derivationPath: `${onekeyVaultKey.derivationPath}/0/0`, + signer: oneKeySigner, + onSignatureReceived: (sig: string, addr: string) => { + setSignature(sig); + if (addr) setAddress(addr); + }, + }) + ); + } + return; + } + if (mediumMode == MEDIUM_MODES.EXPORT) { if (medium === KeyGenerationMode.QR) { const qrData = WalletOperations.createSignMessageString( @@ -321,7 +361,7 @@ export const SignMessageScreen = ({ route, navigation }) => { modalBackground={`${colorMode}.modalWhiteBackground`} textColor={`${colorMode}.textGreen`} subTitleColor={`${colorMode}.modalSubtitleBlack`} - Content={() => mediumSelectionContent(onSigningMediumSelection)} + Content={() => mediumSelectionContent(onSigningMediumSelection, hasOneKeySigner && mediumMode === MEDIUM_MODES.EXPORT)} /> ); @@ -374,7 +414,7 @@ const styles = StyleSheet.create({ }, }); -const mediumSelectionContent = (onSigningMediumSelection) => { +const mediumSelectionContent = (onSigningMediumSelection, showBleOption = false) => { const { colorMode } = useColorMode(); const options = [ @@ -400,6 +440,21 @@ const mediumSelectionContent = (onSigningMediumSelection) => { ), name: KeyGenerationMode.FILE, }, + ...(showBleOption + ? [ + { + title: 'OneKey (BLE)', + icon: ( + } + backgroundColor={`${colorMode}.pantoneGreen`} + width={35} + /> + ), + name: 'BLE', + }, + ] + : []), ]; return ( diff --git a/src/services/onekeyBle/deviceConstants.ts b/src/services/onekeyBle/deviceConstants.ts new file mode 100644 index 0000000000..9211be8bef --- /dev/null +++ b/src/services/onekeyBle/deviceConstants.ts @@ -0,0 +1,37 @@ +import { ImageSourcePropType } from 'react-native'; +import type { SearchDevice } from '@onekeyfe/hd-core'; + +// ─── Device images ─────────────────────────────────────────────────────────── + +export const DEVICE_IMAGES: Record = { + classic: require('src/assets/images/onekey-devices/classic.png'), + classic1s: require('src/assets/images/onekey-devices/classic.png'), + classicpure: require('src/assets/images/onekey-devices/classic-pure.png'), + touch: require('src/assets/images/onekey-devices/touch.png'), + pro: require('src/assets/images/onekey-devices/pro-black.png'), +}; + +// ─── Device type names ─────────────────────────────────────────────────────── + +export const DEVICE_TYPE_NAMES: Record = { + classic: 'OneKey Classic', + classic1s: 'OneKey Classic 1S', + classicpure: 'OneKey Classic 1S Pure', + touch: 'OneKey Touch', + pro: 'OneKey Pro', +}; + +// ─── Helper functions ──────────────────────────────────────────────────────── + +export const getDeviceImage = (deviceOrType?: SearchDevice | string): ImageSourcePropType | null => { + const dt = typeof deviceOrType === 'string' ? deviceOrType : deviceOrType?.deviceType; + return dt ? DEVICE_IMAGES[dt] || null : null; +}; + +export const getDeviceDisplayName = (device: SearchDevice): string => { + if (device?.name && device.name !== 'Unknown') return device.name; + return DEVICE_TYPE_NAMES[device?.deviceType] || 'OneKey Device'; +}; + +export const getDeviceTypeName = (device: SearchDevice): string => + DEVICE_TYPE_NAMES[device?.deviceType] || 'OneKey'; diff --git a/src/services/onekeyBle/index.ts b/src/services/onekeyBle/index.ts new file mode 100644 index 0000000000..eaffc6a51d --- /dev/null +++ b/src/services/onekeyBle/index.ts @@ -0,0 +1,423 @@ +import HardwareBLESDK from '@onekeyfe/hd-ble-sdk'; +import { + type CoreApi, + type Features, + type SearchDevice, + UI_EVENT, + UI_REQUEST, + UI_RESPONSE, +} from '@onekeyfe/hd-core'; +import { BleManager } from 'react-native-ble-plx'; +import { PermissionsAndroid, Platform } from 'react-native'; +import { DeviceEventEmitter } from 'react-native'; +import { NetworkType } from 'src/services/wallets/enums'; + +// ─── UI Event Emitter ──────────────────────────────────────────────────────── +// Components can listen to these events to show appropriate UI prompts. + +export const onekeyUIEmitter = DeviceEventEmitter; +export const ONEKEY_UI_EVENT = 'onekey-ui-event'; + +// Use SDK's own constants as event values +export type OneKeyUIEvent = typeof UI_REQUEST.REQUEST_PIN | typeof UI_REQUEST.REQUEST_BUTTON | typeof UI_REQUEST.REQUEST_PASSPHRASE | 'idle'; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +type SDKResult = { + success: boolean; + payload: T & { error?: string; message?: string }; +}; + +export type OneKeySignerData = { + multiSigPath: string; + multiSigXpub: string; + singleSigPath: string; + singleSigXpub: string; + taprootPath: string; + taprootXpub: string; + mfp: string; +}; + +export type OneKeyDeviceInfo = { + connectId: string; + deviceId: string; + masterFingerprint: string; + deviceLabel: string; // Device name shown on device (e.g. "My OneKey") + serialNo: string; // Hardware serial number (e.g. "PRA471B") +}; + +// ─── Singleton state ────────────────────────────────────────────────────────── + +let sdkInstance: CoreApi | null = null; +let sdkInitPromise: Promise | null = null; +let bleManager: BleManager | null = null; +let uiListenerBound = false; + +const SCAN_TIMEOUT_MS = 15_000; + +// ─── SDK core ───────────────────────────────────────────────────────────────── + +const getCoreSdk = () => HardwareBLESDK as unknown as CoreApi; + +const handleUIEvent = (message: any) => { + if (!sdkInstance) return; + + if (message?.type === UI_REQUEST.REQUEST_PIN) { + onekeyUIEmitter.emit(ONEKEY_UI_EVENT, UI_REQUEST.REQUEST_PIN); + sdkInstance.uiResponse({ + type: UI_RESPONSE.RECEIVE_PIN, + payload: '@@ONEKEY_INPUT_PIN_IN_DEVICE', + }); + return; + } + + if (message?.type === UI_REQUEST.REQUEST_BUTTON) { + onekeyUIEmitter.emit(ONEKEY_UI_EVENT, UI_REQUEST.REQUEST_BUTTON); + return; + } + + if (message?.type === UI_REQUEST.REQUEST_PASSPHRASE) { + onekeyUIEmitter.emit(ONEKEY_UI_EVENT, UI_REQUEST.REQUEST_PASSPHRASE); + sdkInstance.uiResponse({ + type: UI_RESPONSE.RECEIVE_PASSPHRASE, + payload: { + value: '', + passphraseOnDevice: true, + save: false, + }, + }); + } +}; + +const bindUIListener = (sdk: CoreApi) => { + if (uiListenerBound) return; + sdk.on(UI_EVENT, handleUIEvent); + uiListenerBound = true; +}; + +const getErrorMessage = (result: any): string => { + const code = result?.payload?.code; + const error = result?.payload?.error || result?.payload?.message || ''; + + // Friendly messages for known error codes + if (code === 801 || error.includes('Pin invalid')) { + return 'PIN is incorrect. Please try again on your device.'; + } + if (code === 802 || error.includes('Pin cancelled')) { + return 'PIN entry was cancelled.'; + } + if (error.includes('Failure_ActionCancelled')) { + return 'Action was cancelled on the device.'; + } + + return error || 'OneKey operation failed'; +}; + +export const getOneKeySdk = async (): Promise => { + if (sdkInstance) return sdkInstance; + if (sdkInitPromise) return sdkInitPromise; + + sdkInitPromise = (async () => { + const sdk = getCoreSdk(); + await sdk.init({ debug: false, fetchConfig: true }); + sdkInstance = sdk; + bindUIListener(sdk); + return sdk; + })(); + + try { + return await sdkInitPromise; + } catch (e) { + // Only clear on failure so successful init is cached + sdkInitPromise = null; + throw e; + } +}; + +// ─── BLE readiness ──────────────────────────────────────────────────────────── + +const ensureAndroidBLEPermissions = async (): Promise => { + if (Platform.OS !== 'android') return true; + + const permissions: string[] = []; + if (Number(Platform.Version) >= 31) { + permissions.push( + PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, + PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT + ); + } + permissions.push( + PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, + PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION + ); + + const result = await PermissionsAndroid.requestMultiple(permissions); + return Object.values(result).every((v) => v === PermissionsAndroid.RESULTS.GRANTED); +}; + +/** + * Wait for BLE adapter to reach a definitive state (PoweredOn / PoweredOff / Unauthorized). + * BleManager may report 'Unknown' or 'Resetting' transiently after construction; we listen + * for the settled state via onStateChange (with emitCurrentState=true) and resolve as soon + * as we get something actionable, or after a 3 s timeout. + */ +const waitForBleState = (mgr: BleManager): Promise => + new Promise((resolve) => { + const sub = mgr.onStateChange((state) => { + if (state !== 'Unknown' && state !== 'Resetting') { + sub.remove(); + resolve(state); + } + }, true); // emitCurrentState = true + setTimeout(() => { + sub.remove(); + mgr.state().then(resolve); + }, 3000); + }); + +export const ensureOneKeyBLEReady = async () => { + const hasPermission = await ensureAndroidBLEPermissions(); + if (!hasPermission) { + return { ready: false as const, reason: 'MISSING_PERMISSION' as const }; + } + + if (!bleManager) bleManager = new BleManager(); + + const bleState = await waitForBleState(bleManager); + if (bleState !== 'PoweredOn') { + return { ready: false as const, reason: 'BLE_OFF' as const }; + } + + return { ready: true as const, reason: null }; +}; + +// ─── Device discovery ───────────────────────────────────────────────────────── + +export const searchOneKeyDevices = async (): Promise => { + const sdk = await getOneKeySdk(); + + const result = await Promise.race([ + sdk.searchDevices() as Promise>, + new Promise((_, reject) => + setTimeout(() => reject(new Error('BLE scan timed out')), SCAN_TIMEOUT_MS) + ), + ]); + + if (!result?.success) throw new Error(getErrorMessage(result)); + return result.payload || []; +}; + +/** + * Resolve device_id and master fingerprint from a connected device. + * Returns both so callers can verify device identity. + */ +export const getOneKeyDeviceInfo = async (connectId: string): Promise => { + const sdk = await getOneKeySdk(); + const result = (await sdk.getFeatures(connectId)) as SDKResult; + if (!result?.success) throw new Error(getErrorMessage(result)); + + const deviceId = result?.payload?.device_id; + if (!deviceId) throw new Error('Failed to get OneKey device_id'); + + const serialNo = + (result.payload as any)?.onekey_serial_no || + (result.payload as any)?.onekey_serial || + (result.payload as any)?.serial_no || + ''; + const deviceLabel = + result?.payload?.label || result?.payload?.ble_name || `OneKey ${deviceId.slice(-4)}`; + + // Fetch root fingerprint via a lightweight key derivation + const fpResult = (await sdk.btcGetPublicKey(connectId, deviceId, { + path: "m/84'/0'/0'", + showOnOneKey: false, + useEmptyPassphrase: true, + })) as SDKResult; + + if (!fpResult?.success) throw new Error(getErrorMessage(fpResult)); + + const mfp = toMasterFingerprint(fpResult?.payload?.root_fingerprint); + return { connectId, deviceId, masterFingerprint: mfp, deviceLabel, serialNo }; +}; + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +const getCoinTypeByNetwork = (networkType: NetworkType) => + networkType === NetworkType.TESTNET ? 1 : 0; + +const getCoinNameByNetwork = (networkType: NetworkType) => + networkType === NetworkType.TESTNET ? 'TEST' : 'Bitcoin'; + +const toMasterFingerprint = (rootFingerprint?: number): string => { + if (rootFingerprint === undefined || rootFingerprint === null) { + throw new Error('Missing OneKey root_fingerprint'); + } + const fp = rootFingerprint >>> 0; // Force unsigned 32-bit + return fp.toString(16).padStart(8, '0').toUpperCase(); +}; + +const extractXpub = (payload: any): string => { + if (!payload?.xpub) throw new Error('Invalid xpub from OneKey'); + return payload.xpub; +}; + +// ─── Signer data (xpub fetch) ───────────────────────────────────────────────── + +export const fetchOneKeySignerData = async ({ + connectId, + deviceId, + networkType, + accountNumber = 0, +}: { + connectId: string; + deviceId: string; + networkType: NetworkType; + accountNumber?: number; +}): Promise => { + const sdk = await getOneKeySdk(); + const coinType = getCoinTypeByNetwork(networkType); + + const singleSigPath = `m/84'/${coinType}'/${accountNumber}'`; + const multiSigPath = `m/48'/${coinType}'/${accountNumber}'/2'`; + const taprootPath = `m/86'/${coinType}'/${accountNumber}'`; + + const singleSigResult = (await sdk.btcGetPublicKey(connectId, deviceId, { + path: singleSigPath, + showOnOneKey: false, + useEmptyPassphrase: true, + })) as SDKResult; + if (!singleSigResult?.success) throw new Error(getErrorMessage(singleSigResult)); + + const multiSigResult = (await sdk.btcGetPublicKey(connectId, deviceId, { + path: multiSigPath, + showOnOneKey: false, + useEmptyPassphrase: true, + })) as SDKResult; + if (!multiSigResult?.success) throw new Error(getErrorMessage(multiSigResult)); + + const taprootResult = (await sdk.btcGetPublicKey(connectId, deviceId, { + path: taprootPath, + showOnOneKey: false, + useEmptyPassphrase: true, + })) as SDKResult; + if (!taprootResult?.success) throw new Error(getErrorMessage(taprootResult)); + + const mfp = toMasterFingerprint( + singleSigResult?.payload?.root_fingerprint ?? + multiSigResult?.payload?.root_fingerprint ?? + taprootResult?.payload?.root_fingerprint + ); + + return { + multiSigPath, + multiSigXpub: extractXpub(multiSigResult.payload), + singleSigPath, + singleSigXpub: extractXpub(singleSigResult.payload), + taprootPath, + taprootXpub: extractXpub(taprootResult.payload), + mfp, + }; +}; + +// ─── PSBT signing ───────────────────────────────────────────────────────────── + +const convertSignedPsbtToBase64 = (psbt: string): string => { + const sanitized = psbt?.startsWith('0x') ? psbt.slice(2) : psbt; + if (sanitized && /^[a-fA-F0-9]+$/.test(sanitized)) { + return Buffer.from(sanitized, 'hex').toString('base64'); + } + return psbt; +}; + +export const signPsbtWithOneKey = async ({ + connectId, + deviceId, + networkType, + serializedPSBT, +}: { + connectId: string; + deviceId: string; + networkType: NetworkType; + serializedPSBT: string; +}): Promise => { + const sdk = await getOneKeySdk(); + const psbtHex = Buffer.from(serializedPSBT, 'base64').toString('hex'); + const coin = getCoinNameByNetwork(networkType); + + const result = (await sdk.btcSignPsbt(connectId, deviceId, { + psbt: psbtHex, + coin, + useEmptyPassphrase: true, + })) as SDKResult<{ psbt: string }>; + + if (!result?.success) throw new Error(getErrorMessage(result)); + + const signedPsbt = result?.payload?.psbt; + if (!signedPsbt) throw new Error('OneKey returned empty signed PSBT'); + + return convertSignedPsbtToBase64(signedPsbt); +}; + +// ─── Address verification ───────────────────────────────────────────────────── + +export const verifyAddressOnOneKey = async ({ + connectId, + deviceId, + path, + networkType, +}: { + connectId: string; + deviceId: string; + path: string; + networkType: NetworkType; +}): Promise => { + const sdk = await getOneKeySdk(); + const coin = getCoinNameByNetwork(networkType); + + const result = (await sdk.btcGetAddress(connectId, deviceId, { + path, + coin, + showOnOneKey: true, + useEmptyPassphrase: true, + })) as SDKResult<{ address: string }>; + + if (!result?.success) throw new Error(getErrorMessage(result)); + if (!result?.payload?.address) throw new Error('OneKey returned empty address'); + + return result.payload.address; +}; + +// ─── Message signing ────────────────────────────────────────────────────────── + +export const signMessageWithOneKey = async ({ + connectId, + deviceId, + path, + message, + networkType, +}: { + connectId: string; + deviceId: string; + path: string; + message: string; + networkType: NetworkType; +}): Promise<{ address: string; signature: string }> => { + const sdk = await getOneKeySdk(); + const coin = getCoinNameByNetwork(networkType); + const messageHex = Buffer.from(message, 'utf8').toString('hex'); + + const result = (await sdk.btcSignMessage(connectId, deviceId, { + path, + messageHex, + coin, + useEmptyPassphrase: true, + })) as SDKResult<{ address: string; signature: string }>; + + if (!result?.success) throw new Error(getErrorMessage(result)); + if (!result?.payload?.signature) throw new Error('OneKey returned empty signature'); + + return { + address: result.payload.address, + signature: result.payload.signature, + }; +}; diff --git a/src/services/wallets/enums/index.ts b/src/services/wallets/enums/index.ts index ba2d2ae832..e79ab8e63f 100644 --- a/src/services/wallets/enums/index.ts +++ b/src/services/wallets/enums/index.ts @@ -95,6 +95,7 @@ export enum SignerType { MY_KEEPER = 'MY_KEEPER', TREZOR = 'TREZOR', LEDGER = 'LEDGER', + ONEKEY = 'ONEKEY', COLDCARD = 'COLDCARD', PASSPORT = 'PASSPORT', JADE = 'JADE', diff --git a/src/services/wallets/interfaces/vault.ts b/src/services/wallets/interfaces/vault.ts index 85f2a136b8..19eeda073c 100644 --- a/src/services/wallets/interfaces/vault.ts +++ b/src/services/wallets/interfaces/vault.ts @@ -84,6 +84,7 @@ export type SignerExtraData = { familyName?: string; recordID?: string; thumbnailPath?: string; + bleConnectId?: string; // OneKey BLE connectId for direct reconnection }; export interface HealthCheckDetails { diff --git a/yarn.lock b/yarn.lock index 262e69876d..282f1d90a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1792,7 +1792,7 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== -"@noble/hashes@^1.2.0": +"@noble/hashes@^1.2.0", "@noble/hashes@^1.3.3": version "1.8.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== @@ -1828,6 +1828,58 @@ resolved "https://registry.yarnpkg.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz#3dc35ba0f1e66b403c00b39344f870298ebb1c8e" integrity sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA== +"@onekeyfe/hd-ble-sdk@1.1.16": + version "1.1.16" + resolved "https://registry.yarnpkg.com/@onekeyfe/hd-ble-sdk/-/hd-ble-sdk-1.1.16.tgz#124c5cb9bb3c0d16b6ca2ea615e1867cbf55ba18" + integrity sha512-8t5mhPS7QQkcQXAqJJ0VVr+7EX5vT327U32cUYwxdf5N/opvNRkNc3k1io8N8YO6XprGk5+vAqWLT95/C30/Bw== + dependencies: + "@onekeyfe/hd-core" "1.1.16" + "@onekeyfe/hd-shared" "1.1.16" + "@onekeyfe/hd-transport-react-native" "1.1.16" + +"@onekeyfe/hd-core@1.1.16": + version "1.1.16" + resolved "https://registry.yarnpkg.com/@onekeyfe/hd-core/-/hd-core-1.1.16.tgz#31df132b7cc7e31275b39fdd0a08f001a7afa9a1" + integrity sha512-ViTzwUySTXWB81kVHvZuxaHScfcQUZ1+Tgk9We7UXz8zKwLRsU5GfIbqpS1qSZA65/OCCB/fJoMA0zto84FH0Q== + dependencies: + "@onekeyfe/hd-shared" "1.1.16" + "@onekeyfe/hd-transport" "1.1.16" + axios "1.12.2" + bignumber.js "^9.0.2" + bytebuffer "^5.0.1" + jszip "^3.10.1" + parse-uri "^1.0.7" + semver "^7.3.7" + +"@onekeyfe/hd-shared@1.1.16": + version "1.1.16" + resolved "https://registry.yarnpkg.com/@onekeyfe/hd-shared/-/hd-shared-1.1.16.tgz#7fa2b8f1e52348c924a8fc0be18f7d2f0f1b9043" + integrity sha512-PLIK9yZyT8TpbfN+4EaBKzY4mFXSVNBvOLNKmIxH/GnIy5hDBdGyybbhtCTEXjGr55aFLwR0GBy1I+nYXQmoEw== + +"@onekeyfe/hd-transport-react-native@1.1.16": + version "1.1.16" + resolved "https://registry.yarnpkg.com/@onekeyfe/hd-transport-react-native/-/hd-transport-react-native-1.1.16.tgz#381e7117e08767bc28c28ab23218a32540a7ea66" + integrity sha512-CDlmOWYKgJ2pxelEC4w8dSG44Iz6bSJ1LY65lbFdutZ4ZO84GtUmceZ//K+i88dbULKe4qogcyqKG7P6EZcTqA== + dependencies: + "@onekeyfe/hd-shared" "1.1.16" + "@onekeyfe/hd-transport" "1.1.16" + "@onekeyfe/react-native-ble-utils" "^0.1.4" + react-native-ble-plx "3.5.0" + +"@onekeyfe/hd-transport@1.1.16": + version "1.1.16" + resolved "https://registry.yarnpkg.com/@onekeyfe/hd-transport/-/hd-transport-1.1.16.tgz#3ec4ee1f786df1a630e79113dc561b33c5d57bd9" + integrity sha512-z+HKqYkGz+Ub1GV+REFqJqTG+tpZt7296O3qIlwmsURWdWXX4TcpXmCQDcXMt7vru8m9A4MUMkpGr+23MFBluw== + dependencies: + bytebuffer "^5.0.1" + long "^4.0.0" + protobufjs "^6.11.2" + +"@onekeyfe/react-native-ble-utils@^0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@onekeyfe/react-native-ble-utils/-/react-native-ble-utils-0.1.4.tgz#4a262d30cd226e94f1c697c08a3a85a44310690c" + integrity sha512-DTKjjFKdkGktx/qqdu7STFAjpD6ZI3Rdfcyvv/a1Ho4GBs9F/nOJyxEhqiUNbY4XZoVHSwYy2cxYrdX4zGY6eA== + "@otplib/core@^12.0.1": version "12.0.1" resolved "https://registry.yarnpkg.com/@otplib/core/-/core-12.0.1.tgz#73720a8cedce211fe5b3f683cd5a9c098eaf0f8d" @@ -1866,6 +1918,59 @@ "@otplib/plugin-crypto" "^12.0.1" "@otplib/plugin-thirty-two" "^12.0.1" +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== + "@react-aria/checkbox@3.2.1": version "3.2.1" resolved "https://registry.yarnpkg.com/@react-aria/checkbox/-/checkbox-3.2.1.tgz#493d9d584b4db474645a4565c4f899ee3a579f07" @@ -3591,6 +3696,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/long@^4.0.1": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" + integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== + "@types/node-forge@^1.3.0": version "1.3.14" resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.14.tgz#006c2616ccd65550560c2757d8472eb6d3ecea0b" @@ -3617,6 +3727,13 @@ dependencies: undici-types "~6.19.2" +"@types/node@>=13.7.0": + version "25.2.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.2.3.tgz#9c18245be768bdb4ce631566c7da303a5c99a7f8" + integrity sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ== + dependencies: + undici-types "~7.16.0" + "@types/prop-types@*": version "15.7.15" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7" @@ -3868,6 +3985,11 @@ resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== +Base64@~0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/Base64/-/Base64-0.2.1.tgz#ba3a4230708e186705065e66babdd4c35cf60028" + integrity sha512-reGEWshDmTDQDsCec/HduOO9Wyj6yMOupMfhIf3ugN1TDlK2NQW4DDJSqNNtp380SNcvRfXtO8HSCQot0d0SMw== + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -4215,6 +4337,15 @@ axe-core@^4.10.0: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.11.0.tgz#16f74d6482e343ff263d4f4503829e9ee91a86b6" integrity sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ== +axios@1.12.2: + version "1.12.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.12.2.tgz#6c307390136cf7a2278d09cec63b136dfc6e6da7" + integrity sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.4" + proxy-from-env "^1.1.0" + axios@1.8.2: version "1.8.2" resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.2.tgz#fabe06e241dfe83071d4edfbcaa7b1c3a40f7979" @@ -4395,7 +4526,7 @@ base-x@^1.1.0: resolved "https://registry.yarnpkg.com/base-x/-/base-x-1.1.0.tgz#42d3d717474f9ea02207f6d1aa1f426913eeb7ac" integrity sha512-c0WLeG3K5OlL4Skz2/LVdS+MjggByKhowxQpG+JpCLA48s/bGwIDyzA1naFjywtNvp/37fLK0p0FpjTNNLLUXQ== -base-x@^3.0.2: +base-x@^3.0.2, base-x@^3.0.9: version "3.0.11" resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.11.tgz#40d80e2a1aeacba29792ccc6c5354806421287ff" integrity sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA== @@ -4451,7 +4582,7 @@ bignumber.js@9.1.2: resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== -bignumber.js@^9.0.1: +bignumber.js@^9.0.1, bignumber.js@^9.0.2: version "9.3.1" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.1.tgz#759c5aaddf2ffdc4f154f7b493e1c8770f88c4d7" integrity sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ== @@ -4553,7 +4684,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.8, bn.js@^4.11.9: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.2.tgz#3d8fed6796c24e177737f7cc5172ee04ef39ec99" integrity sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw== -bn.js@^5.2.1, bn.js@^5.2.2: +bn.js@^5.1.1, bn.js@^5.2.1, bn.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.2.tgz#82c09f9ebbb17107cd72cb7fd39bd1f9d0aaa566" integrity sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw== @@ -4599,7 +4730,7 @@ braces@^3.0.2, braces@^3.0.3: dependencies: fill-range "^7.1.1" -brorand@^1.0.1, brorand@^1.1.0: +brorand@^1.0.1, brorand@^1.0.5, brorand@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== @@ -4616,7 +4747,7 @@ browserify-aes@^1.0.4, browserify-aes@^1.0.6, browserify-aes@^1.2.0: inherits "^2.0.1" safe-buffer "^5.0.1" -browserify-cipher@^1.0.0: +browserify-cipher@^1.0.0, browserify-cipher@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== @@ -4644,7 +4775,7 @@ browserify-rsa@^4.0.0, browserify-rsa@^4.1.1: randombytes "^2.1.0" safe-buffer "^5.2.1" -browserify-sign@^4.0.4: +browserify-sign@^4.0.4, browserify-sign@^4.2.3: version "4.2.5" resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.5.tgz#3979269fa8af55ba18aac35deef11b45515cd27d" integrity sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw== @@ -4659,6 +4790,13 @@ browserify-sign@^4.0.4: readable-stream "^2.3.8" safe-buffer "^5.2.1" +browserify-zlib@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" + integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== + dependencies: + pako "~1.0.5" + browserslist@^4.24.0, browserslist@^4.28.0: version "4.28.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.0.tgz#9cefece0a386a17a3cd3d22ebf67b9deca1b5929" @@ -4802,6 +4940,13 @@ buffer@^5.1.0, buffer@^5.4.3, buffer@^5.5.0, buffer@^5.6.0: base64-js "^1.3.1" ieee754 "^1.1.13" +bytebuffer@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/bytebuffer/-/bytebuffer-5.0.1.tgz#582eea4b1a873b6d020a48d58df85f0bba6cfddd" + integrity sha512-IuzSdmADppkZ6DlpycMkm8l9zeEq16fWtLvunEwFiYciR/BHo4E8/xs5piFquG+Za8OWmMqHF8zuRviz2LHvRQ== + dependencies: + long "~3" + bytes@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" @@ -5249,7 +5394,7 @@ crc@^3.8.0: dependencies: buffer "^5.1.0" -create-ecdh@^4.0.0: +create-ecdh@^4.0.0, create-ecdh@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== @@ -5313,6 +5458,24 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6: shebang-command "^2.0.0" which "^2.0.1" +crypto-browserify@^3.12.0: + version "3.12.1" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.1.tgz#bb8921bec9acc81633379aa8f52d69b0b69e0dac" + integrity sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ== + dependencies: + browserify-cipher "^1.0.1" + browserify-sign "^4.2.3" + create-ecdh "^4.0.4" + create-hash "^1.2.0" + create-hmac "^1.1.7" + diffie-hellman "^5.0.3" + hash-base "~3.0.4" + inherits "^2.0.4" + pbkdf2 "^3.1.2" + public-encrypt "^4.0.3" + randombytes "^2.1.0" + randomfill "^1.0.4" + crypto-js@4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" @@ -5589,7 +5752,7 @@ diff-sequences@^29.6.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== -diffie-hellman@^5.0.0: +diffie-hellman@^5.0.0, diffie-hellman@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== @@ -6621,7 +6784,7 @@ for-own@^1.0.0: dependencies: for-in "^1.0.1" -form-data@^4.0.0: +form-data@^4.0.0, form-data@^4.0.4: version "4.0.5" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053" integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== @@ -6922,6 +7085,14 @@ hash-base@^3.0.0, hash-base@^3.1.2: safe-buffer "^5.2.1" to-buffer "^1.2.1" +hash-base@~3.0.4: + version "3.0.5" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.5.tgz#52480e285395cf7fba17dc4c9e47acdc7f248a8a" + integrity sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg== + dependencies: + inherits "^2.0.4" + safe-buffer "^5.2.1" + hash.js@^1.0.0, hash.js@^1.0.3: version "1.1.7" resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" @@ -6987,6 +7158,14 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +http-browserify@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/http-browserify/-/http-browserify-1.7.0.tgz#33795ade72df88acfbfd36773cefeda764735b20" + integrity sha512-Irf/LJXmE3cBzU1eaR4+NEX6bmVLqt1wkmDiA7kBwH7zmb0D8kBAXsDmQ88hhj/qv9iEZKlyGx/hrMcFi8sOHw== + dependencies: + Base64 "~0.2.0" + inherits "~2.0.1" + http-errors@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" @@ -6998,6 +7177,11 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" +https-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" + integrity sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg== + https-proxy-agent@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" @@ -7063,6 +7247,11 @@ image-size@^1.0.2: dependencies: queue "6.0.2" +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + immer@^9.0.7: version "9.0.21" resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" @@ -8065,6 +8254,16 @@ jsonfile@^4.0.0: object.assign "^4.1.4" object.values "^1.1.6" +jszip@^3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" + integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + setimmediate "^1.0.5" + keyv@^4.5.3: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -8123,6 +8322,13 @@ levn@^0.4.1: version "0.3.0" resolved "git+https://github.com/bithyve/libportal-react-native.git#2ff681b0b725009768acadb35b2731fb1a402fa3" +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + lighthouse-logger@^1.0.0: version "1.4.2" resolved "https://registry.yarnpkg.com/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz#aef90f9e97cd81db367c7634292ee22079280aaa" @@ -8304,6 +8510,16 @@ logkitty@^0.7.1: dayjs "^1.8.15" yargs "^15.1.0" +long@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + +long@~3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" + integrity sha512-ZYvPPOMqUwPoDsbJaR10iQJYnMuZhRTvHYl62ErLIEX7RgFlziSBUUvrt3OVfc47QlHHpzPZYP17g3Fv7oeJkg== + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -9376,6 +9592,11 @@ pako@2.1.0: resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== +pako@~1.0.2, pako@~1.0.5: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -9412,6 +9633,11 @@ parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse-uri@^1.0.7: + version "1.0.16" + resolved "https://registry.yarnpkg.com/parse-uri/-/parse-uri-1.0.16.tgz#9e730ccc7358d080f90a29e0334c80c8c845e544" + integrity sha512-WMX9ygt2zzbtd3UlChi8S2Uj/dZa0N9QaotTkyRD7v06c50dor4qEWrM5ZvHiiaZYpXal4otRS9hynwwX0DVoA== + parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -9498,7 +9724,7 @@ pbkdf2@3.0.8: dependencies: create-hmac "^1.1.2" -pbkdf2@^3.0.9, pbkdf2@^3.1.5: +pbkdf2@^3.0.9, pbkdf2@^3.1.2, pbkdf2@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.5.tgz#444a59d7a259a95536c56e80c89de31cc01ed366" integrity sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ== @@ -9683,12 +9909,31 @@ prop-types@*, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.0, prop-t object-assign "^4.1.1" react-is "^16.13.1" +protobufjs@^6.11.2: + version "6.11.4" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.4.tgz#29a412c38bf70d89e537b6d02d904a6f448173aa" + integrity sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.1" + "@types/node" ">=13.7.0" + long "^4.0.0" + proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== -public-encrypt@^4.0.0: +public-encrypt@^4.0.0, public-encrypt@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== @@ -9713,7 +9958,7 @@ punycode@1.2.4: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.2.4.tgz#54008ac972aec74175def9cba6df7fa9d3918740" integrity sha512-h/vscxLPvI2l7k/0dFUKZ5I5TgMCJ/Pl+J6rw77PDuQM6UApf/GaRVkjv/YSm2k+fbp7Yw8dxsoe29DolT7h7w== -punycode@^2.1.0: +punycode@^2.1.0, punycode@^2.1.1: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== @@ -9780,7 +10025,7 @@ randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" -randomfill@^1.0.3: +randomfill@^1.0.3, randomfill@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== @@ -9862,6 +10107,11 @@ react-native-biometrics@2.2.0: resolved "https://registry.yarnpkg.com/react-native-biometrics/-/react-native-biometrics-2.2.0.tgz#6fc3e801b83389ae1f6c9c41f7b37b50e7014c4e" integrity sha512-V2O0s2ic7PxVP76CguCfBXvVEyazbjtWv7r20T7D+ZQqBB1XSZ1WzK/Gnr12CVRxHLUZ3/tKHOsz7mzIaXyNoA== +react-native-ble-plx@3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/react-native-ble-plx/-/react-native-ble-plx-3.5.0.tgz#6cfa33c007bf5cc8b573dfcca8915de57cec60be" + integrity sha512-PeSnRswHLwLRVMQkOfDaRICtrGmo94WGKhlSC09XmHlqX2EuYgH+vNJpGcLkd8lyiYpEJyf8wlFAdj9Akliwmw== + react-native-blob-util@0.18.3: version "0.18.3" resolved "https://registry.yarnpkg.com/react-native-blob-util/-/react-native-blob-util-0.18.3.tgz#e3b408dfdd5b175093bbef23f33811f8ef3407ee" @@ -10132,6 +10382,13 @@ react-native-tcp-socket@5.6.2: buffer "^5.4.3" eventemitter3 "^4.0.7" +react-native-url-polyfill@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/react-native-url-polyfill/-/react-native-url-polyfill-2.0.0.tgz#db714520a2985cff1d50ab2e66279b9f91ffd589" + integrity sha512-My330Do7/DvKnEvwQc0WdcBnFPploYKp9CYlefDXzIdEaA+PAhDYllkvGeEroEzvc4Kzzj2O4yVdz8v6fjRvhA== + dependencies: + whatwg-url-without-unicode "8.0.0-3" + react-native-vision-camera@^4.7.2: version "4.7.3" resolved "https://registry.yarnpkg.com/react-native-vision-camera/-/react-native-vision-camera-4.7.3.tgz#ed03cedabcaec54774f5aa40e69afa30069924d4" @@ -10282,7 +10539,7 @@ readable-stream@^1.0.27-1: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@^2.3.8: +readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@^2.3.8, readable-stream@~2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -10588,6 +10845,25 @@ ripemd160@^2.0.0, ripemd160@^2.0.1, ripemd160@^2.0.3: hash-base "^3.1.2" inherits "^2.0.4" +ripple-address-codec@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/ripple-address-codec/-/ripple-address-codec-4.3.1.tgz#68fbaf646bb8567f70743af7f1ce4479f73efbf6" + integrity sha512-Qa3+9wKVvpL/xYtT6+wANsn0A1QcC5CT6IMZbRJZ/1lGt7gmwIfsrCuz1X0+LCEO7zgb+3UT1I1dc0k/5dwKQQ== + dependencies: + base-x "^3.0.9" + create-hash "^1.1.2" + +ripple-keypairs@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/ripple-keypairs/-/ripple-keypairs-1.3.1.tgz#7fa531df36b138134afb53555a87d7f5eb465b2e" + integrity sha512-dmPlraWKJciFJxHcoubDahGnoIalG5e/BtV6HNDUs7wLXmtnLMHt6w4ed9R8MTL2zNrVPiIdI/HCtMMo0Tm7JQ== + dependencies: + bn.js "^5.1.1" + brorand "^1.0.5" + elliptic "^6.5.4" + hash.js "^1.0.3" + ripple-address-codec "^4.3.1" + "rn-nodeify@github:tradle/rn-nodeify#338d8d6ba8438403093e9409e9a9d88ad884926f": version "10.3.0" resolved "https://codeload.github.com/tradle/rn-nodeify/tar.gz/338d8d6ba8438403093e9409e9a9d88ad884926f" @@ -10830,6 +11106,11 @@ set-proto@^1.0.0: es-errors "^1.3.0" es-object-atoms "^1.0.0" +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" @@ -11865,11 +12146,25 @@ webidl-conversions@^3.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== +webidl-conversions@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" + integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== + whatwg-fetch@^3.0.0: version "3.6.20" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70" integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg== +whatwg-url-without-unicode@8.0.0-3: + version "8.0.0-3" + resolved "https://registry.yarnpkg.com/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz#ab6df4bf6caaa6c85a59f6e82c026151d4bb376b" + integrity sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig== + dependencies: + buffer "^5.4.3" + punycode "^2.1.1" + webidl-conversions "^5.0.0" + whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"