diff --git a/packages/connect-examples/expo-example/App.tsx b/packages/connect-examples/expo-example/App.tsx index 28d9ab294..367f0354a 100644 --- a/packages/connect-examples/expo-example/App.tsx +++ b/packages/connect-examples/expo-example/App.tsx @@ -1,9 +1,13 @@ import React from 'react'; import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { enableFreeze, enableScreens } from 'react-native-screens'; +import { View } from 'react-native'; import HomeScreen from './src/views/HomeScreen'; import MockScreen from './src/views/MockConnect'; import { getHardwareSDKInstance } from './src/utils/hardwareInstance'; +import PassphraseTestScreen from './src/views/PassphraseTestScreen'; +import SDKProvider from './src/provider/SDKProvider'; getHardwareSDKInstance(); @@ -11,10 +15,13 @@ const Stack = createNativeStackNavigator(); export default function App() { return ( - - - - + + + + + + + ); } diff --git a/packages/connect-examples/expo-example/src/components/CommonInput.tsx b/packages/connect-examples/expo-example/src/components/CommonInput.tsx new file mode 100644 index 000000000..892e522f1 --- /dev/null +++ b/packages/connect-examples/expo-example/src/components/CommonInput.tsx @@ -0,0 +1,48 @@ +import { useCallback } from 'react'; +import { StyleSheet, Text, TextInput, View } from 'react-native'; + +export type CommonInputProps = { + value: string; + onChange: (value: string) => void; + label: string; + type?: 'number' | 'text'; +}; +export const CommonInput = ({ value, onChange, label, type }: CommonInputProps) => { + const onChangeCallback = useCallback( + (text: string) => { + if (type === 'number') { + onChange(text.replace(/[^\d]+/, '')); + } else { + onChange(text); + } + }, + [onChange, type] + ); + + return ( + + {label} + + + ); +}; +const styles = StyleSheet.create({ + input: { + borderColor: '#E0E0E0', + borderWidth: 1, + borderRadius: 4, + padding: 6, + margin: 2, + fontSize: 16, + }, + label: { + fontWeight: 'bold', + marginBottom: 4, + fontSize: 14, + }, +}); diff --git a/packages/connect-examples/expo-example/src/components/CommonParamsView.tsx b/packages/connect-examples/expo-example/src/components/CommonParamsView.tsx index bc5dcb06d..1f318cdcd 100644 --- a/packages/connect-examples/expo-example/src/components/CommonParamsView.tsx +++ b/packages/connect-examples/expo-example/src/components/CommonParamsView.tsx @@ -1,35 +1,7 @@ -import { useContext } from 'react'; -import { StyleSheet, Switch, Text, TextInput, View } from 'react-native'; +import { StyleSheet, Switch, Text, View } from 'react-native'; import { useCommonParams } from '../provider/CommonParamsProvider'; - -type SwitchInputProps = { - value: boolean; - onToggle: (value: boolean) => void; - label: string; -}; -const SwitchInput = ({ value, onToggle, label }: SwitchInputProps) => ( - - {label} - - -); - -type NumberInputProps = { - value: string; - onChange: (value: string) => void; - label: string; -}; -const NumberInput = ({ value, onChange, label }: NumberInputProps) => ( - - {label} - onChange(text.replace(/[^\d]+/, ''))} - /> - -); +import { CommonInput } from './CommonInput'; +import { SwitchInput } from './SwitchInput'; export default function CommonParamsView() { const { commonParams, setCommonParams: setOptionalParams } = useCommonParams(); @@ -47,23 +19,27 @@ export default function CommonParamsView() { value={!!commonParams.keepSession} onToggle={value => handleSetParam('keepSession', value)} /> - handleSetParam('retryCount', parseInt(value))} /> - handleSetParam('pollIntervalTime', parseInt(value))} /> - handleSetParam('timeout', parseInt(value))} /> - handleSetParam('passphraseState', value)} /> diff --git a/packages/connect-examples/expo-example/src/components/HandleSDKEvents.tsx b/packages/connect-examples/expo-example/src/components/HandleSDKEvents.tsx index ec9688f45..db881e047 100644 --- a/packages/connect-examples/expo-example/src/components/HandleSDKEvents.tsx +++ b/packages/connect-examples/expo-example/src/components/HandleSDKEvents.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useEffect, useState } from 'react'; +import { useCallback, useContext, useEffect, useRef, useState } from 'react'; import { CoreMessage, DEVICE, @@ -8,6 +8,7 @@ import { UI_RESPONSE, } from '@onekeyfe/hd-core'; import { View } from 'react-native'; +import { useIsFocused } from '@react-navigation/native'; import HardwareSDKContext from '../provider/HardwareSDKContext'; import { ReceivePin } from './ReceivePin'; @@ -18,6 +19,13 @@ export default function HandleSDKEvents() { const [showPinInput, setShowPinInput] = useState(false); const [pinValue, setPinValue] = useState(''); + const focus = useIsFocused(); + const focusRef = useRef(false); + + useEffect(() => { + focusRef.current = focus; + }, [focus]); + useEffect(() => { // 监听 SDK 事件 if (registerListener) { @@ -30,6 +38,7 @@ export default function HandleSDKEvents() { }); SDK.on(UI_EVENT, (message: CoreMessage) => { + if (!focusRef.current) return; console.log('TopLEVEL EVENT ===>>>>: ', message); if (message.type === UI_REQUEST.REQUEST_PIN) { // setShowPinInput(true); @@ -75,7 +84,12 @@ export default function HandleSDKEvents() { console.log('example get disconnect event: ', message); }); registerListener = true; - }, [SDK, HardwareLowLevelSDK]); + + return () => { + registerListener = false; + SDK.removeAllListeners(); + }; + }, [SDK, HardwareLowLevelSDK, focus]); // 输入 pin 码的确认回调 const onConfirmPin = useCallback( diff --git a/packages/connect-examples/expo-example/src/components/PassphraseTest/TestMultiWalletChangeView.tsx b/packages/connect-examples/expo-example/src/components/PassphraseTest/TestMultiWalletChangeView.tsx new file mode 100644 index 000000000..fb3fae278 --- /dev/null +++ b/packages/connect-examples/expo-example/src/components/PassphraseTest/TestMultiWalletChangeView.tsx @@ -0,0 +1,445 @@ +import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { CoreMessage, UI_EVENT, UI_REQUEST, UI_RESPONSE } from '@onekeyfe/hd-core'; +import { Button, Text, TextInput, View } from 'react-native'; +import { TestChain, requestAddress, styles } from './utils'; +import HardwareSDKContext from '../../provider/HardwareSDKContext'; +import { useDevice } from '../../provider/DeviceProvider'; +import { CommonInput } from '../CommonInput'; + +type TestPresupposeWallet = { + id: string; + passphrase: string; + emptyPassphraseState?: boolean; + addressList?: { + index: number; + chain: TestChain; + }[]; +}; +type TestResult = { + id: string; + passphrase: string; + passphraseState?: string; + emptyPassphraseState?: boolean; + addressList?: { + index: number; + chain: TestChain; + address?: string; + }[]; +}; +const testPresuppose: { + normal: TestPresupposeWallet[]; + normalWithNormalWallet: TestPresupposeWallet[]; +} = { + normalWithNormalWallet: [ + { + id: '0', + passphrase: '', + emptyPassphraseState: true, + addressList: [ + { + index: 0, + chain: 'btc', + }, + { + index: 1, + chain: 'evm', + }, + ], + }, + { + id: '1', + passphrase: '111', + addressList: [ + { + index: 0, + chain: 'btc', + }, + { + index: 1, + chain: 'evm', + }, + { + index: 2, + chain: 'dot', + }, + { + index: 3, + chain: 'ada', + }, + ], + }, + { + id: '2', + passphrase: '222', + addressList: [ + { + index: 0, + chain: 'btc', + }, + { + index: 1, + chain: 'dot', + }, + ], + }, + { + id: '3', + passphrase: '444', + addressList: [ + { + index: 0, + chain: 'evm', + }, + { + index: 1, + chain: 'ada', + }, + ], + }, + ], + normal: [ + { + id: '1', + passphrase: '111', + addressList: [ + { + index: 0, + chain: 'btc', + }, + { + index: 1, + chain: 'evm', + }, + { + index: 2, + chain: 'dot', + }, + { + index: 3, + chain: 'ada', + }, + ], + }, + { + id: '2', + passphrase: '222', + addressList: [ + { + index: 0, + chain: 'btc', + }, + { + index: 1, + chain: 'dot', + }, + ], + }, + { + id: '3', + passphrase: '444', + addressList: [ + { + index: 0, + chain: 'evm', + }, + { + index: 1, + chain: 'ada', + }, + ], + }, + ], +}; + +export default function TestMultiWalletChangeView() { + const { sdk: SDK } = useContext(HardwareSDKContext); + const { selectedDevice } = useDevice(); + + const [changeWalletCount, setChangeWalletCount] = useState(20); + const [testResult, setTestResult] = useState<{ + done?: boolean; + payload: string; + }>(); + + const [walletParams, setWalletParams] = useState( + JSON.stringify(testPresuppose.normalWithNormalWallet, null, 2) + ); + const [walletResult, setWalletResult] = useState(); + const currentPassphrase = useRef(''); + + useEffect(() => { + if (testResult?.done && !!SDK) { + SDK.removeAllListeners(UI_EVENT); + } + }, [SDK, testResult?.done]); + + const setNormalWithNormalWalletTest = useCallback(() => { + setWalletParams(JSON.stringify(testPresuppose.normalWithNormalWallet, null, 2)); + }, []); + const setNormalTest = useCallback(() => { + setWalletParams(JSON.stringify(testPresuppose.normal, null, 2)); + }, []); + + const beginTest = useCallback(async () => { + const walletList: TestPresupposeWallet[] = JSON.parse(walletParams ?? '[]'); + + const testWalletCount = walletList?.length ?? 0; + if (testWalletCount < 2) { + setTestResult({ + done: true, + payload: '测试钱包数量至少为 2', + }); + return; + } + + if (!SDK) return; + SDK.removeAllListeners(UI_EVENT); + + setTestResult({ + done: false, + payload: '测试中...', + }); + + const connectId = selectedDevice?.connectId ?? ''; + const deviceId = selectedDevice?.features?.device_id ?? ''; + + SDK.on(UI_EVENT, (message: CoreMessage) => { + console.log('TopLEVEL EVENT ===>>>>: ', message); + if (message.type === UI_REQUEST.REQUEST_PIN) { + SDK.uiResponse({ + type: UI_RESPONSE.RECEIVE_PIN, + payload: '@@ONEKEY_INPUT_PIN_IN_DEVICE', + }); + } + if (message.type === UI_REQUEST.REQUEST_PASSPHRASE) { + setTimeout(() => { + SDK.uiResponse({ + type: UI_RESPONSE.RECEIVE_PASSPHRASE, + payload: { + value: currentPassphrase.current, + }, + }); + }, 200); + } + }); + + const cacheWallet = new Map< + string, + { + passphraseState?: string; + addressList?: Map< + number, + { + address: string; + } + >; + } + >(); + + for (const item of walletList ?? []) { + let passphraseState: string | undefined; + currentPassphrase.current = item.passphrase; + if (!item.emptyPassphraseState) { + const passphraseStateRes = await SDK.getPassphraseState(connectId, {}); + if (!passphraseStateRes.success) { + setTestResult({ + done: true, + payload: `getPassphraseState failed ${passphraseStateRes?.payload?.error}`, + }); + return; + } + if (!passphraseStateRes.payload) { + setTestResult({ + done: true, + payload: 'passphrase is not enabled on the device.', + }); + return; + } + + passphraseState = passphraseStateRes.payload; + } + + const cacheAddressList = new Map< + number, + { + address: string; + } + >(); + for (const addressItem of item.addressList ?? []) { + const addressRes = await requestAddress({ + sdk: SDK, + testChain: addressItem.chain, + connectId, + deviceId, + passphraseState, + useEmptyPassphrase: item.emptyPassphraseState, + }); + + if (!addressRes.success) { + setTestResult({ + done: true, + payload: `GetAddress 失败 ${addressRes?.payload?.error}`, + }); + return; + } + + cacheAddressList.set(addressItem.index, { + address: addressRes.payload.address ?? '', + }); + } + + cacheWallet.set(item.id, { + passphraseState, + addressList: cacheAddressList, + }); + } + + const result: TestResult[] = [ + ...(walletList ?? []).map(current => { + const walletRes = cacheWallet.get(current.id); + if (!walletRes) return current; + return { + ...current, + passphraseState: walletRes?.passphraseState, + addressList: current.addressList?.map(addressItem => { + const addressRes = walletRes?.addressList?.get(addressItem.index); + if (!addressRes) return addressItem; + return { + ...addressItem, + address: addressRes?.address, + }; + }), + }; + }), + ]; + + setWalletResult(result); + + // for changeWalletCount count + for (let index = 0; index < changeWalletCount; index++) { + const item = walletList?.[index % (testWalletCount ?? 0)]; + if (!item) { + setTestResult({ + done: true, + payload: 'changeWalletCount error', + }); + return; + } + + currentPassphrase.current = item.passphrase; + for (const addressItem of item.addressList ?? []) { + const passphraseState = result.find(current => current.id === item.id)?.passphraseState; + const address = result + .find(current => current.id === item.id) + ?.addressList?.find(current => current.index === addressItem.index)?.address; + + const addressRes = await requestAddress({ + sdk: SDK, + testChain: addressItem.chain, + connectId, + deviceId, + passphraseState, + useEmptyPassphrase: item.emptyPassphraseState, + }); + + if (!addressRes.success) { + setTestResult({ + done: true, + payload: `id:${item.id} index:${addressItem.index} ${addressItem.chain} GetAddress 失败 ${addressRes?.payload?.error}`, + }); + return; + } + + if (address !== addressRes.payload.address) { + setTestResult({ + done: true, + payload: `id:${item.id} index:${addressItem.index} ${addressItem.chain} GetAddress 结果不一致!!!!`, + }); + return; + } + } + } + + setTestResult({ + done: true, + payload: '测试结果正确', + }); + }, [ + SDK, + changeWalletCount, + selectedDevice?.connectId, + selectedDevice?.features?.device_id, + walletParams, + ]); + + const ContentView = useMemo(() => { + console.log(); + let result; + if (testResult?.done === undefined) { + result = '未测试'; + } else if (testResult?.done) { + result = '完成'; + } else { + result = '进行中'; + } + + return ( + + Passphrase Multi Change Test + 开始之后根据提示解锁硬件, 根据硬件提示确认 passphrase, 不用自己输入 + 测试步骤:1. 根据您填写的参数创建 N 个钱包 + 2. 获取这些钱包多个网络的 address + + 3. 根据测试切换次数,从第一个钱包开始一直切换。会自动填写 passphrase,注意在硬件按确认 + + +