diff --git a/package.json b/package.json index 4fc508f76411..beefb6b100cb 100644 --- a/package.json +++ b/package.json @@ -147,10 +147,10 @@ "@onekeyfe/hd-transport": "1.1.26-alpha.11", "@onekeyfe/hd-transport-electron": "1.1.26-alpha.11", "@onekeyfe/hd-web-sdk": "1.1.26-alpha.11", - "@onekeyfe/hwk-adapter-core": "1.1.26-alpha.11", - "@onekeyfe/hwk-ledger-adapter": "1.1.26-alpha.11", - "@onekeyfe/hwk-ledger-connector-ble": "1.1.26-alpha.11", - "@onekeyfe/hwk-ledger-connector-webhid": "1.1.26-alpha.11", + "@onekeyfe/hwk-adapter-core": "1.1.26-alpha.12", + "@onekeyfe/hwk-ledger-adapter": "1.1.26-alpha.12", + "@onekeyfe/hwk-ledger-connector-ble": "1.1.26-alpha.12", + "@onekeyfe/hwk-ledger-connector-webhid": "1.1.26-alpha.12", "@onekeyfe/onekey-cross-webview": "2.2.68", "@polkadot/extension-inject": "0.54.1", "@polkadot/types": "14.3.1", diff --git a/packages/kit-bg/src/dbs/local/LocalDbBase.ts b/packages/kit-bg/src/dbs/local/LocalDbBase.ts index 8255fb99bfa9..cc1d7639a587 100644 --- a/packages/kit-bg/src/dbs/local/LocalDbBase.ts +++ b/packages/kit-bg/src/dbs/local/LocalDbBase.ts @@ -3320,10 +3320,16 @@ export abstract class LocalDbBase extends LocalDbBaseContainer { const { connectId } = device; const resolvedVendor = vendor ?? EHardwareVendor.onekey; const profile = getVendorProfile(resolvedVendor); + const isUsbTransport = + transportType === EHardwareTransportType.WEBUSB || + transportType === EHardwareTransportType.Bridge; - // Allow empty connectId only for vendors without persistent USB connectId - // (e.g. Ledger DMK generates temporary UUIDs that change every session). - if (!connectId && profile.hasPersistentConnectId('usb')) { + // Empty connectId is allowed only for non-persistent USB transports. + if ( + !connectId && + (profile.hasPersistentConnectId('usb') || + (profile.isThirdParty && !isUsbTransport)) + ) { throw new OneKeyLocalError('createHwWallet ERROR: connectId is required'); } const context = await this.getContext(); diff --git a/packages/kit-bg/src/services/ServiceAccount/ServiceAccount.ts b/packages/kit-bg/src/services/ServiceAccount/ServiceAccount.ts index fa8f002e7119..802b57d58472 100644 --- a/packages/kit-bg/src/services/ServiceAccount/ServiceAccount.ts +++ b/packages/kit-bg/src/services/ServiceAccount/ServiceAccount.ts @@ -101,6 +101,7 @@ import networkUtils from '@onekeyhq/shared/src/utils/networkUtils'; import { EMnemonicType } from '@onekeyhq/shared/src/utils/secret'; import stringUtils from '@onekeyhq/shared/src/utils/stringUtils'; import timerUtils from '@onekeyhq/shared/src/utils/timerUtils'; +import { EHardwareTransportType } from '@onekeyhq/shared/types'; import type { IServerNetwork } from '@onekeyhq/shared/types'; import type { IBatchCreateAccount, @@ -3128,8 +3129,20 @@ class ServiceAccount extends ServiceBase { | EHardwareVendor | undefined); const vendorProfile = vendor ? getVendorProfile(vendor) : undefined; + const isUsbTransport = + transportType === EHardwareTransportType.WEBUSB || + transportType === EHardwareTransportType.Bridge; + if ( + vendorProfile?.isThirdParty && + !params.device.connectId && + !isUsbTransport + ) { + throw new OneKeyLocalError( + 'createHWWalletBase ERROR: connectId is required for non-USB third-party hardware', + ); + } - // Vendors without persistent USB connectId don't need compatible resolution + // Skip compatibility lookup for vendors without persistent USB connectId. const compatibleConnectId = vendorProfile?.isThirdParty && !vendorProfile.hasPersistentConnectId('usb') @@ -3139,18 +3152,11 @@ class ServiceAccount extends ServiceBase { featuresDeviceId: params.device.deviceId ?? '', hardwareCallContext: EHardwareCallContext.USER_INTERACTION, }); - const searchDeviceId = params.device.deviceId ?? ''; const deviceId = deviceUtils.getRawDeviceId({ device: params.device, features, }); - console.log('createHWWalletBase paramsInfo', { - connectId: compatibleConnectId, - deviceId, - searchDeviceId, - }); - let xfp: string | undefined; if (fillingXfpByCallingSdk && !isMockedStandardHwWallet) { xfp = await this.backgroundApi.serviceHardware.buildHwWalletXfp({ @@ -3161,9 +3167,8 @@ class ServiceAccount extends ServiceBase { withUserInteraction: true, vendor, }); - console.log('createHWWalletBase xfp', xfp, compatibleConnectId, deviceId); } - // if the connectId is not compatible, maybe the device is new bluetooth connection device, refresh the device info + // Refresh DB info when compatibility lookup resolves to another connectId. if (compatibleConnectId !== params.device.connectId) { const refreshedDevice = await localDb.getDeviceByQuery({ connectId: params.device.connectId || compatibleConnectId, @@ -3205,10 +3210,7 @@ class ServiceAccount extends ServiceBase { : undefined, transportType, }); - // Chain fingerprints for third-party vendors (Ledger) are generated on-demand - // by ensureLedgerChainFingerprint() in KeyringHardwareBase when a chain's - // keyring is first used. This ensures the fingerprint is generated using the - // SDK's getChainFingerprint() method (single source of truth for hashing). + // Third-party chain fingerprints are generated lazily by the keyring via SDK. appEventBus.emit(EAppEventBusNames.WalletUpdate, undefined); return result; @@ -4154,6 +4156,54 @@ class ServiceAccount extends ServiceBase { return result; } + /** Onboarding orphan cleanup. HW + ledger + 0 accounts; no password prompt. */ + @backgroundMethod() + async removeFailedOnboardingHwWallet({ walletId }: { walletId: string }) { + if (!walletId) { + throw new OneKeyLocalError('walletId is required'); + } + if (!accountUtils.isHwWallet({ walletId })) { + throw new OneKeyLocalError( + 'removeFailedOnboardingHwWallet: only HW wallet allowed', + ); + } + + const wallet = await this.getWalletSafe({ walletId }); + if (!wallet) { + // Already removed; keep cleanup idempotent. + return; + } + + const vendor = wallet.associatedDeviceInfo?.vendor; + if (vendor !== EHardwareVendor.ledger) { + throw new OneKeyLocalError( + `removeFailedOnboardingHwWallet: vendor must be ledger (got ${ + vendor ?? 'undefined' + })`, + ); + } + + // Re-check every indexed account before treating it as an orphan. + const { accounts: indexedAccounts } = await this.getIndexedAccountsOfWallet( + { + walletId, + }, + ); + for (const indexed of indexedAccounts) { + const { accounts } = await this.getAccountsInSameIndexedAccountId({ + indexedAccountId: indexed.id, + }); + if (accounts.length > 0) { + throw new OneKeyLocalError( + `removeFailedOnboardingHwWallet: wallet ${walletId} has accounts; not an orphan`, + ); + } + } + + // localDb.removeWallet handles events, unused devices, and indexed accounts. + await localDb.removeWallet({ walletId }); + } + async buildAccountXpubOrAddress({ getAccountXpubFn, getAccountAddressFn, @@ -5501,34 +5551,47 @@ class ServiceAccount extends ServiceBase { const isHwWallet = accountUtils.isHwWallet({ walletId }); if (isHwWallet) { - const wallet = await localDb.getWalletSafe({ walletId }); - if ( - wallet && - !wallet?.deprecated && - !accountUtils.isValidWalletXfp({ xfp: wallet.xfp }) - ) { - await hardwareWalletXfpStatusAtom.set((v) => ({ - ...v, - [walletId]: { - ...v?.[walletId], - xfpMissing: true, - }, - })); - } + // Third-party HW uses chain fingerprints, not wallet xfp. + const isThirdParty = await this.isThirdPartyHwByWalletId({ walletId }); + if (isThirdParty) { + const status = await hardwareWalletXfpStatusAtom.get(); + if (status?.[walletId]?.xfpMissing) { + await hardwareWalletXfpStatusAtom.set((v) => ({ + ...v, + [walletId]: { ...v?.[walletId], xfpMissing: false }, + })); + } + } else { + const wallet = await localDb.getWalletSafe({ walletId }); + if ( + wallet && + !wallet?.deprecated && + !accountUtils.isValidWalletXfp({ xfp: wallet.xfp }) + ) { + await hardwareWalletXfpStatusAtom.set((v) => ({ + ...v, + [walletId]: { + ...v?.[walletId], + xfpMissing: true, + }, + })); + } - const hardwareWalletXfpStatus = await hardwareWalletXfpStatusAtom.get(); - if ( - hardwareWalletXfpStatus?.[walletId]?.xfpMissing && - wallet && - accountUtils.isValidWalletXfp({ xfp: wallet.xfp }) - ) { - await hardwareWalletXfpStatusAtom.set((v) => ({ - ...v, - [walletId]: { - ...v?.[walletId], - xfpMissing: false, - }, - })); + const hardwareWalletXfpStatus = + await hardwareWalletXfpStatusAtom.get(); + if ( + hardwareWalletXfpStatus?.[walletId]?.xfpMissing && + wallet && + accountUtils.isValidWalletXfp({ xfp: wallet.xfp }) + ) { + await hardwareWalletXfpStatusAtom.set((v) => ({ + ...v, + [walletId]: { + ...v?.[walletId], + xfpMissing: false, + }, + })); + } } } } diff --git a/packages/kit-bg/src/services/ServiceBatchCreateAccount/ServiceBatchCreateAccount.ts b/packages/kit-bg/src/services/ServiceBatchCreateAccount/ServiceBatchCreateAccount.ts index dfecca00dfbd..bb8ef1c86df0 100644 --- a/packages/kit-bg/src/services/ServiceBatchCreateAccount/ServiceBatchCreateAccount.ts +++ b/packages/kit-bg/src/services/ServiceBatchCreateAccount/ServiceBatchCreateAccount.ts @@ -1,5 +1,5 @@ import { HardwareErrorCode } from '@onekeyfe/hd-shared'; -import { HardwareErrorCode as ThirdPartyHwErrorCode } from '@onekeyfe/hwk-adapter-core'; +import { ORPHAN_ELIGIBLE_ERROR_CODES } from '@onekeyfe/hwk-adapter-core'; import { chunk, isNil, range, uniqBy } from 'lodash'; import { @@ -215,7 +215,6 @@ class ServiceBatchCreateAccount extends ServiceBase { networkId: payload.params.networkId, }), ]); - const hwRootFingerprintInfo: { rootFingerprint: number | undefined; } = { @@ -362,7 +361,6 @@ class ServiceBatchCreateAccount extends ServiceBase { walletId, hardwareCallContext: EHardwareCallContext.USER_INTERACTION, }); - let hwAllNetworkPrepareAccountsResponse: | IHwAllNetworkPrepareAccountsResponse | undefined; @@ -927,7 +925,6 @@ class ServiceBatchCreateAccount extends ServiceBase { walletId: params.walletId, hardwareCallContext: EHardwareCallContext.USER_INTERACTION, }); - let hwAllNetworkPrepareAccountsResponse: | IHwAllNetworkPrepareAccountsResponse | undefined; @@ -1094,17 +1091,15 @@ class ServiceBatchCreateAccount extends ServiceBase { isHardwareErrorByCode({ error, code: [ + // OneKey HW (legacy enum) HardwareErrorCode.DeviceNotFound, - // **** PIN\passphrase cancel HardwareErrorCode.PinCancelled, HardwareErrorCode.ActionCancelled, HardwareErrorCode.CallQueueActionCancelled, - HardwareErrorCode.DeviceInterruptedFromOutside, // cancel PIN from app - HardwareErrorCode.DeviceInterruptedFromUser, // cancel PIN from app - // **** third-party hardware - ThirdPartyHwErrorCode.UserAborted, - ThirdPartyHwErrorCode.DeviceDisconnected, - ThirdPartyHwErrorCode.DeviceAppStuck, + HardwareErrorCode.DeviceInterruptedFromOutside, + HardwareErrorCode.DeviceInterruptedFromUser, + // Third-party HW batch-abort codes from SDK. + ...ORPHAN_ELIGIBLE_ERROR_CODES, ], }) ) { diff --git a/packages/kit-bg/src/services/ServiceHardware/ServiceHardware.ledgerBle.test.ts b/packages/kit-bg/src/services/ServiceHardware/ServiceHardware.ledgerBle.test.ts new file mode 100644 index 000000000000..7a293a83a464 --- /dev/null +++ b/packages/kit-bg/src/services/ServiceHardware/ServiceHardware.ledgerBle.test.ts @@ -0,0 +1,101 @@ +import { mapThirdPartyDeviceToSearchDevice } from './thirdPartyDeviceMapping'; + +describe('ServiceHardware Ledger BLE device mapping', () => { + it('preserves the BLE four-character connectId and actual name when connectionType is missing', () => { + const ledgerDevice = { + vendor: 'ledger', + model: 'nanoX', + firmwareVersion: '', + deviceId: '0738', + connectId: '0738', + label: 'Leo', + }; + + const result = mapThirdPartyDeviceToSearchDevice({ + device: ledgerDevice as never, + defaultDeviceName: 'Ledger', + }); + + expect(result).toMatchObject({ + connectId: '0738', + name: 'Leo', + }); + }); + + it('uses the actual BLE device name instead of the vendor default', () => { + const result = mapThirdPartyDeviceToSearchDevice({ + device: { + vendor: 'ledger', + model: 'nanoX', + firmwareVersion: '', + deviceId: 'A58F', + connectId: 'A58F', + label: 'Andox', + connectionType: 'ble', + } as never, + defaultDeviceName: 'Ledger', + }); + + expect(result).toMatchObject({ + connectId: 'A58F', + name: 'Andox', + }); + }); + + it('falls back to the device model when the BLE device has no name', () => { + const result = mapThirdPartyDeviceToSearchDevice({ + device: { + vendor: 'ledger', + model: 'nanoX', + firmwareVersion: '', + deviceId: 'A58F', + connectId: 'A58F', + connectionType: 'ble', + } as never, + defaultDeviceName: 'Ledger', + }); + + expect(result).toMatchObject({ + connectId: 'A58F', + name: 'nanoX', + }); + }); + + it('treats explicit USB devices as USB even when connectId looks like a BLE id', () => { + const result = mapThirdPartyDeviceToSearchDevice({ + device: { + vendor: 'ledger', + model: 'nanoX', + firmwareVersion: '', + deviceId: 'A58F', + connectId: 'A58F', + label: 'Andox', + connectionType: 'usb', + } as never, + defaultDeviceName: 'Ledger', + canMatchDeviceByConnectId: () => true, + }); + + expect(result).toMatchObject({ + connectId: null, + name: 'Andox', + }); + }); + + it('rejects explicit BLE devices without a valid four-character connectId', () => { + expect(() => + mapThirdPartyDeviceToSearchDevice({ + device: { + vendor: 'ledger', + model: 'nanoX', + firmwareVersion: '', + deviceId: '', + connectId: '', + label: 'Leo', + connectionType: 'ble', + } as never, + defaultDeviceName: 'Ledger', + }), + ).toThrow('Third-party BLE connectId is required'); + }); +}); diff --git a/packages/kit-bg/src/services/ServiceHardware/ServiceHardware.ts b/packages/kit-bg/src/services/ServiceHardware/ServiceHardware.ts index 734cb8941c73..c695eb46fdfe 100644 --- a/packages/kit-bg/src/services/ServiceHardware/ServiceHardware.ts +++ b/packages/kit-bg/src/services/ServiceHardware/ServiceHardware.ts @@ -1,5 +1,4 @@ import { EDeviceType, EFirmwareType } from '@onekeyfe/hd-shared'; -import { UI_RESPONSE } from '@onekeyfe/hwk-adapter-core'; import { Semaphore } from 'async-mutex'; import { uniq } from 'lodash'; import semver from 'semver'; @@ -74,9 +73,13 @@ import { DeviceSettingsManager } from './DeviceSettingsManager'; import { HardwareConnectionManager } from './HardwareConnectionManager'; import { HardwareVerifyManager } from './HardwareVerifyManager'; import serviceHardwareUtils from './serviceHardwareUtils'; +import { mapThirdPartyDeviceToSearchDevice } from './thirdPartyDeviceMapping'; import type { IThirdPartyVendor } from './adapters/thirdPartyHardwareAdapterRegistry'; -import type { DeviceInfo, IThirdPartyHardwareAdapter } from './adapters/types'; +import type { + IAdapterUiResponse, + IThirdPartyHardwareAdapter, +} from './adapters/types'; import type { IBaseDeviceProcessingParams, IChangePinParams, @@ -804,43 +807,18 @@ class ServiceHardware extends ServiceBase { success: true, count: devices.length, }); - - const isUuidLike = (s?: string) => - s ? /^[0-9a-f]{8}-[0-9a-f]{4}-/.test(s) : false; + const payload = devices.map((d) => + mapThirdPartyDeviceToSearchDevice({ + device: d, + defaultDeviceName: vendorProfile.defaultDeviceName, + canMatchDeviceByConnectId: (connectId) => + vendorProfile.canMatchDeviceByConnectId(connectId), + }), + ); return { success: true as const, - payload: devices.map((d) => { - const isBle = d.connectionType === 'ble'; - - // BLE: connectId is the stable 4-digit HEX (e.g. "A58F"), name is "Ledger" - // USB: connectId is null (ephemeral), name from label or default - let name: string; - let connectId: string | null = null; - - if (isBle) { - connectId = d.connectId || null; - name = vendorProfile.defaultDeviceName || 'Ledger'; - } else { - const rawName = - d.label || (d as DeviceInfo & { name?: string }).name || ''; - name = isUuidLike(rawName) - ? vendorProfile.defaultDeviceName - : rawName || vendorProfile.defaultDeviceName; - } - - return { - connectId, - deviceId: null, - name, - // Third-party vendors (Ledger) don't map to OneKey IDeviceType; - // use 'unknown' and carry vendor identity separately via - // IConnectYourDeviceItem.vendor at the UI layer. - deviceType: 'unknown', - uuid: '', - commType: 'bridge', - } as SearchDevice; - }), + payload, }; } catch (error) { // Preserve HWK's structured error (code + message) so downstream @@ -848,11 +826,17 @@ class ServiceHardware extends ServiceBase { const err = error as { code?: number | string; message?: string }; const rawCode = typeof err?.code === 'number' ? err.code : Number(err?.code); + const permissionDeniedReason = (err as { reason?: string }).reason; return { success: false as const, payload: { code: Number.isFinite(rawCode) ? rawCode : -1, error: err?.message ?? String(error), + params: permissionDeniedReason + ? { + permissionDeniedReason, + } + : undefined, }, }; } @@ -1736,18 +1720,12 @@ class ServiceHardware extends ServiceBase { @backgroundMethod() async thirdPartyHardwareUiResponse(params: { vendor: EHardwareVendor; - type: 'confirm' | 'cancel'; + response: IAdapterUiResponse; }) { await this.ensureAdaptersInitialized(params.vendor); const adapter = this.getThirdPartyAdapter(params.vendor); if (!adapter) return; - - // Only REQUEST_DEVICE_CONNECT flows through this path today. Extend the - // mapping when PIN / passphrase / select-device dialogs are wired up. - adapter.uiResponse({ - type: UI_RESPONSE.RECEIVE_DEVICE_CONNECT, - payload: { confirmed: params.type === 'confirm' }, - }); + adapter.uiResponse(params.response); } @backgroundMethod() @@ -2189,10 +2167,7 @@ class ServiceHardware extends ServiceBase { features, }); - // Third-party devices (Ledger) manage their own transport. - // Their connectId is already the correct identifier for the current - // connection type (e.g. BLE 4-digit HEX), so skip the OneKey - // USB↔BLE compatibility layer entirely. + // Third-party connectId already matches its active transport. if (device?.vendor) { const vp = getVendorProfile(device.vendor); if (vp.isThirdParty) { @@ -2215,12 +2190,10 @@ class ServiceHardware extends ServiceBase { return device?.connectId || connectId; } - // Determine the transport type to use const result = await this.connectionManager.shouldSwitchTransportType({ connectId: device?.connectId || connectId, hardwareCallContext, }); - console.log('πŸ” shouldSwitchTransportType result:', result); const targetTransportType = result.targetType; const forceTransportType = (await hardwareForceTransportAtom.get()) .forceTransportType; diff --git a/packages/kit-bg/src/services/ServiceHardware/adapters/LedgerAdapter.ts b/packages/kit-bg/src/services/ServiceHardware/adapters/LedgerAdapter.ts index 3260a4b25772..97b157de8642 100644 --- a/packages/kit-bg/src/services/ServiceHardware/adapters/LedgerAdapter.ts +++ b/packages/kit-bg/src/services/ServiceHardware/adapters/LedgerAdapter.ts @@ -39,6 +39,12 @@ export class LedgerAdapter event, ); switch (event.type) { + case EConnectorInteraction.Searching: + void thirdPartyHardwareUiStateAtom.set({ + action: EThirdPartyHardwareUiAction.searching, + vendor: EHardwareVendor.ledger, + }); + break; case EConnectorInteraction.ConfirmOpenApp: void thirdPartyHardwareUiStateAtom.set({ action: EThirdPartyHardwareUiAction.openApp, @@ -46,6 +52,7 @@ export class LedgerAdapter }); break; case EConnectorInteraction.UnlockDevice: + // Toast only; DMK handles the unlock polling and completion event. void thirdPartyHardwareUiStateAtom.set({ action: EThirdPartyHardwareUiAction.unlockDevice, vendor: EHardwareVendor.ledger, @@ -69,14 +76,17 @@ export class LedgerAdapter } }); - this.hw.on(UI_REQUEST.REQUEST_DEVICE_CONNECT, () => { + this.hw.on(UI_REQUEST.REQUEST_DEVICE_CONNECT, (event) => { + const { vendor, reason } = event.payload; defaultLogger.hardware.sdkLog.log( - '[3rdPartyHW][Ledger] REQUEST_DEVICE_CONNECT', + `[3rdPartyHW][Ledger] REQUEST_DEVICE_CONNECT vendor=${vendor} reason=${reason}`, ); this.emitUiEvent({ kind: 'request', - type: EThirdPartyHardwareUiAction.requestUnlock, + type: EThirdPartyHardwareUiAction.requestDeviceNotFound, payload: { + vendor, + reason, message: appLocale.intl.formatMessage({ id: ETranslations.hardware_third_party_connect_ledger_message, }), @@ -84,13 +94,36 @@ export class LedgerAdapter }); }); - // BaseAdapter's onUiEvent -> set atom for request events + this.hw.on(UI_REQUEST.REQUEST_BTC_HIGH_INDEX_CONFIRM, (event) => { + const { vendor, path, accountIndex } = event.payload; + defaultLogger.hardware.sdkLog.log( + `[3rdPartyHW][Ledger] REQUEST_BTC_HIGH_INDEX_CONFIRM path=${path} index=${accountIndex}`, + ); + this.emitUiEvent({ + kind: 'request', + type: EThirdPartyHardwareUiAction.requestBtcHighIndexConfirm, + payload: { + vendor, + path, + accountIndex, + }, + }); + }); + + // SDK signals an externally-cancelled wait β†’ drop any open dialog/toast. + this.hw.on(UI_REQUEST.CLOSE_UI_WINDOW, () => { + defaultLogger.hardware.sdkLog.log('[3rdPartyHW][Ledger] CLOSE_UI_WINDOW'); + void thirdPartyHardwareUiStateAtom.set(undefined); + }); + + // Request events trust the adapter vendor, not SDK payload hints. this.onUiEvent((event) => { if (event.kind === 'request') { + const { reason, message, path, accountIndex } = event.payload ?? {}; void thirdPartyHardwareUiStateAtom.set({ action: event.type as EThirdPartyHardwareUiAction, vendor: EHardwareVendor.ledger, - payload: event.payload, + payload: { reason, message, path, accountIndex }, }); } }); @@ -111,10 +144,6 @@ export class LedgerAdapter defaultLogger.hardware.sdkLog.log( `[3rdPartyHW][Ledger] connectDevice connectId=${connectId}`, ); - void thirdPartyHardwareUiStateAtom.set({ - action: EThirdPartyHardwareUiAction.searching, - vendor: EHardwareVendor.ledger, - }); try { const result = await this.hw.connectDevice(connectId); defaultLogger.hardware.sdkLog.log( @@ -147,7 +176,6 @@ export class LedgerAdapter (error as Error)?.message ?? String(error) }`, ); - // Ensure atom is cleared on unexpected errors void thirdPartyHardwareUiStateAtom.set(undefined); throw error; } diff --git a/packages/kit-bg/src/services/ServiceHardware/adapters/thirdPartyHardwareAdapterRegistry.ts b/packages/kit-bg/src/services/ServiceHardware/adapters/thirdPartyHardwareAdapterRegistry.ts index 674eaf3e3c20..0b9ba3bf77bf 100644 --- a/packages/kit-bg/src/services/ServiceHardware/adapters/thirdPartyHardwareAdapterRegistry.ts +++ b/packages/kit-bg/src/services/ServiceHardware/adapters/thirdPartyHardwareAdapterRegistry.ts @@ -1,4 +1,12 @@ -import { checkBLEPermissions } from '@onekeyhq/shared/src/hardware/blePermissions'; +import { EThirdPartyDevicePermissionDeniedReason } from '@onekeyhq/shared/src/errors/errors/thirdPartyHardwareErrors'; +import { + EAppEventBusNames, + appEventBus, +} from '@onekeyhq/shared/src/eventBus/appEventBus'; +import { + checkBLEPermissions, + checkBLEState, +} from '@onekeyhq/shared/src/hardware/blePermissions'; import { defaultLogger } from '@onekeyhq/shared/src/logger/logger'; import platformEnv from '@onekeyhq/shared/src/platformEnv'; import { EHardwareVendor } from '@onekeyhq/shared/types/device'; @@ -55,15 +63,40 @@ export const thirdPartyHardwareAdapterRegistry = { defaultLogger.hardware.sdkLog.log( '[3rdPartyHW][Registry] REQUEST_DEVICE_PERMISSION', ); - const granted = platformEnv.isNative - ? !!(await checkBLEPermissions()) - : true; + let granted = true; + let reason: EThirdPartyDevicePermissionDeniedReason | undefined; + if (platformEnv.isNative) { + const isPermissionGranted = !!(await checkBLEPermissions()); + if (!isPermissionGranted) { + granted = false; + reason = EThirdPartyDevicePermissionDeniedReason.permissionDenied; + } else { + const isBluetoothOn = await checkBLEState(); + if (!isBluetoothOn) { + granted = false; + reason = EThirdPartyDevicePermissionDeniedReason.bluetoothTurnedOff; + } + defaultLogger.hardware.sdkLog.log( + `[3rdPartyHW][Registry] BLE state enabled=${String(isBluetoothOn)}`, + ); + } + } defaultLogger.hardware.sdkLog.log( `[3rdPartyHW][Registry] BLE permission granted=${String(granted)}`, ); + if (!granted && reason) { + appEventBus.emit( + EAppEventBusNames.ShowThirdPartyHardwarePermissionDialog, + { + vendor: EHardwareVendor.ledger, + reason, + }, + ); + } + const permissionPayload = reason ? { granted, reason } : { granted }; hw.uiResponse({ type: UI_RESPONSE.RECEIVE_DEVICE_PERMISSION, - payload: { granted }, + payload: permissionPayload, }); }); defaultLogger.hardware.sdkLog.log( diff --git a/packages/kit-bg/src/services/ServiceHardware/adapters/types.ts b/packages/kit-bg/src/services/ServiceHardware/adapters/types.ts index d9a60f96d036..3d01fae5f1ca 100644 --- a/packages/kit-bg/src/services/ServiceHardware/adapters/types.ts +++ b/packages/kit-bg/src/services/ServiceHardware/adapters/types.ts @@ -15,13 +15,24 @@ export type { DeviceInfo, IHardwareWallet, Response, IConnector }; // UI Event types (OneKey-specific adapter UI layer) // ===================================================================== -export type IAdapterUiRequestType = EThirdPartyHardwareUiAction.requestUnlock; +export type IAdapterUiRequestType = + | EThirdPartyHardwareUiAction.requestDeviceNotFound + | EThirdPartyHardwareUiAction.requestBtcHighIndexConfirm; export type IAdapterUiRequest = { kind: 'request'; type: IAdapterUiRequestType; payload?: { + /** Vendor that emitted the request, e.g. 'ledger'. */ + vendor?: string; + /** Why the SDK is asking for a reconnect (e.g. 'device-not-found'). */ + reason?: string; + /** Best-effort English fallback when vendor+reason isn't recognized. */ message?: string; + /** BIP-44 path the SDK is asking about (BTC high-index confirm). */ + path?: string; + /** Account index parsed from the path (BTC high-index confirm). */ + accountIndex?: number; }; }; diff --git a/packages/kit-bg/src/services/ServiceHardware/thirdPartyDeviceMapping.ts b/packages/kit-bg/src/services/ServiceHardware/thirdPartyDeviceMapping.ts new file mode 100644 index 000000000000..ff49a6b6bce2 --- /dev/null +++ b/packages/kit-bg/src/services/ServiceHardware/thirdPartyDeviceMapping.ts @@ -0,0 +1,53 @@ +import { OneKeyLocalError } from '@onekeyhq/shared/src/errors'; + +import type { DeviceInfo } from './adapters/types'; +import type { SearchDevice } from '@onekeyfe/hd-core'; + +export function mapThirdPartyDeviceToSearchDevice({ + device, + defaultDeviceName, + canMatchDeviceByConnectId = (connectId) => /^[0-9A-Fa-f]{4}$/.test(connectId), +}: { + device: DeviceInfo; + defaultDeviceName?: string; + canMatchDeviceByConnectId?: (connectId: string) => boolean; +}): SearchDevice { + const isUuidLike = (s?: string) => + s ? /^[0-9a-f]{8}-[0-9a-f]{4}-/.test(s) : false; + const rawName = + device.label || (device as DeviceInfo & { name?: string }).name || ''; + const stableConnectId = + device.connectId && canMatchDeviceByConnectId(device.connectId) + ? device.connectId + : null; + + let connectId: string | null; + switch (device.connectionType) { + case 'ble': + if (!stableConnectId) { + throw new OneKeyLocalError('Third-party BLE connectId is required'); + } + connectId = stableConnectId; + break; + case 'usb': + connectId = null; + break; + default: + // Transport unknown β€” fall back to connectId shape heuristic. + connectId = stableConnectId; + } + + const displayName = + rawName && !isUuidLike(rawName) + ? rawName + : device.model || defaultDeviceName || ''; + + return { + connectId, + deviceId: null, + name: displayName, + deviceType: 'unknown', + uuid: '', + commType: 'bridge', + } as SearchDevice; +} diff --git a/packages/kit-bg/src/services/ServiceHardwareUI/ServiceHardwareUI.ts b/packages/kit-bg/src/services/ServiceHardwareUI/ServiceHardwareUI.ts index ae68bab2ad9b..b22cac230613 100644 --- a/packages/kit-bg/src/services/ServiceHardwareUI/ServiceHardwareUI.ts +++ b/packages/kit-bg/src/services/ServiceHardwareUI/ServiceHardwareUI.ts @@ -31,7 +31,6 @@ import type { import localDb from '../../dbs/local/localDb'; import { EHardwareUiStateAction, - EThirdPartyHardwareUiAction, hardwareUiStateAtom, thirdPartyHardwareUiStateAtom, } from '../../states/jotai/atoms'; @@ -514,16 +513,7 @@ class ServiceHardwareUI extends ServiceBase { connectId, }); } - if ( - isThirdPartyVendor && - !hideCheckingDeviceLoading && - device?.vendor - ) { - void thirdPartyHardwareUiStateAtom.set({ - action: EThirdPartyHardwareUiAction.searching, - vendor: device.vendor, - }); - } + // Third-party searching UI is driven by SDK ui-events. // await waitForCancelDone(); diff --git a/packages/kit-bg/src/states/jotai/atoms/hardware.ts b/packages/kit-bg/src/states/jotai/atoms/hardware.ts index ff1cfbe84e88..6eef7dc81277 100644 --- a/packages/kit-bg/src/states/jotai/atoms/hardware.ts +++ b/packages/kit-bg/src/states/jotai/atoms/hardware.ts @@ -144,15 +144,19 @@ export const { // third-party hardware ui state ----------------------------------- export enum EThirdPartyHardwareUiAction { - // Blocking requests β€” UI waits for user response - requestUnlock = 'request-ledger-unlock', - // Non-blocking notifications β€” UI shows status + // Blocking requests β€” UI waits for user response. + // SDK found no device; ask the user to make it available and retry. + requestDeviceNotFound = 'request-ledger-device-not-found', + // Ledger BTC requires explicit user approval before using index >= 100. + requestBtcHighIndexConfirm = 'request-ledger-btc-high-index-confirm', + // Non-blocking notifications β€” UI shows status. openApp = 'ui-event-ledger-open-app', confirmOnDevice = 'ui-event-ledger-confirm-on-device', searching = 'ui-event-ledger-searching', connecting = 'ui-event-ledger-connecting', processing = 'ui-event-ledger-processing', done = 'ui-event-ledger-done', + // Toast only; DMK keeps polling until the device is unlocked. unlockDevice = 'ui-event-ledger-unlock-device', error = 'ui-event-ledger-error', } @@ -183,6 +187,12 @@ export type IThirdPartyHardwareUiState = { payload?: { message?: string; chain?: string; + /** SDK request reason used for UI copy. */ + reason?: string; + /** BIP-44 path that triggered the request (e.g. requestBtcHighIndexConfirm). */ + path?: string; + /** Account index parsed from the path (e.g. requestBtcHighIndexConfirm). */ + accountIndex?: number; }; }; diff --git a/packages/kit-bg/src/vaults/base/ThirdPartyUnsupportedKeyringStub.ts b/packages/kit-bg/src/vaults/base/ThirdPartyUnsupportedKeyringStub.ts new file mode 100644 index 000000000000..f0a2f3684561 --- /dev/null +++ b/packages/kit-bg/src/vaults/base/ThirdPartyUnsupportedKeyringStub.ts @@ -0,0 +1,83 @@ +import type { ISignedMessagePro, ISignedTxPro } from '@onekeyhq/core/src/types'; +import { ThirdPartyChainNotSupported } from '@onekeyhq/shared/src/errors/errors/thirdPartyHardwareErrors'; + +import { EVaultKeyringTypes } from '../types'; + +import { KeyringBase } from './KeyringBase'; + +import type { VaultBase } from './VaultBase'; +import type { IDBAccount } from '../../dbs/local/types'; +import type { + IBuildHwAllNetworkPrepareAccountsParams, + IBuildPrepareAccountsPrefixedPathParams, + IExportAccountSecretKeysParams, + IExportAccountSecretKeysResult, + IPrepareAccountsParams, + ISignMessageParams, + ISignTransactionParams, +} from '../types'; +import type { AllNetworkAddressParams } from '@onekeyfe/hd-core'; + +// Defers unsupported-chain errors until a user-invoked keyring method runs. +export class ThirdPartyUnsupportedKeyringStub extends KeyringBase { + constructor( + vault: VaultBase, + private readonly meta: { vendor: string; chain?: string }, + ) { + super(vault); + } + + override coreApi = undefined; + + override keyringType = EVaultKeyringTypes.hardware; + + private throwUnsupported(): never { + throw new ThirdPartyChainNotSupported({ + vendor: this.meta.vendor, + chain: this.meta.chain, + payload: {}, + }); + } + + override signTransaction( + _params: ISignTransactionParams, + ): Promise { + this.throwUnsupported(); + } + + override signMessage( + _params: ISignMessageParams, + ): Promise { + this.throwUnsupported(); + } + + override prepareAccounts( + _params: IPrepareAccountsParams, + ): Promise { + this.throwUnsupported(); + } + + override batchGetAddresses( + _params: IPrepareAccountsParams, + ): Promise<{ address: string; path: string }[]> { + this.throwUnsupported(); + } + + override exportAccountSecretKeys( + _params: IExportAccountSecretKeysParams, + ): Promise { + this.throwUnsupported(); + } + + override buildHwAllNetworkPrepareAccountsParams( + _params: IBuildHwAllNetworkPrepareAccountsParams, + ): Promise { + this.throwUnsupported(); + } + + override buildPrepareAccountsPrefixedPath( + _params: IBuildPrepareAccountsPrefixedPathParams, + ): string { + this.throwUnsupported(); + } +} diff --git a/packages/kit-bg/src/vaults/factory.ts b/packages/kit-bg/src/vaults/factory.ts index ab80b8e0be72..5e9c92420117 100644 --- a/packages/kit-bg/src/vaults/factory.ts +++ b/packages/kit-bg/src/vaults/factory.ts @@ -46,12 +46,12 @@ import { OneKeyLocalError, VaultKeyringNotDefinedError, } from '@onekeyhq/shared/src/errors'; -import { ThirdPartyChainNotSupported } from '@onekeyhq/shared/src/errors/errors/thirdPartyHardwareErrors'; import type { IOneKeyError } from '@onekeyhq/shared/src/errors/types/errorTypes'; import { ensureRunOnBackground } from '@onekeyhq/shared/src/utils/assertUtils'; import networkUtils from '@onekeyhq/shared/src/utils/networkUtils'; import { EHardwareVendor } from '@onekeyhq/shared/types/device'; +import { ThirdPartyUnsupportedKeyringStub } from './base/ThirdPartyUnsupportedKeyringStub'; import { VaultFactory } from './base/VaultFactory'; import type { KeyringBase, KeyringBaseMock } from './base/KeyringBase'; @@ -104,23 +104,24 @@ export async function createKeyringInstance(vault: VaultBase) { switch (vendor) { case EHardwareVendor.ledger: if (!keyringMap.hwLedger) { - throw new ThirdPartyChainNotSupported({ + // Defer unsupported-chain errors until a keyring method is invoked. + keyring = new ThirdPartyUnsupportedKeyringStub(vault, { vendor: 'Ledger', chain: await resolveChainName(), - payload: {}, }); + } else { + keyring = new keyringMap.hwLedger(vault); } - keyring = new keyringMap.hwLedger(vault); break; case EHardwareVendor.trezor: if (!keyringMap.hwTrezor) { - throw new ThirdPartyChainNotSupported({ + keyring = new ThirdPartyUnsupportedKeyringStub(vault, { vendor: 'Trezor', chain: await resolveChainName(), - payload: {}, }); + } else { + keyring = new keyringMap.hwTrezor(vault); } - keyring = new keyringMap.hwTrezor(vault); break; case EHardwareVendor.onekey: case undefined: diff --git a/packages/kit-bg/src/vaults/impls/btc/KeyringHardwareLedger.ts b/packages/kit-bg/src/vaults/impls/btc/KeyringHardwareLedger.ts index cc14cdad08cb..fc4f380e79f8 100644 --- a/packages/kit-bg/src/vaults/impls/btc/KeyringHardwareLedger.ts +++ b/packages/kit-bg/src/vaults/impls/btc/KeyringHardwareLedger.ts @@ -19,6 +19,7 @@ import { import { slicePathTemplate } from '@onekeyhq/core/src/utils'; import { NotImplemented, OneKeyLocalError } from '@onekeyhq/shared/src/errors'; import { convertThirdPartyDeviceError } from '@onekeyhq/shared/src/errors/utils/thirdPartyDeviceErrorUtils'; +import { ETranslations } from '@onekeyhq/shared/src/locale'; import accountUtils from '@onekeyhq/shared/src/utils/accountUtils'; import { checkIsDefined } from '@onekeyhq/shared/src/utils/assertUtils'; import { @@ -518,9 +519,9 @@ export class KeyringHardwareLedger extends KeyringHardwareBtcBase { params.messages.map( async (payload: { message: string; type?: string }) => { if (payload.type === 'bip322-simple') { - throw new NotImplemented( - 'Ledger does not support BIP-322 message signing', - ); + throw new NotImplemented({ + key: ETranslations.hardware_third_party_method_not_supported, + }); } const messageHex = Buffer.from(payload.message).toString('hex'); diff --git a/packages/kit/src/components/AccountSelector/hooks/useAccountSelectorCreateAddress.tsx b/packages/kit/src/components/AccountSelector/hooks/useAccountSelectorCreateAddress.tsx index 3603f556c4c3..b97f03feadde 100644 --- a/packages/kit/src/components/AccountSelector/hooks/useAccountSelectorCreateAddress.tsx +++ b/packages/kit/src/components/AccountSelector/hooks/useAccountSelectorCreateAddress.tsx @@ -150,6 +150,17 @@ export function useAccountSelectorCreateAddress() { throw new OneKeyErrorAirGapAccountNotFound(); } + if ( + result?.failedAccounts?.length && + !accountUtils.isQrWallet({ walletId: account.walletId }) + ) { + for (const failedAccount of result.failedAccounts) { + Toast.error({ + title: failedAccount.error.message || 'Unknown error', + }); + } + } + // If indexedAccountId was empty, get the first indexed account from wallet let resultIndexedAccountId = account?.indexedAccountId; if (!resultIndexedAccountId && account?.walletId) { diff --git a/packages/kit/src/provider/Container/ThirdPartyHardwareUiStateContainer/index.tsx b/packages/kit/src/provider/Container/ThirdPartyHardwareUiStateContainer/index.tsx index c7619de9f3ea..feac575a47da 100644 --- a/packages/kit/src/provider/Container/ThirdPartyHardwareUiStateContainer/index.tsx +++ b/packages/kit/src/provider/Container/ThirdPartyHardwareUiStateContainer/index.tsx @@ -1,8 +1,10 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { UI_RESPONSE } from '@onekeyfe/hwk-adapter-core'; import { useIntl } from 'react-intl'; import { + Dialog, DialogContainer, Icon, IconButton, @@ -10,13 +12,13 @@ import { Portal, SizableText, Stack, - Toast, XStack, YStack, } from '@onekeyhq/components'; import type { IDialogInstance, ILottieViewProps } from '@onekeyhq/components'; import type { IShowToasterInstance } from '@onekeyhq/components/src/actions/Toast/ShowCustom'; import { ShowCustom } from '@onekeyhq/components/src/actions/Toast/ShowCustom'; +import type { IAdapterUiResponse } from '@onekeyhq/kit-bg/src/services/ServiceHardware/adapters/types'; import type { IThirdPartyHardwareUiState } from '@onekeyhq/kit-bg/src/states/jotai/atoms'; import { EThirdPartyHardwareUiAction, @@ -24,11 +26,20 @@ import { thirdPartyHardwareUiStateAtom, useThirdPartyHardwareUiStateAtom, } from '@onekeyhq/kit-bg/src/states/jotai/atoms'; +import { EThirdPartyDevicePermissionDeniedReason } from '@onekeyhq/shared/src/errors/errors/thirdPartyHardwareErrors'; +import { + EAppEventBusNames, + appEventBus, +} from '@onekeyhq/shared/src/eventBus/appEventBus'; import { getVendorProfile } from '@onekeyhq/shared/src/hardware/vendorProfile'; import { ETranslations } from '@onekeyhq/shared/src/locale'; import { EHardwareVendor } from '@onekeyhq/shared/types/device'; import backgroundApiProxy from '../../../background/instance/backgroundApiProxy'; +import { + OpenBleSettingsDialog, + RequireBlePermissionDialog, +} from '../../../components/Hardware/HardwareDialog'; import { useThemeVariant } from '../../../hooks/useThemeVariant'; import type { IntlShape } from 'react-intl'; @@ -37,9 +48,13 @@ const AUTO_CLOSED_FLAG = 'autoClosed'; const SHOW_CLOSE_BUTTON_DELAY = 8000; const TOAST_VIEWPORT_NAME = 'THIRD_PARTY_HW_TOAST'; -// --------------------------------------------------------------------------- -// Toast content for "confirm on device" β€” no Lottie, simple icon + text -// --------------------------------------------------------------------------- +function OpenBleSettingsDialogRender({ ref }: { ref: any }) { + return ; +} + +function RequireBlePermissionDialogRender({ ref }: { ref: any }) { + return ; +} function getDeviceLabel(vendor: string | undefined): string { const fallback = 'Device'; @@ -97,21 +112,24 @@ function getLedgerActionAnimation( function DeviceActionToast({ action, vendor, + onCloseByUser, }: { action?: string; vendor: string; + onCloseByUser: () => void; }) { const intl = useIntl(); const [showCloseButton, setShowCloseButton] = useState(false); const themeVariant = useThemeVariant(); useEffect(() => { + setShowCloseButton(false); const timer = setTimeout( () => setShowCloseButton(true), SHOW_CLOSE_BUTTON_DELAY, ); return () => clearTimeout(timer); - }, []); + }, [action, vendor]); const label = getToastLabel(action, vendor, intl); @@ -151,9 +169,11 @@ function DeviceActionToast({ {showCloseButton ? ( - - - + ) : null} @@ -161,11 +181,10 @@ function DeviceActionToast({ ); } -// --------------------------------------------------------------------------- -// Dialog content config -// --------------------------------------------------------------------------- - -function getDialogContent(state: IThirdPartyHardwareUiState): { +function getDialogContent( + state: IThirdPartyHardwareUiState, + intl: IntlShape, +): { title: string; message: string; showFooter: boolean; @@ -174,7 +193,7 @@ function getDialogContent(state: IThirdPartyHardwareUiState): { const device = getDeviceLabel(vendor); switch (action) { - case EThirdPartyHardwareUiAction.requestUnlock: + case EThirdPartyHardwareUiAction.requestDeviceNotFound: // TODO: replace with ETranslations + ICU {device} placeholder when available return { title: `Connect ${device}`, @@ -183,57 +202,154 @@ function getDialogContent(state: IThirdPartyHardwareUiState): { `Please connect and unlock your ${device} device, then press Confirm.`, showFooter: true, }; - // open-app, searching, unlock-device, confirm-on-device β†’ handled by Toast - // error β†’ let withHardwareProcessing handle it, no separate dialog + case EThirdPartyHardwareUiAction.requestBtcHighIndexConfirm: + return { + title: intl.formatMessage({ + id: ETranslations.hardware_third_party_btc_high_index_confirm_title, + }), + message: intl.formatMessage( + { + id: ETranslations.hardware_third_party_btc_high_index_confirm_desc, + }, + { + path: payload?.path ?? '', + accountIndex: payload?.accountIndex ?? '', + }, + ), + showFooter: true, + }; default: return { title: '', message: '', showFooter: false }; } } -// Actions that need confirm/cancel footer (blocking requests) -const REQUEST_ACTIONS = new Set([EThirdPartyHardwareUiAction.requestUnlock]); - -// --------------------------------------------------------------------------- -// Container -// --------------------------------------------------------------------------- +const REQUEST_ACTIONS = new Set([ + EThirdPartyHardwareUiAction.requestDeviceNotFound, + EThirdPartyHardwareUiAction.requestBtcHighIndexConfirm, +]); function ThirdPartyHardwareUiStateContainerCmp() { + const intl = useIntl(); const [uiState] = useThirdPartyHardwareUiStateAtom(); const uiStateRef = useRef(uiState); uiStateRef.current = uiState; const dialogInstanceRef = useRef(null); + const permissionDialogInstanceRef = useRef(null); const toastInstanceRef = useRef(null); const isToastAction = isThirdPartyToastAction(uiState?.action); const isDialogAction = !!uiState && !isToastAction; - const handleClose = useCallback(async (params?: { flag?: string }) => { - if (params?.flag !== AUTO_CLOSED_FLAG) { - const vendor = uiStateRef.current?.vendor; - if (vendor) { - await backgroundApiProxy.serviceHardware.thirdPartyHardwareCancel({ - vendor, - }); - } + // Close callbacks fire for programmatic transitions too, so only explicit + // user buttons are allowed to cancel SDK work. + const handleToastClose = useCallback(async () => undefined, []); + + const handleDialogClose = useCallback(async (params?: { flag?: string }) => { + if (params?.flag === AUTO_CLOSED_FLAG) { + await thirdPartyHardwareUiStateAtom.set(undefined); } + }, []); + + const handlePermissionDialogClose = useCallback(async () => { await thirdPartyHardwareUiStateAtom.set(undefined); }, []); + useEffect(() => { + const callback = async ({ + vendor, + reason, + }: { + vendor: EHardwareVendor; + reason: EThirdPartyDevicePermissionDeniedReason; + }) => { + if (vendor !== EHardwareVendor.ledger) { + return; + } + await permissionDialogInstanceRef.current?.close(); + permissionDialogInstanceRef.current = Dialog.show({ + dialogContainer: + reason === EThirdPartyDevicePermissionDeniedReason.bluetoothTurnedOff + ? OpenBleSettingsDialogRender + : RequireBlePermissionDialogRender, + onClose: handlePermissionDialogClose, + }); + }; + appEventBus.on( + EAppEventBusNames.ShowThirdPartyHardwarePermissionDialog, + callback, + ); + return () => { + appEventBus.off( + EAppEventBusNames.ShowThirdPartyHardwarePermissionDialog, + callback, + ); + }; + }, [handlePermissionDialogClose]); + + const buildUiResponse = useCallback( + ( + action: EThirdPartyHardwareUiAction | undefined, + confirmed: boolean, + ): IAdapterUiResponse | null => { + switch (action) { + case EThirdPartyHardwareUiAction.requestDeviceNotFound: + return { + type: UI_RESPONSE.RECEIVE_DEVICE_CONNECT, + payload: { confirmed }, + }; + case EThirdPartyHardwareUiAction.requestBtcHighIndexConfirm: + return { + type: UI_RESPONSE.RECEIVE_BTC_HIGH_INDEX_CONFIRM, + payload: { confirmed }, + }; + default: + return null; + } + }, + [], + ); + + const handleUserCancel = useCallback( + async (close: () => Promise) => { + const vendor = uiStateRef.current?.vendor; + const action = uiStateRef.current?.action; + if (vendor) { + const response = buildUiResponse(action, false); + if (response) { + await backgroundApiProxy.serviceHardware.thirdPartyHardwareUiResponse( + { vendor, response }, + ); + } else { + await backgroundApiProxy.serviceHardware.thirdPartyHardwareCancel({ + vendor, + }); + } + } + await thirdPartyHardwareUiStateAtom.set(undefined); + await close(); + }, + [buildUiResponse], + ); + const handleConfirm = useCallback(async () => { const vendor = uiStateRef.current?.vendor; + const action = uiStateRef.current?.action; if (vendor) { - await backgroundApiProxy.serviceHardware.thirdPartyHardwareUiResponse({ - vendor, - type: 'confirm', - }); + const response = buildUiResponse(action, true); + if (response) { + await backgroundApiProxy.serviceHardware.thirdPartyHardwareUiResponse({ + vendor, + response, + }); + } } await thirdPartyHardwareUiStateAtom.set(undefined); - }, []); + }, [buildUiResponse]); const dialogContent = useMemo(() => { if (!uiState || isToastAction) return null; - const { message } = getDialogContent(uiState); + const { message } = getDialogContent(uiState, intl); return ( @@ -241,21 +357,33 @@ function ThirdPartyHardwareUiStateContainerCmp() { ); - }, [uiState, isToastAction]); + }, [uiState, isToastAction, intl]); const dialogTitle = useMemo(() => { if (!uiState || isToastAction) return ''; - return getDialogContent(uiState).title; - }, [uiState, isToastAction]); + return getDialogContent(uiState, intl).title; + }, [uiState, isToastAction, intl]); const showFooter = useMemo(() => { if (!uiState) return false; return REQUEST_ACTIONS.has(uiState.action); }, [uiState]); + const handleToastUserClose = useCallback(async () => { + const vendor = uiStateRef.current?.vendor; + try { + if (vendor) { + await backgroundApiProxy.serviceHardware.thirdPartyHardwareCancel({ + vendor, + }); + } + } finally { + await thirdPartyHardwareUiStateAtom.set(undefined); + } + }, []); + return ( <> - {/* Toast for "confirm on device" */} - {/* Dialog for everything else */} {isDialogAction ? ( ) : null} diff --git a/packages/kit/src/states/jotai/contexts/accountSelector/actions.tsx b/packages/kit/src/states/jotai/contexts/accountSelector/actions.tsx index 06a8e632d9ea..9b3b3f46308a 100644 --- a/packages/kit/src/states/jotai/contexts/accountSelector/actions.tsx +++ b/packages/kit/src/states/jotai/contexts/accountSelector/actions.tsx @@ -1,6 +1,9 @@ import { useRef } from 'react'; -import { HardwareErrorCode as ThirdPartyHwErrorCode } from '@onekeyfe/hwk-adapter-core'; +import { + ORPHAN_ELIGIBLE_ERROR_CODES, + HardwareErrorCode as ThirdPartyHwErrorCode, +} from '@onekeyfe/hwk-adapter-core'; import { Semaphore } from 'async-mutex'; import { cloneDeep, isEmpty, isEqual, isUndefined, omitBy } from 'lodash'; @@ -115,16 +118,6 @@ export type IFinalizeWalletSetupCreateWalletResult = { }; }; -// Ledger USB has no stable device id, so a failed batch leaves an orphan -// wallet shell each retry. Restricted to codes that fire on the FIRST -// chain (i.e. before any account is persisted) β€” DeviceDisconnected and -// ChainNotSupported can fire mid-batch after partial success, hiding the -// wallet there would orphan the already-created accounts. -const LEDGER_ORPHAN_HIDE_CODES: number[] = [ - ThirdPartyHwErrorCode.UserAborted, - ThirdPartyHwErrorCode.DeviceAppStuck, -]; - class AccountSelectorActions extends ContextJotaiActionsBase { refresh = contextAtomMethod((_, set, payload: { num: number }) => { const { num } = payload; @@ -757,13 +750,7 @@ class AccountSelectorActions extends ContextJotaiActionsBase { }; return createResult; } catch (error) { - // Soft-hide the just-created Ledger wallet shell when batch aborts. - // Uses isTemp + hideImmediately β€” no DB deletion. Vendor-gated to - // Ledger so other HW lines (OneKey/BLE) keep their dedup'able shells. - // Final guard is the DB account count: even on UserAborted / - // DeviceAppStuck the user may have completed earlier chains in the - // batch β€” if any IDBAccount belongs to the wallet, the shell is no - // longer empty and must not be hidden. + // Cleanup only empty Ledger onboarding shells. const isLedgerWallet = createdResult?.wallet?.associatedDeviceInfo?.vendor === EHardwareVendor.ledger; @@ -773,7 +760,7 @@ class AccountSelectorActions extends ContextJotaiActionsBase { isLedgerWallet && isHardwareErrorByCode({ error: error as IOneKeyError | undefined, - code: LEDGER_ORPHAN_HIDE_CODES, + code: ORPHAN_ELIGIBLE_ERROR_CODES, }) ) { const walletId = createdResult.wallet?.id; @@ -785,16 +772,14 @@ class AccountSelectorActions extends ContextJotaiActionsBase { indexedAccountId, }); if (accounts.length === 0) { - await serviceAccount.setWalletTempStatus({ + await serviceAccount.removeFailedOnboardingHwWallet({ walletId, - isTemp: true, - hideImmediately: true, }); } - } catch (hideErr) { + } catch (cleanupErr) { defaultLogger.app.error.log( - `withFinalizeWalletSetupStep softHide failed: ${ - (hideErr as Error)?.message || String(hideErr) + `withFinalizeWalletSetupStep orphan cleanup failed: ${ + (cleanupErr as Error)?.message || String(cleanupErr) }`, ); } @@ -886,7 +871,7 @@ class AccountSelectorActions extends ContextJotaiActionsBase { const allAppNotInstalled = result.addedAccounts.length === 0 && failedList.every( - (f) => f.error.code === ThirdPartyHwErrorCode.AppNotOpen, + (f) => f.error.code === ThirdPartyHwErrorCode.AppNotInstalled, ); if (allAppNotInstalled) { Toast.error({ @@ -899,7 +884,7 @@ class AccountSelectorActions extends ContextJotaiActionsBase { } // Strip AppNotInstalled errors, let other errors fall through failedList = failedList.filter( - (f) => f.error.code !== ThirdPartyHwErrorCode.AppNotOpen, + (f) => f.error.code !== ThirdPartyHwErrorCode.AppNotInstalled, ); } } diff --git a/packages/kit/src/views/Onboardingv2/pages/ConnectionFlowLedger.tsx b/packages/kit/src/views/Onboardingv2/pages/ConnectionFlowLedger.tsx index 43b93a68198f..d9f40307d707 100644 --- a/packages/kit/src/views/Onboardingv2/pages/ConnectionFlowLedger.tsx +++ b/packages/kit/src/views/Onboardingv2/pages/ConnectionFlowLedger.tsx @@ -6,7 +6,6 @@ import { useIntl } from 'react-intl'; import { Button, - Dialog, EVideoResizeMode, HeightTransition, SizableText, @@ -28,7 +27,6 @@ import type { IConnectYourDeviceItem } from '@onekeyhq/shared/types/device'; import { EHardwareVendor } from '@onekeyhq/shared/types/device'; import backgroundApiProxy from '../../../background/instance/backgroundApiProxy'; -import { RequireBlePermissionDialog } from '../../../components/Hardware/HardwareDialog'; import { ListItem } from '../../../components/ListItem'; import { WalletAvatar } from '../../../components/WalletAvatar'; import useAppNavigation from '../../../hooks/useAppNavigation'; @@ -46,10 +44,6 @@ enum EConnectionStatus { listing = 'listing', } -function RequireBlePermissionDialogRender({ ref }: { ref: any }) { - return ; -} - function DeviceVideo({ themeVariant }: { themeVariant: 'light' | 'dark' }) { const videoSource = useMemo( () => @@ -127,13 +121,7 @@ export default function LedgerConnectionFlow() { pollsCompleted += 1; if (!response.success) { const error = convertDeviceError(response.payload); - // BLE permission denied β†’ show system-settings dialog instead of toast - // (shares RequireBlePermissionDialog with OneKey native BLE flow). - if (error instanceof ThirdPartyDevicePermissionDenied) { - Dialog.show({ - dialogContainer: RequireBlePermissionDialogRender, - }); - } else { + if (!(error instanceof ThirdPartyDevicePermissionDenied)) { Toast.error({ title: error.message || diff --git a/packages/shared/src/errors/errors/thirdPartyHardwareErrors.ts b/packages/shared/src/errors/errors/thirdPartyHardwareErrors.ts index d7556706f6d7..b3536cf66588 100644 --- a/packages/shared/src/errors/errors/thirdPartyHardwareErrors.ts +++ b/packages/shared/src/errors/errors/thirdPartyHardwareErrors.ts @@ -28,6 +28,11 @@ export class ThirdPartyHardwareError extends OneKeyHardwareError { appName?: string; } +export enum EThirdPartyDevicePermissionDeniedReason { + bluetoothTurnedOff = 'bluetoothTurnedOff', + permissionDenied = 'permissionDenied', +} + // Do NOT pass `defaultMessage` β€” the locale key's translation already holds // the human-readable text. @@ -53,7 +58,7 @@ export class ThirdPartyAppNotInstalled extends ThirdPartyHardwareError { this.appName = props?.appName; } - override code = ThirdPartyHwErrorCode.AppNotOpen; + override code = ThirdPartyHwErrorCode.AppNotInstalled; } /** Device is locked β€” user needs to unlock */ @@ -100,14 +105,40 @@ export class ThirdPartyUserAborted extends ThirdPartyHardwareError { /** OS-level device permission (Bluetooth / USB) denied. */ export class ThirdPartyDevicePermissionDenied extends ThirdPartyHardwareError { - constructor(props?: IOneKeyErrorHardwareProps & { vendor?: string }) { + reason?: EThirdPartyDevicePermissionDeniedReason; + + constructor( + props?: IOneKeyErrorHardwareProps & { + vendor?: string; + reason?: EThirdPartyDevicePermissionDeniedReason; + }, + ) { super( - normalizeErrorProps(props, { - defaultKey: ETranslations.onboarding_bluetooth_permission_needed, - defaultAutoToast: true, - }), + normalizeErrorProps( + props + ? { + ...props, + payload: { + ...props.payload, + params: { + ...props.payload.params, + permissionDeniedReason: props.reason, + }, + }, + } + : props, + { + defaultKey: + props?.reason === + EThirdPartyDevicePermissionDeniedReason.bluetoothTurnedOff + ? ETranslations.hardware_bluetooth_need_turned_on_error + : ETranslations.onboarding_bluetooth_permission_needed, + defaultAutoToast: true, + }, + ), ); this.vendor = props?.vendor; + this.reason = props?.reason; } override code = ThirdPartyHwErrorCode.DevicePermissionDenied; @@ -191,6 +222,21 @@ export class ThirdPartyOperationTimeout extends ThirdPartyHardwareError { override code = ThirdPartyHwErrorCode.OperationTimeout; } +/** BLE SMP pairing 30s window expired. Distinct from generic OperationTimeout + * so future PIN / passphrase / SSO confirmation timeouts don't collide. */ +export class ThirdPartyBlePairingTimeout extends ThirdPartyHardwareError { + constructor(props?: IOneKeyErrorHardwareProps) { + super( + normalizeErrorProps(props, { + defaultKey: ETranslations.hardware_bluetooth_pairing_failed, + defaultAutoToast: true, + }), + ); + } + + override code = ThirdPartyHwErrorCode.BlePairingTimeout; +} + /** Chain has no keyring impl for this vendor (e.g. Ledger doesn't support Aptos). */ export class ThirdPartyChainNotSupported extends ThirdPartyHardwareError { constructor( diff --git a/packages/shared/src/errors/utils/deviceErrorUtils.ts b/packages/shared/src/errors/utils/deviceErrorUtils.ts index 408b4c188f6d..b4b25d401179 100644 --- a/packages/shared/src/errors/utils/deviceErrorUtils.ts +++ b/packages/shared/src/errors/utils/deviceErrorUtils.ts @@ -255,6 +255,7 @@ export function convertDeviceError( return convertThirdPartyDeviceError({ code: Number(code), error: payload.error ?? message ?? '', + params: payload.params, }); } return new HardwareErrors.UnknownHardwareError({ payload }); diff --git a/packages/shared/src/errors/utils/thirdPartyDeviceErrorUtils.ts b/packages/shared/src/errors/utils/thirdPartyDeviceErrorUtils.ts index 7e76d72d1696..84a5f8ee60b9 100644 --- a/packages/shared/src/errors/utils/thirdPartyDeviceErrorUtils.ts +++ b/packages/shared/src/errors/utils/thirdPartyDeviceErrorUtils.ts @@ -21,12 +21,18 @@ interface IThirdPartyErrorContext { * ``` */ export function convertThirdPartyDeviceError( - payload: { error: string; code: number; appName?: string }, + payload: { + error: string; + code: number; + appName?: string; + params?: IOneKeyHardwareErrorPayload['params']; + }, context?: IThirdPartyErrorContext, ) { const hwPayload: IOneKeyHardwareErrorPayload = { code: payload.code, message: payload.error, + params: payload.params, }; const props = { payload: hwPayload, @@ -65,7 +71,7 @@ export function convertThirdPartyDeviceError( code: payload.code, }); - case ThirdPartyHwErrorCode.AppNotOpen: + case ThirdPartyHwErrorCode.AppNotInstalled: return new ThirdPartyErrors.ThirdPartyAppNotInstalled(props); case ThirdPartyHwErrorCode.UserRejected: @@ -75,7 +81,10 @@ export function convertThirdPartyDeviceError( return new ThirdPartyErrors.ThirdPartyUserAborted(props); case ThirdPartyHwErrorCode.DevicePermissionDenied: - return new ThirdPartyErrors.ThirdPartyDevicePermissionDenied(props); + return new ThirdPartyErrors.ThirdPartyDevicePermissionDenied({ + ...props, + reason: payload.params?.permissionDeniedReason, + }); case ThirdPartyHwErrorCode.DeviceLocked: return new ThirdPartyErrors.ThirdPartyDeviceLocked(props); @@ -98,6 +107,9 @@ export function convertThirdPartyDeviceError( case ThirdPartyHwErrorCode.OperationTimeout: return new ThirdPartyErrors.ThirdPartyOperationTimeout(props); + case ThirdPartyHwErrorCode.BlePairingTimeout: + return new ThirdPartyErrors.ThirdPartyBlePairingTimeout(props); + case ThirdPartyHwErrorCode.MethodNotSupported: return new ThirdPartyErrors.ThirdPartyMethodNotSupported(props); diff --git a/packages/shared/src/eventBus/appEventBus.ts b/packages/shared/src/eventBus/appEventBus.ts index 288b01f0edb5..552ca3657e07 100644 --- a/packages/shared/src/eventBus/appEventBus.ts +++ b/packages/shared/src/eventBus/appEventBus.ts @@ -12,6 +12,7 @@ import type { IAccountSelectorSelectedAccount } from '@onekeyhq/kit-bg/src/dbs/s import type { EHardwareUiStateAction } from '@onekeyhq/kit-bg/src/states/jotai/atoms'; import type { IAccountDeriveTypes } from '@onekeyhq/kit-bg/src/vaults/types'; import type { IAirGapUrJson } from '@onekeyhq/qr-wallet-sdk'; +import type { EThirdPartyDevicePermissionDeniedReason } from '@onekeyhq/shared/src/errors/errors/thirdPartyHardwareErrors'; import type { IOneKeyHardwareErrorPayload } from '@onekeyhq/shared/src/errors/types/errorTypes'; import type { ETranslations } from '@onekeyhq/shared/src/locale'; import type { EEnterWay } from '@onekeyhq/shared/src/logger/scopes/dex'; @@ -30,6 +31,7 @@ import type { IFetchAccountDeFiPositionsResp, IProtocolSummary, } from '../../types/defi'; +import type { EHardwareVendor } from '../../types/device'; import type { IFeeSelectorItem } from '../../types/fee'; import type { ESubscriptionType } from '../../types/hyperliquid/types'; import type { @@ -367,6 +369,10 @@ export interface IAppEventBusPayload { [EAppEventBusNames.RequestHardwareUIDialog]: { uiRequestType: EHardwareUiStateAction; }; + [EAppEventBusNames.ShowThirdPartyHardwarePermissionDialog]: { + vendor: EHardwareVendor; + reason: EThirdPartyDevicePermissionDeniedReason; + }; [EAppEventBusNames.RequestDeviceInBootloaderForWebDevice]: undefined; [EAppEventBusNames.RequestDeviceForSwitchFirmwareWebDevice]: undefined; [EAppEventBusNames.EnabledNetworksChanged]: undefined; diff --git a/packages/shared/src/eventBus/appEventBusNames.ts b/packages/shared/src/eventBus/appEventBusNames.ts index ad60ee2f9364..229e741c05ef 100644 --- a/packages/shared/src/eventBus/appEventBusNames.ts +++ b/packages/shared/src/eventBus/appEventBusNames.ts @@ -96,6 +96,7 @@ export enum EAppEventBusNames { CheckAddressBeforeSending = 'CheckAddressBeforeSending', HideTabBar = 'HideTabBar', RequestHardwareUIDialog = 'RequestHardwareUIDialog', + ShowThirdPartyHardwarePermissionDialog = 'ShowThirdPartyHardwarePermissionDialog', RequestDeviceInBootloaderForWebDevice = 'RequestDeviceInBootloaderForWebDevice', RequestDeviceForSwitchFirmwareWebDevice = 'RequestDeviceForSwitchFirmwareWebDevice', EnabledNetworksChanged = 'EnabledNetworksChanged', diff --git a/packages/shared/src/hardware/blePermissions.ts b/packages/shared/src/hardware/blePermissions.ts index 98006cc9fac2..05d9d2a5a2e7 100644 --- a/packages/shared/src/hardware/blePermissions.ts +++ b/packages/shared/src/hardware/blePermissions.ts @@ -10,6 +10,8 @@ import { } from '../modules3rdParty/react-native-permissions'; import platformEnv from '../platformEnv'; +import bleManagerInstance from './bleManager'; + export async function openBLESettings() { if (platformEnv.isNativeIOS) { await Linking.openURL('App-Prefs:Bluetooth'); @@ -22,6 +24,14 @@ export async function openBLEPermissionsSettings() { await openSettings(); } +export async function checkBLEState() { + if (!platformEnv.isNative) { + return true; + } + const state = await bleManagerInstance.checkState(); + return state === 'on'; +} + export async function checkBLEPermissions() { if (platformEnv.isNativeIOS) { // If you only call the `request` function to check if Bluetooth permission is enabled, diff --git a/yarn.lock b/yarn.lock index 7a492a8ad82d..2356d233d2d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8867,18 +8867,18 @@ __metadata: languageName: node linkType: hard -"@onekeyfe/hwk-adapter-core@npm:1.1.26-alpha.11": - version: 1.1.26-alpha.11 - resolution: "@onekeyfe/hwk-adapter-core@npm:1.1.26-alpha.11" +"@onekeyfe/hwk-adapter-core@npm:1.1.26-alpha.12": + version: 1.1.26-alpha.12 + resolution: "@onekeyfe/hwk-adapter-core@npm:1.1.26-alpha.12" dependencies: "@noble/hashes": "npm:^1.7.1" - checksum: 10/d99a811199cea9ee2287fabb5473e8fb4fb88063a90cacdc8e41d12fc054fde378a09cafe0c0177f5e914a6342b102c4f5b242e89c1ade60879515db70702d8b + checksum: 10/42ac8f63aa750bba9b1d6fb93300836e6600988cd3eb8d71c14c77daab2b34d5b9b532f0e02ab0eecdbb39e46b9f87e02dcaa995c085824331f618cbff7266f0 languageName: node linkType: hard -"@onekeyfe/hwk-ledger-adapter@npm:1.1.26-alpha.11": - version: 1.1.26-alpha.11 - resolution: "@onekeyfe/hwk-ledger-adapter@npm:1.1.26-alpha.11" +"@onekeyfe/hwk-ledger-adapter@npm:1.1.26-alpha.12": + version: 1.1.26-alpha.12 + resolution: "@onekeyfe/hwk-ledger-adapter@npm:1.1.26-alpha.12" dependencies: "@ledgerhq/context-module": "npm:^1.0.0" "@ledgerhq/device-management-kit": "npm:^1.1.0" @@ -8887,39 +8887,39 @@ __metadata: "@ledgerhq/device-signer-kit-solana": "npm:^1.7.0" "@ledgerhq/hw-app-trx": "npm:^6.34.1" "@ledgerhq/hw-transport": "npm:^6.34.1" - "@onekeyfe/hwk-adapter-core": "npm:1.1.26-alpha.11" + "@onekeyfe/hwk-adapter-core": "npm:1.1.26-alpha.12" bitcoinjs-lib: "npm:@onekeyfe/bitcoinjs-lib@7.0.1" - checksum: 10/695326de044dd6be5c27164c03636046d6d2d0e0f21b7292b9cf5b8950ad59dab665e9ac8c8eb1b55c70b057512dfda942991fc360a155b31e21f3d53e5a3e61 + checksum: 10/072f30c7dcdfbd90057bdbb216fd880c417b76a52402b29ea230fc2b5772c8b01a5aee9d7562db1db85463dc94adc75eff6f037cca104035b737df9f067c48ec languageName: node linkType: hard -"@onekeyfe/hwk-ledger-connector-ble@npm:1.1.26-alpha.11": - version: 1.1.26-alpha.11 - resolution: "@onekeyfe/hwk-ledger-connector-ble@npm:1.1.26-alpha.11" +"@onekeyfe/hwk-ledger-connector-ble@npm:1.1.26-alpha.12": + version: 1.1.26-alpha.12 + resolution: "@onekeyfe/hwk-ledger-connector-ble@npm:1.1.26-alpha.12" dependencies: "@ledgerhq/device-management-kit": "npm:^1.1.0" "@ledgerhq/device-signer-kit-bitcoin": "npm:^1.0.0" "@ledgerhq/device-signer-kit-ethereum": "npm:^1.9.0" "@ledgerhq/device-signer-kit-solana": "npm:^1.7.0" "@ledgerhq/device-transport-kit-react-native-ble": "npm:^1.0.0" - "@onekeyfe/hwk-adapter-core": "npm:1.1.26-alpha.11" - "@onekeyfe/hwk-ledger-adapter": "npm:1.1.26-alpha.11" - checksum: 10/94f61e7ec55fa2f889f3e8072cf29487fb23aee9538b6122f6262612ed115db821bc30cf9519de5b8b7e699a641ae8465c9aea0cd77206fea540c87b4d5a5b8b + "@onekeyfe/hwk-adapter-core": "npm:1.1.26-alpha.12" + "@onekeyfe/hwk-ledger-adapter": "npm:1.1.26-alpha.12" + checksum: 10/26e8981569bad306cf5b72f88938529e74e24e2f3f580e0618369ec1ec2e6056e57d5a9d0eea18251273b0ae11c03d1ebdaaf38b9459fcef243364b414368d7e languageName: node linkType: hard -"@onekeyfe/hwk-ledger-connector-webhid@npm:1.1.26-alpha.11": - version: 1.1.26-alpha.11 - resolution: "@onekeyfe/hwk-ledger-connector-webhid@npm:1.1.26-alpha.11" +"@onekeyfe/hwk-ledger-connector-webhid@npm:1.1.26-alpha.12": + version: 1.1.26-alpha.12 + resolution: "@onekeyfe/hwk-ledger-connector-webhid@npm:1.1.26-alpha.12" dependencies: "@ledgerhq/device-management-kit": "npm:^1.1.0" "@ledgerhq/device-signer-kit-bitcoin": "npm:^1.0.0" "@ledgerhq/device-signer-kit-ethereum": "npm:^1.9.0" "@ledgerhq/device-signer-kit-solana": "npm:^1.7.0" "@ledgerhq/device-transport-kit-web-hid": "npm:^1.0.0" - "@onekeyfe/hwk-adapter-core": "npm:1.1.26-alpha.11" - "@onekeyfe/hwk-ledger-adapter": "npm:1.1.26-alpha.11" - checksum: 10/824bd0b637b091d09b4162a5f4def2678b8abb20ae3f400c840bdef0e12e6d366cc38d6b1a428d205b8a79776fcfbff8013cf0dda26ba1490fdf28fa20810d97 + "@onekeyfe/hwk-adapter-core": "npm:1.1.26-alpha.12" + "@onekeyfe/hwk-ledger-adapter": "npm:1.1.26-alpha.12" + checksum: 10/7d7d83fb885e4c0dce50d84809318e3d0464511eb58df402ad8d48c7826a44747836eaafe0b010693ad263aab86ef21632b1cda42b4a03b988614860c1ada787 languageName: node linkType: hard @@ -9561,10 +9561,10 @@ __metadata: "@onekeyfe/hd-transport": "npm:1.1.26-alpha.11" "@onekeyfe/hd-transport-electron": "npm:1.1.26-alpha.11" "@onekeyfe/hd-web-sdk": "npm:1.1.26-alpha.11" - "@onekeyfe/hwk-adapter-core": "npm:1.1.26-alpha.11" - "@onekeyfe/hwk-ledger-adapter": "npm:1.1.26-alpha.11" - "@onekeyfe/hwk-ledger-connector-ble": "npm:1.1.26-alpha.11" - "@onekeyfe/hwk-ledger-connector-webhid": "npm:1.1.26-alpha.11" + "@onekeyfe/hwk-adapter-core": "npm:1.1.26-alpha.12" + "@onekeyfe/hwk-ledger-adapter": "npm:1.1.26-alpha.12" + "@onekeyfe/hwk-ledger-connector-ble": "npm:1.1.26-alpha.12" + "@onekeyfe/hwk-ledger-connector-webhid": "npm:1.1.26-alpha.12" "@onekeyfe/onekey-cross-webview": "npm:2.2.68" "@open-wc/webpack-import-meta-loader": "npm:^0.4.7" "@pmmmwh/react-refresh-webpack-plugin": "npm:^0.5.11"