diff --git a/.gitmodules b/.gitmodules index 2ea236d7b..d62ab4a36 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,7 @@ [submodule "submodules/firmware"] path = submodules/firmware url = https://github.com/OneKeyHQ/firmware.git +[submodule "submodules/firmware-pro2"] + path = submodules/firmware-pro2 + url = https://github.com/OneKeyHQ/firmware-pro2.git + branch = dev diff --git a/packages/core/__tests__/DeviceCommands.test.ts b/packages/core/__tests__/DeviceCommands.test.ts index 101610a0a..e1181ba7a 100644 --- a/packages/core/__tests__/DeviceCommands.test.ts +++ b/packages/core/__tests__/DeviceCommands.test.ts @@ -16,6 +16,31 @@ const createCommands = () => { }; describe('DeviceCommands failure mapping', () => { + it('maps Protocol V2 DeviceLocked failure to a structured hardware error', async () => { + const commands = createCommands(); + + await expect( + commands._filterCommonTypes( + { + type: 'Failure', + message: { + code: 'Failure_ProcessError', + subcode: 9, + message: 'Device locked', + }, + } as any, + 'DeviceSettingsPageShow' + ) + ).rejects.toMatchObject({ + errorCode: HardwareErrorCode.DeviceLocked, + params: { + failureCode: 'Failure_ProcessError', + subcode: 9, + firmwareMessage: 'Device locked', + }, + }); + }); + it.each([ ['ButtonAck', 'Not in Ethereum signing mode'], ['PinMatrixAck', 'Not in Conflux signing mode'], diff --git a/packages/core/__tests__/RequestQueue.test.ts b/packages/core/__tests__/RequestQueue.test.ts new file mode 100644 index 000000000..01c59a0c4 --- /dev/null +++ b/packages/core/__tests__/RequestQueue.test.ts @@ -0,0 +1,34 @@ +import RequestQueue from '../src/core/RequestQueue'; + +import type { BaseMethod } from '../src/api/BaseMethod'; + +jest.mock('../src/data/config', () => ({ + getSDKVersion: jest.fn(() => '1.0.0'), + DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/', +})); + +function buildMethod(responseID: number, operationId?: string) { + return { + payload: { operationId }, + responseID, + } as unknown as BaseMethod; +} + +describe('RequestQueue operation cancellation', () => { + test('aborts only the request matching the operation id', () => { + const queue = new RequestQueue(); + const portfolio = queue.createTask(buildMethod(1, 'portfolio-1')); + const userAction = queue.createTask(buildMethod(2, 'user-action')); + + expect(queue.abortOperation('portfolio-1')).toBe(true); + expect(portfolio.abortController?.signal.aborted).toBe(true); + expect(userAction.abortController?.signal.aborted).toBe(false); + }); + + test('returns false for an unknown operation id', () => { + const queue = new RequestQueue(); + queue.createTask(buildMethod(1, 'portfolio-1')); + + expect(queue.abortOperation('missing')).toBe(false); + }); +}); diff --git a/packages/core/__tests__/connectSettings.test.ts b/packages/core/__tests__/connectSettings.test.ts new file mode 100644 index 000000000..ab27eddc9 --- /dev/null +++ b/packages/core/__tests__/connectSettings.test.ts @@ -0,0 +1,19 @@ +import { parseConnectSettings } from '../src/data-manager/connectSettings'; + +jest.mock('../src/data/config', () => ({ + getSDKVersion: jest.fn(() => '1.0.0'), + DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/', +})); + +describe('parseConnectSettings', () => { + it('keeps custom configFetcher in parsed settings', () => { + const configFetcher = jest.fn(); + + const settings = parseConnectSettings({ + fetchConfig: true, + configFetcher, + }); + + expect(settings.configFetcher).toBe(configFetcher); + }); +}); diff --git a/packages/core/__tests__/device-wallet-session-store.test.ts b/packages/core/__tests__/device-wallet-session-store.test.ts new file mode 100644 index 000000000..22dfdf210 --- /dev/null +++ b/packages/core/__tests__/device-wallet-session-store.test.ts @@ -0,0 +1,131 @@ +import ClearSessionCache from '../src/api/ClearSessionCache'; +import { + DeviceWalletSessionStore, + deviceWalletSessionStore, +} from '../src/device/DeviceWalletSessionStore'; +import { createCoreApi } from '../src/inject'; + +jest.mock('../src/data/config', () => ({ + getSDKVersion: jest.fn(() => '1.0.0'), + DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/', +})); + +describe('DeviceWalletSessionStore', () => { + test('requires passphraseState for wallet session lookup', () => { + const store = new DeviceWalletSessionStore(); + store.set('device-1', 'hidden-a', 'session-a'); + + expect(store.get('device-1', undefined)).toBeUndefined(); + expect(store.get('device-1', 'hidden-a')).toBe('session-a'); + }); + + test('isolates wallets and devices', () => { + const store = new DeviceWalletSessionStore(); + store.set('device-1', 'hidden-a', 'session-a'); + store.set('device-1', 'hidden-b', 'session-b'); + store.set('device-2', 'hidden-a', 'session-c'); + + expect(store.get('device-1', 'hidden-a')).toBe('session-a'); + expect(store.get('device-1', 'hidden-b')).toBe('session-b'); + expect(store.get('device-2', 'hidden-a')).toBe('session-c'); + }); + + test('keeps pending sessions unreadable until wallet binding', () => { + const store = new DeviceWalletSessionStore(); + store.setPending('device-1', 'pending-session'); + + expect(store.get('device-1', undefined)).toBeUndefined(); + expect(store.getPending('device-1')).toBe('pending-session'); + }); + + test('migrates descriptor keys to stable device ids', () => { + const store = new DeviceWalletSessionStore(); + store.set('ble-path', 'hidden-a', 'session-a'); + store.setPending('ble-path', 'pending-session'); + + store.migrateDeviceKey('ble-path', 'stable-device-id'); + + expect(store.get('ble-path', 'hidden-a')).toBeUndefined(); + expect(store.get('stable-device-id', 'hidden-a')).toBe('session-a'); + expect(store.getPending('ble-path')).toBeUndefined(); + expect(store.getPending('stable-device-id')).toBe('pending-session'); + }); + + test('clears one wallet, one device, or all sessions', () => { + const store = new DeviceWalletSessionStore(); + store.set('device-1', 'hidden-a', 'session-a'); + store.set('device-1', 'hidden-b', 'session-b'); + store.set('device-2', 'hidden-a', 'session-c'); + + store.delete('device-1', 'hidden-a'); + expect(store.get('device-1', 'hidden-a')).toBeUndefined(); + expect(store.get('device-1', 'hidden-b')).toBe('session-b'); + + store.deleteDevice('device-1'); + expect(store.get('device-1', 'hidden-b')).toBeUndefined(); + expect(store.get('device-2', 'hidden-a')).toBe('session-c'); + + store.clear(); + expect(store.get('device-2', 'hidden-a')).toBeUndefined(); + }); +}); + +describe('ClearSessionCache', () => { + beforeEach(() => { + deviceWalletSessionStore.clear(); + }); + + test('clears one wallet without using a device', async () => { + deviceWalletSessionStore.set('device-1', 'hidden-a', 'session-a'); + deviceWalletSessionStore.set('device-1', 'hidden-b', 'session-b'); + const method = new ClearSessionCache({ + payload: { + method: 'clearSessionCache', + deviceId: 'device-1', + passphraseState: 'hidden-a', + }, + }); + + method.init(); + + expect(method.useDevice).toBe(false); + await expect(method.run()).resolves.toEqual({ cleared: true }); + expect(deviceWalletSessionStore.get('device-1', 'hidden-a')).toBeUndefined(); + expect(deviceWalletSessionStore.get('device-1', 'hidden-b')).toBe('session-b'); + }); + + test('clears one device or the full runtime store', async () => { + deviceWalletSessionStore.set('device-1', 'hidden-a', 'session-a'); + deviceWalletSessionStore.set('device-2', 'hidden-a', 'session-b'); + + const clearDevice = new ClearSessionCache({ + payload: { method: 'clearSessionCache', deviceId: 'device-1' }, + }); + clearDevice.init(); + await clearDevice.run(); + + expect(deviceWalletSessionStore.get('device-1', 'hidden-a')).toBeUndefined(); + expect(deviceWalletSessionStore.get('device-2', 'hidden-a')).toBe('session-b'); + + const clearAll = new ClearSessionCache({ + payload: { method: 'clearSessionCache' }, + }); + clearAll.init(); + await clearAll.run(); + + expect(deviceWalletSessionStore.get('device-2', 'hidden-a')).toBeUndefined(); + }); + + test('routes cache clearing through the active CoreApi call channel', async () => { + const call = jest.fn().mockResolvedValue({ success: true, payload: { cleared: true } }); + const api = createCoreApi(call as any); + + await api.clearSessionCache({ deviceId: 'device-1', passphraseState: 'hidden-a' }); + + expect(call).toHaveBeenCalledWith({ + method: 'clearSessionCache', + deviceId: 'device-1', + passphraseState: 'hidden-a', + }); + }); +}); diff --git a/packages/core/__tests__/evmLedgerLegacySafety.test.ts b/packages/core/__tests__/evmLedgerLegacySafety.test.ts index aed55b823..6e101932e 100644 --- a/packages/core/__tests__/evmLedgerLegacySafety.test.ts +++ b/packages/core/__tests__/evmLedgerLegacySafety.test.ts @@ -2,6 +2,8 @@ import AllNetworkGetAddressBase from '../src/api/allnetwork/AllNetworkGetAddress import EvmGetAddress from '../src/api/evm/EVMGetAddress'; import EVMGetPublicKey from '../src/api/evm/EVMGetPublicKey'; import { findMethod } from '../src/api/utils'; +import { getDeviceType, getFirmwareType, getMethodVersionRange } from '../src/utils'; +import { getDeviceFirmwareVersion } from '../src/utils/deviceVersionUtils'; jest.mock('../src/data/config', () => ({ getSDKVersion: jest.fn(() => '1.0.0'), @@ -14,16 +16,25 @@ jest.mock('../src/api/utils', () => ({ const createDevice = (onekeyDeviceType: string) => { const typedCall = jest.fn(); + const features = { + deviceType: onekeyDeviceType.toLowerCase(), + safety_checks: 'Strict', + }; return { typedCall, device: { - features: { - onekey_device_type: onekeyDeviceType, - safety_checks: 'Strict', - }, + features, commands: { typedCall, }, + // BaseMethod 通过 Device accessor 读取设备状态,stub 复用真实工具函数保持映射一致 + isProtocolV2: () => false, + getCurrentDeviceType: () => getDeviceType(features as any), + getCurrentSafetyChecks: () => features.safety_checks, + getCurrentFirmwareType: () => getFirmwareType(features as any), + getCurrentFirmwareVersionString: () => getDeviceFirmwareVersion(features as any)?.join('.'), + getCurrentMethodVersionRange: (fn: (model: any) => any) => + getMethodVersionRange(features as any, fn), }, }; }; diff --git a/packages/core/__tests__/evmSignTransaction.test.ts b/packages/core/__tests__/evmSignTransaction.test.ts index 3585772be..2d721f056 100644 --- a/packages/core/__tests__/evmSignTransaction.test.ts +++ b/packages/core/__tests__/evmSignTransaction.test.ts @@ -10,7 +10,7 @@ jest.mock('../src/data/config', () => ({ // Mock the device and transport manager jest.mock('../src/data-manager/TransportManager', () => ({ - getMessageVersion: jest.fn(() => 'v2'), + getProtocolV1MessageSchema: jest.fn(() => 'v1CurrentSchema'), })); jest.mock('../src/device/Device', () => ({ diff --git a/packages/core/__tests__/evmSignTypedData.test.ts b/packages/core/__tests__/evmSignTypedData.test.ts index a612e3f71..c31d463a0 100644 --- a/packages/core/__tests__/evmSignTypedData.test.ts +++ b/packages/core/__tests__/evmSignTypedData.test.ts @@ -9,7 +9,7 @@ jest.mock('../src/data/config', () => ({ })); jest.mock('../src/data-manager/TransportManager', () => ({ - getMessageVersion: jest.fn(() => 'v2'), + getProtocolV1MessageSchema: jest.fn(() => 'v1CurrentSchema'), })); jest.mock('../src/device/Device', () => ({ diff --git a/packages/core/__tests__/logBlockEvent.test.ts b/packages/core/__tests__/logBlockEvent.test.ts new file mode 100644 index 000000000..8b4a2f705 --- /dev/null +++ b/packages/core/__tests__/logBlockEvent.test.ts @@ -0,0 +1,37 @@ +import { UI_RESPONSE, getLogBlockLabel } from '../src/events'; + +describe('getLogBlockLabel', () => { + it('blocks evmSignTypedData params before logging large typed data', () => { + expect( + getLogBlockLabel({ + method: 'evmSignTypedData', + data: { + message: { + data: `0x${'ab'.repeat(4096)}`, + }, + }, + }) + ).toBe('evmSignTypedData'); + }); + + it('blocks evmSignTypedData iframe call payload before bridge logging', () => { + expect( + getLogBlockLabel({ + event: 'iframe-call', + type: 'iframe-call', + payload: { + method: 'evmSignTypedData', + data: { + message: { + data: `0x${'ab'.repeat(4096)}`, + }, + }, + }, + }) + ).toBe('evmSignTypedData'); + }); + + it('keeps existing sensitive UI response blocking', () => { + expect(getLogBlockLabel({ type: UI_RESPONSE.RECEIVE_PIN })).toBe(UI_RESPONSE.RECEIVE_PIN); + }); +}); diff --git a/packages/core/__tests__/preInitialize.test.ts b/packages/core/__tests__/preInitialize.test.ts index 89eba74d2..260146459 100644 --- a/packages/core/__tests__/preInitialize.test.ts +++ b/packages/core/__tests__/preInitialize.test.ts @@ -1,4 +1,9 @@ import { Device } from '../src/device/Device'; +import { initCore } from '../src/core'; +import PreInitialize from '../src/api/device/PreInitialize'; +import { IFRAME } from '../src/events'; + +import type Core from '../src/core'; jest.mock('../src/data/config', () => ({ getSDKVersion: jest.fn(() => '1.0.0'), @@ -6,6 +11,10 @@ jest.mock('../src/data/config', () => ({ })); describe('preInitialize', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + it('matches pre-initialized state by passphraseState only', () => { const device = Object.create(Device.prototype) as Device; @@ -19,4 +28,28 @@ describe('preInitialize', () => { }) ).toBe(true); }); + + it('cleans request lifecycle when preInitialize is acknowledged without connectId', async () => { + const core: Core = initCore(); + const disposeSpy = jest.spyOn(PreInitialize.prototype, 'dispose'); + + try { + const response = await core.handleMessage({ + id: 1001, + type: IFRAME.CALL, + payload: { + method: 'preInitialize', + }, + } as any); + + expect(response).toMatchObject({ + success: true, + payload: true, + }); + expect((core as any).tracingContext.activeRequests.has(1001)).toBe(false); + expect(disposeSpy).toHaveBeenCalledTimes(1); + } finally { + core.dispose(); + } + }); }); diff --git a/packages/core/__tests__/pro2Wallpaper.test.ts b/packages/core/__tests__/pro2Wallpaper.test.ts new file mode 100644 index 000000000..0347669c9 --- /dev/null +++ b/packages/core/__tests__/pro2Wallpaper.test.ts @@ -0,0 +1,39 @@ +import { encodePro2Wallpaper } from '../src/utils/pro2Wallpaper'; + +describe('encodePro2Wallpaper', () => { + test('encodes opaque pixels as aligned LVGL v9 RGB565 data', () => { + const result = encodePro2Wallpaper({ + width: 2, + height: 1, + rgba: new Uint8Array([255, 0, 0, 255, 0, 255, 0, 255]), + }); + + expect(result.colorFormat).toBe('RGB565'); + expect(Array.from(result.data.slice(0, 12))).toEqual([ + 0x19, 0x12, 0, 0, 2, 0, 1, 0, 4, 0, 0, 0, + ]); + expect(Array.from(result.data.slice(12))).toEqual([0x00, 0xf0, 0xc0, 0x07]); + }); + + test('encodes transparent pixels as RGB565A8 with alpha plane after RGB data', () => { + const result = encodePro2Wallpaper({ + width: 2, + height: 1, + rgba: new Uint8Array([0, 0, 255, 128, 255, 255, 255, 255]), + }); + + expect(result.colorFormat).toBe('RGB565A8'); + expect(result.data[1]).toBe(0x14); + expect(Array.from(result.data.slice(8, 10))).toEqual([4, 0]); + expect(Array.from(result.data.slice(12))).toEqual([0x1f, 0x00, 0xff, 0xff, 128, 255]); + }); + + test('rejects invalid dimensions and RGBA byte length', () => { + expect(() => encodePro2Wallpaper({ width: 0, height: 1, rgba: new Uint8Array() })).toThrow( + 'width' + ); + expect(() => encodePro2Wallpaper({ width: 2, height: 1, rgba: new Uint8Array(4) })).toThrow( + '8' + ); + }); +}); diff --git a/packages/core/__tests__/protocol-v2-firmware-targets.test.ts b/packages/core/__tests__/protocol-v2-firmware-targets.test.ts new file mode 100644 index 000000000..c7f6b8c49 --- /dev/null +++ b/packages/core/__tests__/protocol-v2-firmware-targets.test.ts @@ -0,0 +1,19 @@ +import { ProtocolV2FirmwareTargetType } from '../src/protocols/protocol-v2/firmware'; + +describe('Protocol V2 firmware target contract', () => { + test('matches the current firmware-pro2 target ids', () => { + expect(ProtocolV2FirmwareTargetType).toEqual({ + FW_MGMT_TARGET_INVALID: 0, + FW_MGMT_TARGET_CRATE: 1, + FW_MGMT_TARGET_ROMLOADER: 2, + FW_MGMT_TARGET_BOOTLOADER: 3, + FW_MGMT_TARGET_APPLICATION_P1: 4, + FW_MGMT_TARGET_APPLICATION_P2: 5, + FW_MGMT_TARGET_COPROCESSOR: 6, + FW_MGMT_TARGET_SE01: 7, + FW_MGMT_TARGET_SE02: 8, + FW_MGMT_TARGET_SE03: 9, + FW_MGMT_TARGET_SE04: 10, + }); + }); +}); diff --git a/packages/core/__tests__/protocol-v2.test.ts b/packages/core/__tests__/protocol-v2.test.ts new file mode 100644 index 000000000..9a1a79560 --- /dev/null +++ b/packages/core/__tests__/protocol-v2.test.ts @@ -0,0 +1,4658 @@ +import { HardwareErrorCode } from '@onekeyfe/hd-shared'; +import { DeviceRebootType } from '@onekeyfe/hd-transport'; + +import * as firmwareBinaryApi from '../src/api/firmware/getBinary'; +import DnxGetAddress from '../src/api/dynex/DnxGetAddress'; +import DnxSignTransaction from '../src/api/dynex/DnxSignTransaction'; +import DirList from '../src/api/DirList'; +import FileRead from '../src/api/FileRead'; +import FileWrite from '../src/api/FileWrite'; +import UploadPortfolio from '../src/api/UploadPortfolio'; +import DeviceFactoryInfoGet from '../src/api/protocol-v2/DeviceFactoryInfoGet'; +import DeviceFactoryInfoSet from '../src/api/protocol-v2/DeviceFactoryInfoSet'; +import DeviceFirmwareUpdate from '../src/api/protocol-v2/DeviceFirmwareUpdate'; +import DeviceGetFirmwareUpdateStatus from '../src/api/protocol-v2/DeviceGetFirmwareUpdateStatus'; +import DeviceGetOnboardingStatus from '../src/api/protocol-v2/DeviceGetOnboardingStatus'; +import DeviceInfoGet from '../src/api/protocol-v2/DeviceInfoGet'; +import DeviceReboot from '../src/api/protocol-v2/DeviceReboot'; +import DeviceSettingsGet from '../src/api/protocol-v2/DeviceSettingsGet'; +import DeviceSettingsPageShow from '../src/api/protocol-v2/DeviceSettingsPageShow'; +import DeviceSettingsSet from '../src/api/protocol-v2/DeviceSettingsSet'; +import DeviceUploadWallpaper from '../src/api/protocol-v2/DeviceUploadWallpaper'; +import DeviceSessionGet from '../src/api/protocol-v2/DeviceSessionGet'; +import DeviceStatusGet from '../src/api/protocol-v2/DeviceStatusGet'; +import FilesystemFormat from '../src/api/protocol-v2/FilesystemFormat'; +import FilesystemPermissionFix from '../src/api/protocol-v2/FilesystemPermissionFix'; +import ProtocolInfoRequest from '../src/api/protocol-v2/ProtocolInfoRequest'; +import EVMSignTypedData from '../src/api/evm/EVMSignTypedData'; +import EVMSignMessageEIP712 from '../src/api/evm/EVMSignMessageEIP712'; +import FirmwareUpdateV3 from '../src/api/FirmwareUpdateV3'; +import FirmwareUpdateV4 from '../src/api/FirmwareUpdateV4'; +import GetDeviceInfo from '../src/api/GetDeviceInfo'; +import GetPassphraseState from '../src/api/GetPassphraseState'; +import GetOnekeyFeatures from '../src/api/GetOnekeyFeatures'; +import { batchGetPublickeys } from '../src/api/helpers/batchGetPublickeys'; +import LnurlAuth from '../src/api/lightning/LnurlAuth'; +import PolkadotGetAddress from '../src/api/polkadot/PolkadotGetAddress'; +import SuiGetAddress from '../src/api/sui/SuiGetAddress'; +import SuiGetPublicKey from '../src/api/sui/SuiGetPublicKey'; +import SuiSignMessage from '../src/api/sui/SuiSignMessage'; +import SuiSignTransaction from '../src/api/sui/SuiSignTransaction'; +import SolGetAddress from '../src/api/solana/SolGetAddress'; +import SolSignMessage from '../src/api/solana/SolSignMessage'; +import SolSignOffchainMessage from '../src/api/solana/SolSignOffchainMessage'; +import SolSignTransaction from '../src/api/solana/SolSignTransaction'; +import TonGetAddress from '../src/api/ton/TonGetAddress'; +import TonSignData from '../src/api/ton/TonSignData'; +import TonSignMessage from '../src/api/ton/TonSignMessage'; +import TonSignProof from '../src/api/ton/TonSignProof'; +import TronGetAddress from '../src/api/tron/TronGetAddress'; +import StellarGetAddress from '../src/api/stellar/StellarGetAddress'; +import BenfenSignMessage from '../src/api/benfen/BenfenSignMessage'; +import { getBitcoinForkVersionRange } from '../src/api/btc/helpers/versionLimit'; +import { DataManager } from '../src/data-manager'; +import { createCoreApi } from '../src/inject'; +import { Device, preloadSessionCache } from '../src/device/Device'; +import { UI_REQUEST } from '../src/events/ui-request'; +import { + PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS, + requestProtocolV2DeviceInfo, +} from '../src/protocols/protocol-v2/features'; +import { + getProtocolV2WalletSession, + refreshProtocolV2DeviceStatus, +} from '../src/protocols/protocol-v2/walletSession'; +import { runMethodWithUnlockRetry } from '../src/protocols/protocol-v2/unlockRetry'; +import { buildProfileFromProtocolV2, buildProtocolV2FeaturesPayload } from '../src/deviceProfile'; +import { + getDeviceType, + getFirmwareType, + getFirmwareUpdateField, + getFirmwareUpdateFieldArray, + getMethodVersionRange, + isMethodVersionRangeUnsupported, +} from '../src/utils'; +import { getDeviceFirmwareVersion } from '../src/utils/deviceVersionUtils'; +import { + getPassphraseState, + getPassphraseStateWithRefreshDeviceInfo, +} from '../src/utils/deviceFeaturesUtils'; + +import type { DeviceCommands } from '../src/device/DeviceCommands'; +import type { Features } from '../src/types'; +import type { ProtocolV2DeviceInfo } from '../src/protocols/protocol-v2/features'; + +jest.mock('../src/data/config', () => ({ + getSDKVersion: jest.fn(() => '1.0.0'), + DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/', +})); + +describe('DeviceUploadWallpaper', () => { + test('encodes, uploads and applies a Pro2 wallpaper', async () => { + const rgba = new Uint8Array(604 * 1024 * 4).fill(255); + const typedCall = jest.fn().mockImplementation((request, _response, params) => { + if (request === 'FilesystemDirMake') return { message: { message: 'directory ready' } }; + if (request === 'FilesystemFileWrite') { + const file = params.file as { data: Uint8Array; offset: number }; + return { message: { processed_byte: file.offset + file.data.byteLength } }; + } + if (request === 'DeviceSettingsSet') { + return { message: { message: 'wallpaper applied' } }; + } + throw new Error(`Unexpected request: ${request}`); + }); + const method = new DeviceUploadWallpaper({ + id: 1, + payload: { method: 'deviceUploadWallpaper', width: 604, height: 1024, rgba }, + }); + (method as any).device = stubDevice({ commands: { typedCall } }); + + method.init(); + const result = await method.run(); + + expect(method.requireProtocolV2).toBe(true); + expect(method.unlockPolicy).toBe('retry-on-locked'); + expect(typedCall).toHaveBeenNthCalledWith(1, 'FilesystemDirMake', 'Success', { + path: 'vol0:/wallpapers/user', + }); + const fileWriteCall = typedCall.mock.calls.find(call => call[0] === 'FilesystemFileWrite'); + expect(fileWriteCall?.[2]).toMatchObject({ + file: { path: expect.stringMatching(/^vol0:\/wallpapers\/user\/wallpaper-[a-f0-9]+\.bin$/) }, + overwrite: true, + append: false, + }); + expect(typedCall).toHaveBeenLastCalledWith('DeviceSettingsSet', 'Success', { + settings: { wallpaper_path: result.path }, + }); + expect(typedCall.mock.calls.some(call => call[0] === 'SetWallpaper')).toBe(false); + expect(result).toMatchObject({ colorFormat: 'RGB565', message: 'wallpaper applied' }); + }); + + test('文件上传失败时不修改 wallpaper_path', async () => { + const typedCall = jest.fn().mockImplementation(request => { + if (request === 'FilesystemDirMake') return { message: {} }; + if (request === 'FilesystemFileWrite') throw new Error('write failed'); + return { message: {} }; + }); + const method = new DeviceUploadWallpaper({ + id: 1, + payload: { + method: 'deviceUploadWallpaper', + width: 604, + height: 1024, + rgba: new Uint8Array(604 * 1024 * 4), + }, + }); + (method as any).device = stubDevice({ commands: { typedCall } }); + method.init(); + + await expect(method.run()).rejects.toThrow('write failed'); + expect(typedCall.mock.calls.some(call => call[0] === 'DeviceSettingsSet')).toBe(false); + }); + + test('rejects unsafe filenames before device communication', () => { + const method = new DeviceUploadWallpaper({ + id: 1, + payload: { + method: 'deviceUploadWallpaper', + width: 604, + height: 1024, + rgba: new Uint8Array(604 * 1024 * 4), + fileName: '../wallpaper.bin', + }, + }); + + expect(() => method.init()).toThrow('fileName'); + }); +}); + +describe('UploadPortfolio', () => { + test('stages the complete package before applying PortfolioUpdate', async () => { + const packageBytes = new Uint8Array([1, 2, 3]); + const typedCall = jest + .fn() + .mockResolvedValueOnce({ message: { processed_byte: 3 } }) + .mockResolvedValueOnce({ message: { message: 'Portfolio updated' } }); + const method = new UploadPortfolio({ + id: 1, + payload: { + method: 'uploadPortfolio', + packageBytes, + }, + }); + (method as any).device = stubDevice({ commands: { typedCall } }); + method.postMessage = jest.fn(); + + method.init(); + const result = await method.run(); + + expect(typedCall).toHaveBeenNthCalledWith( + 1, + 'FilesystemFileWrite', + 'FilesystemFile', + { + file: { + path: 'vol1:/portfolio/portfolio.pfol.pending', + offset: 0, + total_size: 3, + data: packageBytes, + }, + overwrite: true, + append: false, + ui_percentage: 100, + }, + { timeoutMs: undefined } + ); + expect(typedCall).toHaveBeenNthCalledWith(2, 'PortfolioUpdate', 'Success', {}); + expect(result).toMatchObject({ + path: 'vol1:/portfolio/portfolio.pfol.pending', + processed_byte: 3, + portfolioUpdated: true, + }); + }); + + test('does not apply PortfolioUpdate when staging fails', async () => { + const typedCall = jest.fn().mockRejectedValue(new Error('write failed')); + const method = new UploadPortfolio({ + id: 1, + payload: { + method: 'uploadPortfolio', + packageBytes: new Uint8Array([1]), + }, + }); + (method as any).device = stubDevice({ commands: { typedCall } }); + + method.init(); + + await expect(method.run()).rejects.toThrow('write failed'); + expect(typedCall).toHaveBeenCalledTimes(1); + }); + + test('stops after the acknowledged chunk when the operation is aborted', async () => { + const packageBytes = new Uint8Array(4001); + const abortController = new AbortController(); + const typedCall = jest.fn().mockImplementationOnce(() => { + abortController.abort(); + return Promise.resolve({ message: { processed_byte: 2048 } }); + }); + const method = new UploadPortfolio({ + id: 1, + payload: { + method: 'uploadPortfolio', + packageBytes, + operationId: 'portfolio-1', + }, + }); + method.abortSignal = abortController.signal; + (method as any).device = stubDevice({ commands: { typedCall } }); + + method.init(); + + await expect(method.run()).rejects.toMatchObject({ + errorCode: HardwareErrorCode.CallQueueActionCancelled, + }); + expect(typedCall).toHaveBeenCalledTimes(1); + expect(typedCall).not.toHaveBeenCalledWith('PortfolioUpdate', 'Success', {}); + }); +}); + +const descriptor = { + id: 'ble-id', + path: 'usb-path', +}; + +/** + * 为纯对象 stub 设备补齐 Device 的协议判别与 getCurrent* accessor, + * 缺省实现与 Device.ts 语义保持一致;已有同名字段(如 jest.fn())不覆盖。 + */ +function stubDevice>(device: T): T { + const d = device as any; + d.isProtocolV2 ??= () => d.originalDescriptor?.protocolType === 'V2'; + d.getProtocol ??= () => (d.isProtocolV2() ? 'V2' : 'V1'); + d.getCurrentDeviceType ??= () => getDeviceType(d.features); + d.getCurrentFirmwareType ??= () => getFirmwareType(d.features); + d.getCurrentFirmwareVersionString ??= () => + d.features ? getDeviceFirmwareVersion(d.features).join('.') : undefined; + d.getCurrentBLEFirmwareVersionString ??= () => d.features?.bleVersion; + d.getCurrentSafetyChecks ??= () => d.features?.safetyChecks; + d.getCurrentDeviceId ??= () => d.features?.deviceId || undefined; + d.getCurrentSerialNo ??= () => d.features?.serialNo || ''; + d.getCurrentPassphraseProtection ??= () => d.features?.passphraseProtection; + d.getCurrentMethodVersionRange ??= (fn: (model: any) => any) => { + const deviceType = d.getCurrentDeviceType(); + const range = fn(deviceType); + if (range) return range; + return getMethodVersionRange(d.features, fn); + }; + d.updateProtocolV2Features ??= (deviceInfo?: ProtocolV2DeviceInfo) => { + d.features = buildProtocolV2FeaturesPayload(deviceInfo, d.features); + return d.features; + }; + return device; +} + +// 直接复用生产映射函数,避免测试内副本与实现漂移(之前的手抄副本已缺失 se boot 字段) +function normalizeProtocolV2Features(_descriptor: unknown, deviceInfo?: ProtocolV2DeviceInfo) { + return buildProtocolV2FeaturesPayload(deviceInfo); +} + +const protocolV2BootloaderDeviceInfo: ProtocolV2DeviceInfo = { + protocol_version: 1, + hw: { + serial_no: 'PR9999999999', + }, + fw: { + bootloader: { + version: '0.0.1', + }, + }, +}; + +async function requestProtocolV2Features({ + commands, + descriptor: inputDescriptor, +}: { + commands: DeviceCommands; + descriptor?: unknown; +}) { + const deviceInfo = await requestProtocolV2DeviceInfo({ commands }); + return normalizeProtocolV2Features(inputDescriptor ?? descriptor, deviceInfo); +} + +describe('Protocol V2 feature adapter', () => { + test('normalizes Protocol V2 DeviceInfo into existing Features fields', () => { + const features = normalizeProtocolV2Features(descriptor as any, { + protocol_version: 1, + hw: { + serial_no: 'PR2SERIAL', + }, + fw: { + romloader: { + version: '0.1.0', + build_id: 'rom-build', + hash: [1, 2, 255], + }, + application_data: { + version: '9.8.7', + build_id: 'app-data-build', + hash: [9, 8, 7], + }, + bootloader: { + version: '0.2.0', + build_id: 'boot-build', + hash: new Uint8Array([10, 11]), + }, + application: { + version: '1.2.3', + build_id: 'app-build', + hash: 'abc123', + }, + }, + coprocessor: { + application: { + version: '4.5.6', + build_id: 'bt-build', + hash: [12, 13], + }, + bt_adv_name: 'Pro2 BLE', + }, + se1: { + application: { + version: '7.8.9', + build_id: 'se-build', + hash: [14, 15], + }, + state: 85, + }, + se2: { + application: { + version: '8.0.0', + }, + state: 0, + }, + status: { + device_id: 'PRO2-DEVICE-ID', + unlocked: true, + init_states: false, + backup_required: true, + passphrase_enabled: true, + attach_to_pin_enabled: true, + unlocked_by_attach_to_pin: true, + }, + }); + + expect(features.deviceId).toBe('PRO2-DEVICE-ID'); + expect(features.serialNo).toBe('PR2SERIAL'); + expect(features.deviceType).toBe('pro2'); + expect(features.protocolVersion).toBe(1); + expect(features.firmwareVersion).toBe('1.2.3'); + expect(features.verify?.firmwareBuildId).toBe('app-build'); + expect(features.verify?.firmwareHash).toBe('abc123'); + expect(features.bootloaderVersion).toBe('0.2.0'); + expect(features.verify?.bootloaderBuildId).toBe('boot-build'); + expect(features.verify?.bootloaderHash).toBe('0a0b'); + expect(features.boardVersion).toBe('0.1.0'); + expect(features.verify?.boardBuildId).toBe('rom-build'); + expect(features.verify?.boardHash).toBe('0102ff'); + expect(features.bleName).toBe('Pro2 BLE'); + expect(features.bleVersion).toBe('4.5.6'); + expect(features.verify?.bleHash).toBe('0c0d'); + expect(features.se01Version).toBe('7.8.9'); + expect(features.verify?.se01Hash).toBe('0e0f'); + expect(features.label).toBeNull(); + expect(features.language).toBeNull(); + expect(features.initialized).toBe(false); + expect(features.unlocked).toBe(true); + expect(features.backupRequired).toBe(true); + expect(features.passphraseProtection).toBe(true); + expect(features.attachToPinEnabled).toBe(true); + expect(features.unlockedAttachPin).toBe(true); + expect(features.bleEnabled).toBeNull(); + }); + + test('marks Protocol V2 DeviceInfo without status as bootloader mode', () => { + const deviceInfo = { + protocol_version: 1, + hw: { + serial_no: 'PR2BOOT', + }, + fw: { + bootloader: { + version: '0.2.0', + }, + }, + }; + const features = normalizeProtocolV2Features(descriptor as any, deviceInfo); + const profile = buildProfileFromProtocolV2({ deviceInfo }); + + expect(features.mode).toBe('bootloader'); + expect(features.bootloaderMode).toBe(true); + expect(features.initialized).toBeNull(); + expect(profile.status.mode).toBe('bootloader'); + expect(profile.status.bootloaderMode).toBe(true); + }); + + test('marks current romloader-shaped Protocol V2 DeviceInfo as romloader mode', () => { + const deviceInfo = { + protocol_version: 1, + hw: { + serial_no: 'PR2ROM', + }, + fw: { + romloader: { + version: '1.0.0', + }, + bootloader: { + version: '2.0.0', + }, + }, + }; + const features = normalizeProtocolV2Features(descriptor as any, deviceInfo); + const profile = buildProfileFromProtocolV2({ deviceInfo }); + + expect(features.mode).toBe('romloader'); + expect(features.bootloaderMode).toBe(false); + expect(features.boardVersion).toBe('1.0.0'); + expect(profile.status.mode).toBe('romloader'); + expect(profile.status.bootloaderMode).toBe(false); + expect(profile.versions.board).toBe('1.0.0'); + }); + + test('uses DeviceSessionGet for Protocol V2 passphrase sessions', async () => { + const features = normalizeProtocolV2Features(descriptor as any); + features.firmwareVersion = '1.2.3'; + features.passphraseProtection = true; + const typedCall = jest.fn().mockResolvedValue({ + type: 'DeviceSession', + message: { + btc_test_address: 'state-1', + session_id: 'session-1', + }, + }); + const commands = { typedCall } as unknown as DeviceCommands; + const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any); + (device as any).features = features; + (device as any).commands = commands; + + await getPassphraseState(device, { + expectPassphraseState: 'state-1', + }); + expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {}); + + await expect( + getPassphraseState(device, { + expectPassphraseState: 'state-2', + }) + ).rejects.toEqual( + expect.objectContaining({ + errorCode: HardwareErrorCode.DeviceCheckPassphraseStateError, + }) + ); + expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {}); + + await getPassphraseState(device, { + onlyMainPin: true, + }); + expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {}); + }); + + test('deviceStatusGet returns raw DeviceStatus without updating features', async () => { + const typedCall = jest.fn().mockResolvedValue({ + type: 'DeviceStatus', + message: { device_id: 'device-1', unlocked: true }, + }); + const updateProtocolV2Features = jest.fn(); + const method = new DeviceStatusGet({ + payload: { method: 'deviceStatusGet', connectId: 'connect-id' }, + }); + method.init(); + method.device = stubDevice({ + originalDescriptor: { ...descriptor, protocolType: 'V2' }, + commands: { typedCall }, + updateProtocolV2Features, + }) as any; + + await expect(method.run()).resolves.toEqual({ device_id: 'device-1', unlocked: true }); + expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {}); + expect(updateProtocolV2Features).not.toHaveBeenCalled(); + }); + + test('deviceGetOnboardingStatus returns the real Protocol V2 onboarding stage', async () => { + const typedCall = jest.fn().mockResolvedValue({ + type: 'DevOnboardingStatus', + message: { stage: 2, status_code: 3, detail_code: 4 }, + }); + const method = new DeviceGetOnboardingStatus({ + payload: { + method: 'deviceGetOnboardingStatus', + connectId: 'connect-id', + }, + }); + method.init(); + method.device = stubDevice({ + originalDescriptor: { ...descriptor, protocolType: 'V2' }, + commands: { typedCall }, + }) as any; + + await expect(method.run()).resolves.toEqual({ + stage: 2, + status_code: 3, + detail_code: 4, + }); + expect(typedCall).toHaveBeenCalledWith('DevGetOnboardingStatus', 'DevOnboardingStatus', {}); + expect(method.requireProtocolV2).toBe(true); + }); + + test('deviceSessionGet sends an empty request and does not mutate wallet cache', async () => { + const typedCall = jest.fn().mockResolvedValue({ + type: 'DeviceSession', + message: { session_id: 'new-session', btc_test_address: 'state-a' }, + }); + const updateInternalState = jest.fn(); + const method = new DeviceSessionGet({ + payload: { + method: 'deviceSessionGet', + connectId: 'connect-id', + }, + }); + method.init(); + method.device = stubDevice({ + originalDescriptor: { ...descriptor, protocolType: 'V2' }, + commands: { typedCall }, + updateInternalState, + }) as any; + + await expect(method.run()).resolves.toEqual({ + session_id: 'new-session', + btc_test_address: 'state-a', + }); + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {}); + expect(updateInternalState).not.toHaveBeenCalled(); + }); + + test('routes raw status and session methods through CoreApi', async () => { + const call = jest.fn().mockResolvedValue({ success: true, payload: {} }); + const api = createCoreApi(call as any); + + await api.deviceStatusGet('connect-id', { retryCount: 1 }); + await api.deviceGetOnboardingStatus('connect-id', { retryCount: 1 }); + await api.deviceSessionGet('connect-id', { retryCount: 1 }); + + expect(call).toHaveBeenNthCalledWith(1, { + method: 'deviceStatusGet', + connectId: 'connect-id', + retryCount: 1, + }); + expect(call).toHaveBeenNthCalledWith(2, { + method: 'deviceGetOnboardingStatus', + connectId: 'connect-id', + retryCount: 1, + }); + expect(call).toHaveBeenNthCalledWith(3, { + method: 'deviceSessionGet', + connectId: 'connect-id', + retryCount: 1, + }); + }); + + test('reuses the cached session id for the selected Pro2 wallet', async () => { + const device = Device.fromDescriptor({ + id: 'cache-device-1', + path: 'cache-path-1', + protocolType: 'V2', + } as any); + (device as any).features = normalizeProtocolV2Features( + { ...descriptor, protocolType: 'V2' } as any, + { + status: { + device_id: 'stable-device-1', + unlocked: true, + passphrase_enabled: true, + }, + } + ); + device.passphraseState = 'state-a'; + preloadSessionCache('stable-device-1', 'state-a', 'session-a'); + const typedCall = jest.fn().mockResolvedValue({ + type: 'DeviceSession', + message: { session_id: 'session-b', btc_test_address: 'state-a' }, + }); + (device as any).commands = { typedCall }; + + await expect(getProtocolV2WalletSession(device)).resolves.toMatchObject({ + passphraseState: 'state-a', + newSession: 'session-b', + }); + + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', { + session_id: 'session-a', + }); + expect(device.getInternalState()).toBe('session-b'); + }); + + test('does not reuse another Pro2 wallet session without passphraseState', async () => { + const device = Device.fromDescriptor({ + id: 'cache-device-2', + path: 'cache-path-2', + protocolType: 'V2', + } as any); + (device as any).features = normalizeProtocolV2Features( + { ...descriptor, protocolType: 'V2' } as any, + { status: { device_id: 'stable-device-2', unlocked: true } } + ); + preloadSessionCache('stable-device-2', 'state-a', 'session-a'); + const typedCall = jest.fn().mockResolvedValue({ + type: 'DeviceSession', + message: { session_id: 'main-session' }, + }); + (device as any).commands = { typedCall }; + + await getProtocolV2WalletSession(device); + + expect(typedCall).toHaveBeenCalledWith('DeviceSessionGet', 'DeviceSession', {}); + expect(device.getInternalState()).toBeUndefined(); + }); + + test('retries without the selected Pro2 cache entry after invalid session rejection', async () => { + const device = Device.fromDescriptor({ + id: 'cache-device-3', + path: 'cache-path-3', + protocolType: 'V2', + } as any); + (device as any).features = normalizeProtocolV2Features( + { ...descriptor, protocolType: 'V2' } as any, + { + status: { + device_id: 'stable-device-3', + unlocked: true, + passphrase_enabled: true, + }, + } + ); + device.passphraseState = 'state-a'; + preloadSessionCache('stable-device-3', 'state-a', 'session-a'); + const typedCall = jest + .fn() + .mockRejectedValueOnce(new Error('Failure_ProcessError,Failure_InvalidSession')) + .mockResolvedValueOnce({ + type: 'DeviceSession', + message: { session_id: 'session-b', btc_test_address: 'state-a' }, + }); + (device as any).commands = { typedCall }; + + await expect(getProtocolV2WalletSession(device)).resolves.toMatchObject({ + passphraseState: 'state-a', + newSession: 'session-b', + }); + expect(typedCall.mock.calls).toEqual([ + ['DeviceSessionGet', 'DeviceSession', { session_id: 'session-a' }], + ['DeviceSessionGet', 'DeviceSession', {}], + ]); + expect(device.getInternalState()).toBe('session-b'); + }); + + test('does not retry an invalid Pro2 session when no cached session was sent', async () => { + const device = Device.fromDescriptor({ + id: 'cache-device-no-session', + path: 'cache-path-no-session', + protocolType: 'V2', + } as any); + (device as any).features = normalizeProtocolV2Features( + { ...descriptor, protocolType: 'V2' } as any, + { + status: { + device_id: 'stable-device-no-session', + unlocked: true, + passphrase_enabled: true, + }, + } + ); + const typedCall = jest + .fn() + .mockRejectedValue(new Error('Failure_ProcessError,Failure_InvalidSession')); + (device as any).commands = { typedCall }; + + await expect(getProtocolV2WalletSession(device)).rejects.toThrow('Failure_InvalidSession'); + expect(typedCall.mock.calls).toEqual([['DeviceSessionGet', 'DeviceSession', {}]]); + }); + + test('rejects a mismatched Pro2 wallet state and clears the selected cache entry', async () => { + const device = Device.fromDescriptor({ + id: 'cache-device-mismatch', + path: 'cache-path-mismatch', + protocolType: 'V2', + } as any); + (device as any).features = normalizeProtocolV2Features( + { ...descriptor, protocolType: 'V2' } as any, + { + status: { + device_id: 'stable-device-mismatch', + unlocked: true, + passphrase_enabled: true, + }, + } + ); + device.passphraseState = 'state-a'; + preloadSessionCache('stable-device-mismatch', 'state-a', 'session-a'); + (device as any).commands = { + typedCall: jest.fn().mockResolvedValue({ + type: 'DeviceSession', + message: { session_id: 'session-b', btc_test_address: 'state-b' }, + }), + }; + + await expect( + getPassphraseStateWithRefreshDeviceInfo(device, { expectPassphraseState: 'state-a' }) + ).rejects.toEqual( + expect.objectContaining({ + errorCode: HardwareErrorCode.DeviceCheckPassphraseStateError, + }) + ); + expect(device.getInternalState()).toBeUndefined(); + }); + + test('rejects DeviceSessionGet while cached Pro2 status is locked', async () => { + const device = Device.fromDescriptor({ + id: 'cache-device-4', + path: 'cache-path-4', + protocolType: 'V2', + } as any); + (device as any).features = normalizeProtocolV2Features( + { ...descriptor, protocolType: 'V2' } as any, + { status: { device_id: 'stable-device-4', unlocked: false } } + ); + const typedCall = jest.fn(); + (device as any).commands = { typedCall }; + + await expect(getProtocolV2WalletSession(device)).rejects.toThrow('Device is locked'); + expect(typedCall).not.toHaveBeenCalled(); + }); + + test('does not request a Pro2 wallet session before features are initialized', async () => { + const device = Device.fromDescriptor({ + id: 'cache-device-5', + path: 'cache-path-5', + protocolType: 'V2', + } as any); + const typedCall = jest.fn(); + (device as any).commands = { typedCall }; + + await expect(getPassphraseStateWithRefreshDeviceInfo(device)).resolves.toEqual({ + passphraseState: undefined, + newSession: undefined, + unlockedAttachPin: undefined, + }); + expect(typedCall).not.toHaveBeenCalled(); + }); + + test('merges DeviceStatus into existing Protocol V2 features', async () => { + const device = Device.fromDescriptor({ + id: 'status-device', + path: 'status-path', + protocolType: 'V2', + } as any); + (device as any).features = normalizeProtocolV2Features( + { ...descriptor, protocolType: 'V2' } as any, + { + protocol_version: 2, + fw: { application: { version: '1.2.3' } }, + se1: { application: { version: '4.5.6' } }, + status: { unlocked: false, passphrase_enabled: false }, + } + ); + const typedCall = jest.fn().mockResolvedValue({ + type: 'DeviceStatus', + message: { + device_id: 'stable-status-device', + unlocked: true, + passphrase_enabled: true, + }, + }); + (device as any).commands = { typedCall }; + + const features = await refreshProtocolV2DeviceStatus(device); + + expect(features).toMatchObject({ + deviceId: 'stable-status-device', + unlocked: true, + passphraseProtection: true, + firmwareVersion: '1.2.3', + se01Version: '4.5.6', + }); + expect(typedCall).toHaveBeenCalledWith('DeviceStatusGet', 'DeviceStatus', {}); + }); + + test('returns passphrase state string for existing Pro devices', async () => { + const features = { + deviceId: 'pro-device-id', + deviceType: 'pro', + firmwareVersion: '4.15.0', + passphraseProtection: true, + sessionId: 'feature-session', + unlockedAttachPin: true, + }; + const typedCall = jest.fn().mockResolvedValue({ + type: 'PassphraseState', + message: { + passphrase_state: 'state-pro', + session_id: 'session-pro', + unlocked_attach_pin: false, + }, + }); + const updateInternalState = jest.fn(); + const method = new GetPassphraseState({ + payload: { + method: 'getPassphraseState', + connectId: 'connect-id', + }, + }); + method.device = stubDevice({ + originalDescriptor: { ...descriptor, protocolType: 'V1' }, + features, + commands: { typedCall }, + updateInternalState, + getCurrentDeviceId: () => 'pro-device-id', + getCurrentPassphraseProtection: () => true, + }) as any; + + await expect(method.run()).resolves.toBe('state-pro'); + expect(updateInternalState).toHaveBeenCalledWith( + true, + 'state-pro', + 'pro-device-id', + 'session-pro', + 'feature-session' + ); + }); + + test('returns passphrase state string for Pro2 devices', async () => { + const features = { + deviceId: null, + deviceType: 'pro2', + firmwareVersion: '4.15.0', + passphraseProtection: true, + sessionId: 'feature-session', + unlockedAttachPin: true, + }; + const typedCall = jest.fn().mockResolvedValue({ + type: 'DeviceSession', + message: { + btc_test_address: 'state-pro2', + session_id: 'session-pro2', + }, + }); + const updateInternalState = jest.fn(); + const getFeatures = jest.fn().mockResolvedValue(features); + const method = new GetPassphraseState({ + payload: { + method: 'getPassphraseState', + connectId: 'connect-id', + }, + }); + method.device = stubDevice({ + originalDescriptor: { ...descriptor, protocolType: 'V2' }, + features, + commands: { typedCall }, + updateInternalState, + getFeatures, + getCurrentDeviceType: () => 'pro2', + getCurrentDeviceId: () => undefined, + getCurrentPassphraseProtection: () => true, + }) as any; + + await expect(method.run()).resolves.toBe('state-pro2'); + expect(getFeatures).not.toHaveBeenCalled(); + }); + + test('honors initSession when getting Pro2 passphrase state', async () => { + const features = { + deviceId: 'pro2-device-id', + deviceType: 'pro2', + firmwareVersion: '9.9.9', + passphraseProtection: true, + sessionId: 'old-feature-session', + unlockedAttachPin: false, + }; + const typedCall = jest.fn().mockResolvedValue({ + type: 'DeviceSession', + message: { + btc_test_address: 'state-pro2-new', + }, + }); + const clearInternalState = jest.fn(); + const updateInternalState = jest.fn(); + const method = new GetPassphraseState({ + payload: { + method: 'getPassphraseState', + connectId: 'connect-id', + initSession: true, + }, + }); + method.device = stubDevice({ + originalDescriptor: { ...descriptor, protocolType: 'V2' }, + features, + commands: { typedCall }, + clearInternalState, + updateInternalState, + getCurrentDeviceId: () => 'pro2-device-id', + getCurrentPassphraseProtection: () => true, + }) as any; + + await expect(method.run()).resolves.toBe('state-pro2-new'); + expect(clearInternalState).toHaveBeenCalledTimes(1); + expect(updateInternalState).toHaveBeenCalledWith( + true, + 'state-pro2-new', + 'pro2-device-id', + undefined, + null + ); + }); + + test('stores Pro2 passphrase session cache without synthetic device id', async () => { + const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any); + const typedCall = jest.fn().mockResolvedValue({ + type: 'DeviceSession', + message: { + btc_test_address: 'state-profile', + session_id: 'session-profile', + }, + }); + + (device as any).features = { + ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, { + hw: { serial_no: 'PR2SERIAL' }, + fw: { application: { version: '4.15.0' } }, + status: { passphrase_enabled: true }, + }), + unlocked: true, + }; + (device as any).commands = { typedCall }; + + await getPassphraseStateWithRefreshDeviceInfo(device); + + device.passphraseState = 'state-profile'; + expect(device.getInternalState()).toBe('session-profile'); + expect(device.getInternalState('CACHED-ID')).toBeUndefined(); + }); + + test('stores Pro2 passphrase sessions without selecting them implicitly', async () => { + const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any); + const typedCall = jest.fn().mockResolvedValue({ + type: 'DeviceSession', + message: { + btc_test_address: 'state-auto', + session_id: 'session-auto', + }, + }); + + (device as any).features = normalizeProtocolV2Features( + { + ...descriptor, + protocolType: 'V2', + } as any, + { + hw: { serial_no: 'PR2SERIAL' }, + } + ); + (device as any).features.firmwareVersion = '4.15.0'; + (device as any).features.passphraseProtection = true; + (device as any).features.unlocked = true; + (device as any).commands = { typedCall }; + + await expect(getPassphraseStateWithRefreshDeviceInfo(device)).resolves.toMatchObject({ + passphraseState: 'state-auto', + newSession: 'session-auto', + }); + + expect(device.passphraseState).toBeUndefined(); + expect(device.features?.passphraseProtection).toBe(true); + expect(device.features?.sessionId).toBeNull(); + expect(device.getInternalState()).toBeUndefined(); + device.passphraseState = 'state-auto'; + expect(device.getInternalState()).toBe('session-auto'); + expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {}); + }); + + test('does not mark Pro2 passphrase enabled from a main PIN session alone', async () => { + const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any); + const typedCall = jest.fn().mockResolvedValue({ + type: 'DeviceSession', + message: { + session_id: 'main-pin-session', + }, + }); + + (device as any).features = normalizeProtocolV2Features( + { + ...descriptor, + protocolType: 'V2', + } as any, + { + status: { + passphrase_enabled: false, + }, + } + ); + (device as any).features.firmwareVersion = '4.15.0'; + (device as any).features.unlocked = true; + (device as any).commands = { typedCall }; + + await expect( + getPassphraseStateWithRefreshDeviceInfo(device, { onlyMainPin: true }) + ).resolves.toMatchObject({ + passphraseState: undefined, + newSession: 'main-pin-session', + }); + + expect(device.features?.passphraseProtection).toBe(false); + expect(device.features?.sessionId).toBeNull(); + expect(device.getInternalState()).toBeUndefined(); + expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {}); + }); + + test('does not let skipPassphraseCheck hide Pro2 passphrase state mismatch', async () => { + const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any); + const typedCall = jest.fn().mockResolvedValue({ + type: 'DeviceSession', + message: { + btc_test_address: 'wrong-state', + session_id: 'wrong-session', + }, + }); + + (device as any).features = normalizeProtocolV2Features({ + ...descriptor, + protocolType: 'V2', + } as any); + (device as any).features.firmwareVersion = '4.15.0'; + (device as any).features.passphraseProtection = true; + (device as any).commands = { typedCall }; + + await expect(device.checkPassphraseStateSafety('expected-state', false, true)).rejects.toEqual( + expect.objectContaining({ + errorCode: HardwareErrorCode.DeviceCheckPassphraseStateError, + }) + ); + + expect(device.getInternalState()).toBeUndefined(); + expect(typedCall).toHaveBeenNthCalledWith(1, 'DeviceSessionGet', 'DeviceSession', {}); + }); + + test('marks fallback features as unavailable when DeviceInfo is missing', () => { + const features = normalizeProtocolV2Features(descriptor as any); + + expect(features.deviceId).toBeNull(); + expect(features.serialNo).toBe(''); + expect(features.initialized).toBeNull(); + expect(features.unlocked).toBeNull(); + expect(features.firmwarePresent).toBeNull(); + }); + + test('uses Protocol V2 status.device_id and does not fall back to serial_no', () => { + const features = normalizeProtocolV2Features( + { + id: 'PR2000000000', + path: 'PR2000000000', + protocolType: 'V2', + } as any, + { + hw: { + serial_no: 'PR9999999999', + }, + status: { + device_id: 'DEVICE-ID-9999', + }, + } + ); + + expect(features.deviceId).toBe('DEVICE-ID-9999'); + expect(features.serialNo).toBe('PR9999999999'); + }); + + test('does not use Protocol V2 serial_no as deviceId when hw.device_id is absent', () => { + const features = normalizeProtocolV2Features( + { + id: 'PR2000000000', + path: 'PR2000000000', + protocolType: 'V2', + } as any, + { + hw: { + serial_no: 'PR9999999999', + }, + } + ); + + expect(features.deviceId).toBeNull(); + expect(features.serialNo).toBe('PR9999999999'); + }); + + test('uses Protocol V2 features directly when profile is absent', () => { + const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any); + (device as any).features = { + ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any), + serialNo: 'CACHED-SERIAL', + label: 'Cached Label', + bleName: 'Cached BLE', + passphraseProtection: true, + }; + + expect(device.toMessageObject()).toMatchObject({ + uuid: 'CACHED-SERIAL', + deviceId: null, + bleName: 'Cached BLE', + label: 'Cached Label', + deviceType: 'pro2', + }); + expect(device.getCurrentPassphraseProtection()).toBe(true); + expect(device.hasUsePassphrase()).toBe(true); + }); + + test('syncs Protocol V2 cached features without cached profile', () => { + const cached = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any); + (cached as any).features = { + ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any), + deviceId: null, + serialNo: 'CACHED-SERIAL', + }; + + const current = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any); + current.updateFromCache(cached); + + expect(current.getCurrentDeviceId()).toBeUndefined(); + expect(current.getCurrentSerialNo()).toBe('CACHED-SERIAL'); + expect(current.toMessageObject()).toMatchObject({ + uuid: 'CACHED-SERIAL', + deviceId: null, + }); + }); + + test('initializes Protocol V2 features from lightweight DeviceInfoGet', async () => { + const commands = { + typedCall: jest.fn().mockResolvedValueOnce({ + type: 'DeviceInfo', + message: { + hw: { serial_no: 'PR2SERIAL' }, + status: { + device_id: 'PRO2-STATUS-ID', + init_states: true, + passphrase_enabled: true, + }, + }, + }), + }; + + const features = await requestProtocolV2Features({ + commands: commands as unknown as DeviceCommands, + descriptor: descriptor as any, + }); + + expect(features.deviceId).toBe('PRO2-STATUS-ID'); + expect(features.initialized).toBe(true); + expect(features.passphraseProtection).toBe(true); + expect(commands.typedCall).toHaveBeenCalledTimes(1); + expect(commands.typedCall).toHaveBeenNthCalledWith( + 1, + 'DeviceInfoGet', + 'DeviceInfo', + { + targets: { + hw: true, + fw: true, + coprocessor: true, + status: true, + }, + types: { + version: true, + specific: true, + }, + }, + { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS } + ); + }); + + test('fails initialization when Protocol V2 DeviceInfoGet fails', async () => { + const commands = { + typedCall: jest.fn().mockRejectedValueOnce(new Error('DeviceInfo not supported')), + }; + + await expect( + requestProtocolV2Features({ + commands: commands as unknown as DeviceCommands, + descriptor: descriptor as any, + }) + ).rejects.toThrow('DeviceInfo not supported'); + }); + + test('refreshes Protocol V2 basic device info with the lightweight request', async () => { + const typedCall = jest.fn().mockResolvedValueOnce({ + type: 'DeviceInfo', + message: { + hw: { serial_no: 'PR2SERIAL' }, + fw: { + application: { + version: '5.6.7', + }, + }, + coprocessor: { + application: { + version: '8.9.10', + }, + bt_adv_name: 'Raw Pro2 BLE', + }, + status: { + device_id: 'PRO2-BASIC-ID', + unlocked_by_attach_to_pin: true, + init_states: true, + passphrase_enabled: true, + }, + }, + }); + const method = new GetDeviceInfo({ + id: 1, + payload: { + method: 'getDeviceInfo', + scope: 'basic', + refresh: true, + }, + }); + method.init(); + (method as any).device = stubDevice({ + originalDescriptor: { ...descriptor, protocolType: 'V2' }, + features: { + ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any), + device_id: 'CACHED-ID', + serial_no: 'CACHED-SERIAL', + label: 'Cached Pro2', + onekey_firmware_version: '1.2.3', + onekey_ble_version: '2.3.4', + }, + commands: { typedCall }, + _updateFeatures: jest.fn(), + }); + + const result = await method.run(); + + expect(typedCall).toHaveBeenCalledWith( + 'DeviceInfoGet', + 'DeviceInfo', + { + targets: { + hw: true, + fw: true, + coprocessor: true, + status: true, + }, + types: { + version: true, + specific: true, + }, + }, + { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS } + ); + expect(result).toMatchObject({ + protocol: 'V2', + deviceType: 'pro2', + deviceId: 'PRO2-BASIC-ID', + serialNo: 'PR2SERIAL', + label: null, + bleName: 'Raw Pro2 BLE', + status: { + initialized: true, + unlockedAttachPin: true, + passphraseProtection: true, + }, + versions: { + firmware: '5.6.7', + ble: '8.9.10', + }, + }); + }); + + test('does not fill Protocol V2 DeviceProfile from cached V1-shaped fields', async () => { + const typedCall = jest.fn().mockResolvedValueOnce({ + type: 'DeviceInfo', + message: { + status: { + init_states: true, + }, + }, + }); + const method = new GetDeviceInfo({ + id: 1, + payload: { + method: 'getDeviceInfo', + scope: 'basic', + }, + }); + method.init(); + (method as any).device = stubDevice({ + originalDescriptor: { ...descriptor, protocolType: 'V2' }, + features: { + ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any), + device_id: 'STALE-ID', + serial_no: 'STALE-SERIAL', + label: 'Stale Pro2', + ble_name: 'Stale BLE', + passphrase_protection: true, + onekey_firmware_version: '1.2.3', + onekey_ble_version: '2.3.4', + }, + commands: { typedCall }, + }); + + const result = await method.run(); + + expect(result).toMatchObject({ + protocol: 'V2', + // 身份字段不得取自缓存的 V1-shaped features(STALE-ID / STALE-SERIAL), + // hw.serial_no 缺失时也不回退 descriptor.path。 + deviceId: '', + serialNo: '', + label: null, + bleName: null, + status: { + passphraseProtection: null, + noBackup: null, + }, + versions: { + firmware: null, + ble: null, + }, + }); + }); + + test('reads full Protocol V2 device info only for verify scope', async () => { + const typedCall = jest.fn().mockResolvedValueOnce({ + type: 'DeviceInfo', + message: { + hw: { serial_no: 'PR2SERIAL' }, + status: { init_states: true }, + }, + }); + const method = new GetDeviceInfo({ + id: 1, + payload: { + method: 'getDeviceInfo', + scope: 'verify', + }, + }); + method.init(); + (method as any).device = stubDevice({ + originalDescriptor: { ...descriptor, protocolType: 'V2' }, + features: normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any), + commands: { typedCall }, + _updateFeatures: jest.fn(), + }); + + await method.run(); + + expect(typedCall).toHaveBeenCalledWith( + 'DeviceInfoGet', + 'DeviceInfo', + { + targets: { + hw: true, + fw: true, + coprocessor: true, + se1: true, + se2: true, + se3: true, + se4: true, + status: true, + }, + types: { + version: true, + build_id: true, + hash: true, + specific: true, + }, + }, + { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS } + ); + }); + + test('reads Protocol V2 SE version fields for versions scope', async () => { + const typedCall = jest.fn().mockResolvedValueOnce({ + type: 'DeviceInfo', + message: { + hw: { serial_no: 'PR2SERIAL' }, + se1: { + application: { version: '1.0.1' }, + bootloader: { version: '1.0.0' }, + }, + se2: { + application: { version: '2.0.1' }, + bootloader: { version: '2.0.0' }, + }, + status: { init_states: true }, + }, + }); + const method = new GetDeviceInfo({ + id: 1, + payload: { + method: 'getDeviceInfo', + scope: 'versions', + }, + }); + method.init(); + (method as any).device = stubDevice({ + originalDescriptor: { ...descriptor, protocolType: 'V2' }, + features: normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any), + commands: { typedCall }, + }); + + const result = await method.run(); + + expect(typedCall).toHaveBeenCalledWith( + 'DeviceInfoGet', + 'DeviceInfo', + { + targets: { + hw: true, + fw: true, + coprocessor: true, + se1: true, + se2: true, + se3: true, + se4: true, + status: true, + }, + types: { + version: true, + specific: true, + }, + }, + { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS } + ); + expect(result.versions).toMatchObject({ + se01: '1.0.1', + se01Boot: '1.0.0', + se02: '2.0.1', + se02Boot: '2.0.0', + }); + expect(result).not.toHaveProperty('verify'); + }); + + test('does not inherit Pro or Pro model fallback ranges for Protocol V2 devices', () => { + const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any); + const checkedTypes: string[] = []; + + const versionRange = getMethodVersionRange(features, type => { + checkedTypes.push(type); + if (type === 'pro' || type === 'model_touch') { + return { min: '4.10.0' }; + } + return undefined; + }); + + expect(versionRange).toBeUndefined(); + expect(checkedTypes).toEqual(['pro2']); + }); + + test('uses firmware-v1 as the Pro2 remote firmware config field', () => { + const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any); + + expect( + getFirmwareUpdateField({ + features, + updateType: 'firmware', + firmwareType: 'universal', + }) + ).toBe('firmware-v1'); + expect(getFirmwareUpdateFieldArray(features, 'firmware')).toEqual(['firmware-v1']); + }); + + test('marks known unsupported public-chain methods as unsupported on Protocol V2', () => { + const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, { + fw: { + application: { + version: '9.9.9', + }, + }, + }); + const stellar = new StellarGetAddress({ + id: 1, + payload: { + method: 'stellarGetAddress', + path: "m/44'/148'/0'", + }, + }); + const benfen = new BenfenSignMessage({ + id: 1, + payload: { + method: 'benfenSignMessage', + path: "m/44'/728'/0'/0'/0'", + messageHex: '0x1234', + }, + }); + const lnurlAuth = new LnurlAuth({ + id: 1, + payload: { + method: 'lnurlAuth', + domain: 'example.com', + k1: '1234', + }, + }); + + stellar.init(); + benfen.init(); + lnurlAuth.init(); + + const stellarRange = getMethodVersionRange(features, type => stellar.getVersionRange()[type]); + const benfenRange = getMethodVersionRange(features, type => benfen.getVersionRange()[type]); + const lnurlAuthRange = getMethodVersionRange( + features, + type => lnurlAuth.getVersionRange()[type] + ); + const neuraiRange = getMethodVersionRange( + features, + type => getBitcoinForkVersionRange(['Neurai'])[type] + ); + + expect(isMethodVersionRangeUnsupported(stellarRange)).toBe(true); + expect(isMethodVersionRangeUnsupported(benfenRange)).toBe(true); + expect(isMethodVersionRangeUnsupported(lnurlAuthRange)).toBe(true); + expect(isMethodVersionRangeUnsupported(neuraiRange)).toBe(true); + }); + + test('does not block batch public key support checks on Protocol V2', async () => { + const paths = [{ address_n: [0x8000002c, 0x80000000, 0x80000000] }] as any; + const typedCall = jest.fn().mockResolvedValue({ + type: 'EcdsaPublicKeys', + message: { + root_fingerprint: 123, + public_keys: [], + hd_nodes: [{}], + }, + }); + const device = stubDevice({ + originalDescriptor: { protocolType: 'V2' }, + features: normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, { + fw: { + application: { + version: '4.14.0', + }, + }, + }), + commands: { typedCall }, + getCurrentDeviceType: () => 'pro2', + }); + + await expect( + batchGetPublickeys(device as any, paths, 'secp256k1', 0, { includeNode: true }) + ).resolves.toMatchObject({ + root_fingerprint: 123, + hd_nodes: [{}], + }); + expect(typedCall).toHaveBeenCalledWith('BatchGetPublickeys', 'EcdsaPublicKeys', { + paths, + ecdsa_curve_name: 'secp256k1', + include_node: true, + }); + }); + + test('does not call V1 OnekeyGetFeatures for Protocol V2 devices', async () => { + const method = new GetOnekeyFeatures({ + id: 1, + payload: { + method: 'getOnekeyFeatures', + }, + }); + const typedCall = jest.fn().mockResolvedValue({ + type: 'DeviceInfo', + message: { + protocol_version: 1, + hw: { serial_no: 'PR2SERIAL' }, + fw: { application: { version: '1.2.3', build_id: 'app-build' } }, + coprocessor: { bt_adv_name: 'Pro2 BLE' }, + status: { init_states: true }, + }, + }); + + (method as any).device = stubDevice({ + originalDescriptor: { protocolType: 'V2' }, + commands: { typedCall }, + }); + + const message = await method.run(); + + // V2 走 DeviceInfoGet 完整请求(含 SE 与 hash/build_id),而不是 V1 OnekeyGetFeatures + expect(typedCall).toHaveBeenCalledTimes(1); + expect(typedCall).toHaveBeenCalledWith( + 'DeviceInfoGet', + 'DeviceInfo', + expect.objectContaining({ + targets: expect.objectContaining({ se1: true, se2: true, se3: true, se4: true }), + types: expect.objectContaining({ build_id: true, hash: true }), + }), + expect.anything() + ); + expect(message).toEqual({}); + expect(message).not.toHaveProperty('label'); + }); + + test('refreshes cached Protocol V2 features with a lightweight status request on later runs', async () => { + const device = Device.fromDescriptor({ + path: 'usb-path', + protocolType: 'V2', + } as any); + const typedCall = jest + .fn() + .mockResolvedValueOnce({ + type: 'DeviceInfo', + message: { + hw: { serial_no: 'PR2SERIAL' }, + fw: { application: { version: '1.2.3' } }, + status: { + device_id: 'PRO2-REFRESH-ID', + passphrase_enabled: true, + }, + }, + }) + // 第二次 run 的轻量 status 刷新:设备端 label / init_states 等可能已变化 + .mockResolvedValueOnce({ + type: 'DeviceInfo', + message: { + hw: { serial_no: 'PR2SERIAL' }, + status: { + device_id: 'PRO2-REFRESH-ID', + passphrase_enabled: false, + }, + }, + }); + + (device as any).commands = { typedCall }; + + await device.initialize(); + await device.initialize(); + + expect(device.features).toMatchObject({ + deviceId: 'PRO2-REFRESH-ID', + firmwareVersion: '1.2.3', + passphraseProtection: false, + label: null, + }); + expect((device as any).profile).toBeUndefined(); + // status 字段被第二次刷新更新 + expect(device.features?.passphraseProtection).toBe(false); + expect(device.features?.label).toBeNull(); + // 轻量刷新不含 fw target,已有版本信息按字段级合并保留 + expect(device.features?.firmwareVersion).toBe('1.2.3'); + expect(typedCall).toHaveBeenCalledTimes(2); + expect(typedCall).toHaveBeenNthCalledWith( + 1, + 'DeviceInfoGet', + 'DeviceInfo', + { + targets: { + hw: true, + fw: true, + coprocessor: true, + status: true, + }, + types: { + version: true, + specific: true, + }, + }, + { + timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS, + } + ); + // 第二次为 status-only 轻量请求(hw/coprocessor 仅用于身份字段,避免顶层覆盖清空) + expect(typedCall).toHaveBeenNthCalledWith( + 2, + 'DeviceInfoGet', + 'DeviceInfo', + { + targets: { + hw: true, + coprocessor: true, + status: true, + }, + types: { + version: true, + specific: true, + }, + }, + { + timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS, + } + ); + }); + + test('refreshes Protocol V2 features without falling back to V1 GetFeatures', async () => { + const device = Device.fromDescriptor({ + path: 'usb-path', + protocolType: 'V2', + } as any); + const typedCall = jest + .fn() + .mockResolvedValueOnce({ + type: 'DeviceInfo', + message: { + hw: { serial_no: 'PR2SERIAL' }, + fw: { application: { version: '1.2.3' } }, + status: { init_states: true }, + }, + }) + .mockResolvedValueOnce({ + type: 'DeviceInfo', + message: { + hw: { serial_no: 'PR2SERIAL' }, + fw: { application: { version: '1.2.4' } }, + status: { init_states: true, passphrase_enabled: true }, + }, + }); + + (device as any).commands = { typedCall }; + + await device.initialize(); + const features = await device.getFeatures(); + + expect(device.features).toMatchObject({ + deviceId: null, + firmwareVersion: '1.2.4', + passphraseProtection: true, + }); + expect(features).toMatchObject({ + deviceType: 'pro2', + serialNo: 'PR2SERIAL', + firmwareVersion: '1.2.4', + passphraseProtection: true, + }); + expect(typedCall).toHaveBeenCalledTimes(2); + expect(typedCall).toHaveBeenNthCalledWith( + 2, + 'DeviceInfoGet', + 'DeviceInfo', + { + targets: { + hw: true, + fw: true, + coprocessor: true, + status: true, + }, + types: { + version: true, + specific: true, + }, + }, + { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS } + ); + expect(typedCall).not.toHaveBeenCalledWith('GetFeatures', 'Features', {}); + }); + + test('keeps Protocol V2 features available for method internals such as evmSignTypedData', async () => { + const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any); + const typedCall = jest.fn().mockResolvedValueOnce({ + type: 'DeviceInfo', + message: { + hw: { serial_no: 'PR2SERIAL' }, + fw: { application: { version: '1.2.4' } }, + status: { init_states: true, passphrase_enabled: true }, + }, + }); + (device as any).commands = { typedCall }; + + await device.initialize(); + expect(device.features).toBeDefined(); + + const method = new EVMSignTypedData({ + id: 1, + payload: { + method: 'evmSignTypedData', + path: "m/44'/60'/0'/0/0", + metamaskV4Compat: true, + data: { + types: { + EIP712Domain: [], + Mail: [{ name: 'contents', type: 'string' }], + }, + primaryType: 'Mail', + domain: {}, + message: { contents: 'hello' }, + }, + }, + }); + method.init(); + (method as any).device = stubDevice({ + features: device.features, + commands: { typedCall: jest.fn() }, + }); + (method as any).signTypedData = jest + .fn() + .mockResolvedValue({ address: '0x0', signature: '0x1' }); + + await expect(method.run()).resolves.toEqual({ address: '0x0', signature: '0x1' }); + }); + + test('unlocks Protocol V2 devices via DeviceSessionAskPin regardless of Pro-series version gates', async () => { + const device = Device.fromDescriptor({ + path: 'usb-path', + protocolType: 'V2', + } as any); + const typedCall = jest.fn().mockImplementation(requestType => { + if (requestType === 'DeviceSessionAskPin') { + return { + type: 'Success', + message: { message: 'ok' }, + }; + } + if (requestType === 'DeviceStatusGet') { + return { + type: 'DeviceStatus', + message: { + unlocked: true, + unlocked_by_attach_to_pin: true, + passphrase_enabled: true, + }, + }; + } + throw new Error(`Unexpected request: ${requestType}`); + }); + + (device as any).commands = { typedCall }; + // Pro2 版本线独立于 Pro(这里是 1.2.3,不满足 Pro 系列 4.15.0 门槛), + // Protocol V2 走独立的设备端 PIN 解锁流程,不走 Pro 系列版本门槛或 GetAddress 探测回退。 + (device as any).features = normalizeProtocolV2Features( + { ...descriptor, protocolType: 'V2' } as any, + { + hw: { serial_no: 'PR2SERIAL' }, + fw: { application: { version: '1.2.3' } }, + status: { init_states: true }, + } + ); + + const features = await device.unlockDevice(); + + expect(typedCall.mock.calls).toEqual([ + ['DeviceSessionAskPin', 'Success'], + ['DeviceStatusGet', 'DeviceStatus', {}], + ]); + expect(typedCall).not.toHaveBeenCalledWith('GetAddress', 'Address', expect.anything()); + expect(typedCall).not.toHaveBeenCalledWith('GetFeatures', 'Features', {}); + expect(features).toMatchObject({ + deviceType: 'pro2', + firmwareVersion: '1.2.3', + unlocked: true, + unlockedAttachPin: true, + passphraseProtection: true, + }); + expect(features.raw?.protocolV2DeviceInfo?.status).toMatchObject({ + unlocked: true, + unlocked_by_attach_to_pin: true, + passphrase_enabled: true, + }); + }); + + test('syncs Protocol V2 features passphrase state from DeviceStatus after unlock', async () => { + const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any); + (device as any).features = normalizeProtocolV2Features( + { ...descriptor, protocolType: 'V2' } as any, + { + hw: { serial_no: 'PR2SERIAL' }, + fw: { application: { version: '4.15.0' } }, + status: { passphrase_enabled: false }, + } + ); + const typedCall = jest.fn().mockImplementation(requestType => { + if (requestType === 'DeviceSessionAskPin') { + return { + type: 'Success', + message: { message: 'ok' }, + }; + } + if (requestType === 'DeviceStatusGet') { + return { + type: 'DeviceStatus', + message: { + unlocked: true, + unlocked_by_attach_to_pin: true, + passphrase_enabled: true, + }, + }; + } + throw new Error(`Unexpected request: ${requestType}`); + }); + (device as any).commands = { typedCall }; + + await device.unlockDevice(); + + expect(typedCall.mock.calls).toEqual([ + ['DeviceSessionAskPin', 'Success'], + ['DeviceStatusGet', 'DeviceStatus', {}], + ]); + expect((device as any).profile).toBeUndefined(); + expect(device.features?.unlocked).toBe(true); + expect(device.features?.passphraseProtection).toBe(true); + expect(device.features?.unlockedAttachPin).toBe(true); + }); + + test('maps unsupported Protocol V2 DeviceSessionAskPin to DeviceNotSupportMethod', async () => { + const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any); + (device as any).features = normalizeProtocolV2Features( + { ...descriptor, protocolType: 'V2' } as any, + { + fw: { application: { version: '1.2.3' } }, + status: { unlocked: false }, + } + ); + const typedCall = jest + .fn() + .mockRejectedValue(new Error('Failure_UnexpectedMessage,Unknown message')); + (device as any).commands = { typedCall }; + + await expect(device.unlockDevice()).rejects.toEqual( + expect.objectContaining({ + errorCode: HardwareErrorCode.DeviceNotSupportMethod, + }) + ); + expect(typedCall).toHaveBeenCalledTimes(1); + expect(typedCall.mock.calls).toEqual([['DeviceSessionAskPin', 'Success']]); + expect(typedCall).not.toHaveBeenCalledWith( + 'DeviceSessionGet', + 'DeviceSession', + expect.anything() + ); + }); + + test('keeps Protocol V1 unlock response handling unchanged', async () => { + const device = Device.fromDescriptor({ path: 'v1-path', protocolType: 'V1' } as any); + (device as any).features = { + deviceType: 'pro', + firmwareVersion: '4.15.0', + capabilities: [], + unlocked: false, + passphraseProtection: false, + } as Features; + const typedCall = jest.fn().mockResolvedValue({ + type: 'UnLockDeviceResponse', + message: { + unlocked: true, + unlocked_attach_pin: true, + passphrase_protection: true, + }, + }); + (device as any).commands = { typedCall }; + + await expect(device.unlockDevice()).resolves.toMatchObject({ + unlocked: true, + unlockedAttachPin: true, + passphraseProtection: true, + }); + expect(typedCall.mock.calls).toEqual([['UnLockDevice', 'UnLockDeviceResponse']]); + }); +}); + +describe('API compatibility handling', () => { + test('returns a typed unsupported error for deprecated EIP712 message signing on Protocol V2', async () => { + const method = new EVMSignMessageEIP712({ + id: 1, + payload: { + method: 'evmSignMessageEIP712', + path: "m/44'/60'/0'/0/0", + domainHash: '0x'.concat('11'.repeat(32)), + messageHash: '0x'.concat('22'.repeat(32)), + }, + }); + + method.init(); + (method as any).device = stubDevice({ + features: { + deviceType: 'pro2', + }, + originalDescriptor: { + protocolType: 'V2', + }, + getCurrentFirmwareVersionString: () => '4.15.0', + getCurrentMethodVersionRange: (selectRange: (deviceType: string) => unknown) => + selectRange('pro2'), + getCurrentFirmwareType: () => 'universal', + }); + + await expect(method.run()).rejects.toEqual( + expect.objectContaining({ + errorCode: HardwareErrorCode.DeviceNotSupportMethod, + }) + ); + }); + + test('does not mark supported Pro2 Tron, Solana, TON, SUI and Polkadot methods as unsupported', () => { + const features = { + deviceType: 'pro2', + } as Features; + + const tronMethod = new TronGetAddress({ + id: 1, + payload: { + method: 'tronGetAddress', + path: "m/44'/195'/0'/0/0", + showOnOneKey: false, + }, + }); + const solMethod = new SolGetAddress({ + id: 2, + payload: { + method: 'solGetAddress', + path: "m/44'/501'/0'/0'", + showOnOneKey: false, + }, + }); + const tonGetAddressMethod = new TonGetAddress({ + id: 3, + payload: { + method: 'tonGetAddress', + path: "m/44'/607'/0'", + showOnOneKey: false, + }, + }); + const tonSignMessageMethod = new TonSignMessage({ + id: 4, + payload: { + method: 'tonSignMessage', + path: "m/44'/607'/0'", + }, + }); + const tonSignProofMethod = new TonSignProof({ + id: 5, + payload: { + method: 'tonSignProof', + path: "m/44'/607'/0'", + }, + }); + const tonSignDataMethod = new TonSignData({ + id: 6, + payload: { + method: 'tonSignData', + path: "m/44'/607'/0'", + }, + }); + const suiGetAddressMethod = new SuiGetAddress({ + id: 7, + payload: { + method: 'suiGetAddress', + path: "m/44'/784'/0'/0'/0'", + showOnOneKey: false, + }, + }); + const suiGetPublicKeyMethod = new SuiGetPublicKey({ + id: 8, + payload: { + method: 'suiGetPublicKey', + path: "m/44'/784'/0'/0'/0'", + }, + }); + const suiSignMessageMethod = new SuiSignMessage({ + id: 9, + payload: { + method: 'suiSignMessage', + path: "m/44'/784'/0'/0'/0'", + messageHex: '0x1234', + }, + }); + const suiSignTransactionMethod = new SuiSignTransaction({ + id: 10, + payload: { + method: 'suiSignTransaction', + path: "m/44'/784'/0'/0'/0'", + rawTx: '0x1234', + }, + }); + const polkadotGetAddressMethod = new PolkadotGetAddress({ + id: 11, + payload: { + method: 'polkadotGetAddress', + path: "m/44'/354'/0'/0'/0'", + prefix: 0, + network: 'polkadot', + showOnOneKey: false, + }, + }); + + polkadotGetAddressMethod.init(); + + expect( + isMethodVersionRangeUnsupported( + getMethodVersionRange(features, type => tronMethod.getVersionRange()[type]) + ) + ).toBe(false); + expect( + isMethodVersionRangeUnsupported( + getMethodVersionRange(features, type => solMethod.getVersionRange()[type]) + ) + ).toBe(false); + expect( + getMethodVersionRange(features, type => tonGetAddressMethod.getVersionRange()[type]) + ).toEqual({ + min: '0.0.0', + }); + expect( + getMethodVersionRange(features, type => tonSignMessageMethod.getVersionRange()[type]) + ).toEqual({ + min: '0.0.0', + }); + expect( + getMethodVersionRange(features, type => tonSignProofMethod.getVersionRange()[type]) + ).toEqual({ + min: '0.0.0', + }); + expect( + getMethodVersionRange(features, type => tonSignDataMethod.getVersionRange()[type]) + ).toBeUndefined(); + expect( + getMethodVersionRange(features, type => suiGetAddressMethod.getVersionRange()[type]) + ).toEqual({ + min: '0.0.0', + }); + expect( + getMethodVersionRange(features, type => suiGetPublicKeyMethod.getVersionRange()[type]) + ).toEqual({ + min: '0.0.0', + }); + expect( + getMethodVersionRange(features, type => suiSignMessageMethod.getVersionRange()[type]) + ).toEqual({ + min: '0.0.0', + }); + expect( + getMethodVersionRange(features, type => suiSignTransactionMethod.getVersionRange()[type]) + ).toEqual({ + min: '0.0.0', + }); + expect( + getMethodVersionRange(features, type => polkadotGetAddressMethod.getVersionRange()[type]) + ).toEqual({ + min: '0.0.0', + }); + }); + + test('allows Pro2 Solana signing methods through Protocol V2 version checks', () => { + const features = { + deviceType: 'pro2', + } as Features; + + const solSignMessageMethod = new SolSignMessage({ + id: 1, + payload: { + method: 'solSignMessage', + path: "m/44'/501'/0'/0'", + messageHex: '48656c6c6f', + }, + }); + const solSignOffchainMessageMethod = new SolSignOffchainMessage({ + id: 2, + payload: { + method: 'solSignOffchainMessage', + path: "m/44'/501'/0'/0'", + messageHex: '48656c6c6f', + }, + }); + const solSignTransactionMethod = new SolSignTransaction({ + id: 3, + payload: { + method: 'solSignTransaction', + path: "m/44'/501'/0'/0'", + rawTx: '00', + }, + }); + const solSignVersionedTransactionMethod = new SolSignTransaction({ + id: 4, + payload: { + method: 'solSignTransaction', + path: "m/44'/501'/0'/0'", + rawTx: '80', + }, + }); + + solSignTransactionMethod.init(); + solSignVersionedTransactionMethod.init(); + + expect( + getMethodVersionRange(features, type => solSignMessageMethod.getVersionRange()[type]) + ).toEqual({ + min: '0.0.0', + }); + expect( + getMethodVersionRange(features, type => solSignOffchainMessageMethod.getVersionRange()[type]) + ).toEqual({ + min: '0.0.0', + }); + expect( + getMethodVersionRange(features, type => solSignTransactionMethod.getVersionRange()[type]) + ).toEqual({ + min: '0.0.0', + }); + expect( + getMethodVersionRange( + features, + type => solSignVersionedTransactionMethod.getVersionRange()[type] + ) + ).toEqual({ + min: '0.0.0', + }); + }); + + test('uses chunk transfer for large Sui transactions on Protocol V2', async () => { + const rawTx = '0x'.concat('ab'.repeat(5000)); + const typedCall = jest.fn(() => ({ + type: 'SuiSignedTx', + message: { + public_key: '', + signature: '', + }, + })); + const method = new SuiSignTransaction({ + id: 1, + payload: { + method: 'suiSignTransaction', + path: "m/44'/784'/0'/0'/0'", + rawTx, + }, + }); + + method.init(); + (method as any).device = stubDevice({ + features: { + deviceType: 'pro2', + }, + originalDescriptor: { + protocolType: 'V2', + }, + getCommands: () => ({ + typedCall, + }), + }); + + await method.run(); + + expect(typedCall).toHaveBeenCalledTimes(1); + const [, , params] = typedCall.mock.calls[0]; + expect(params).toEqual( + expect.objectContaining({ + raw_tx: '', + data_length: 5000, + }) + ); + expect(params.data_initial_chunk).toHaveLength(2048); + }); + + test('returns a typed unsupported error for Dynex signing on Protocol V2', async () => { + const method = new DnxSignTransaction({ + id: 1, + payload: { + method: 'dnxSignTransaction', + path: "m/44'/29538'/0'/0/0", + inputs: [ + { + prevIndex: 1, + globalIndex: 1, + txPubkey: '00', + prevOutPubkey: '00', + amount: '1', + }, + ], + toAddress: 'dnx-address', + amount: '1', + fee: '1', + }, + }); + + method.init(); + (method as any).device = stubDevice({ + originalDescriptor: { protocolType: 'V2' }, + features: normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any), + }); + + await expect(method.run()).rejects.toMatchObject({ + errorCode: HardwareErrorCode.DeviceNotSupportMethod, + }); + }); + + test('returns a typed unsupported error for Dynex address on Protocol V2', async () => { + const method = new DnxGetAddress({ + id: 1, + payload: { + method: 'dnxGetAddress', + path: "m/44'/29538'/0'/0/0", + showOnOneKey: false, + }, + }); + + method.init(); + (method as any).device = stubDevice({ + originalDescriptor: { protocolType: 'V2' }, + features: normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any), + }); + + await expect(method.run()).rejects.toMatchObject({ + errorCode: HardwareErrorCode.DeviceNotSupportMethod, + }); + }); +}); + +describe('Protocol V2 firmware update targets', () => { + const OKPP_HEADER_SIZE = 0x52a0; + + const buildOkppHeader = ({ + type = 'RESC', + version = [1, 0, 0], + payloadHash = 'ab'.repeat(64), + headerHash = 'cd'.repeat(64), + }: { + type?: string; + version?: [number, number, number]; + payloadHash?: string; + headerHash?: string; + } = {}) => { + const header = new Uint8Array(OKPP_HEADER_SIZE); + const view = new DataView(header.buffer); + 'OKPP'.split('').forEach((char, index) => { + header[index] = char.charCodeAt(0); + }); + view.setUint32(0x04, 1, true); + type.split('').forEach((char, index) => { + header[0x08 + index] = char.charCodeAt(0); + }); + view.setUint32(0x0c, OKPP_HEADER_SIZE, true); + view.setUint32(0x10, version[0] * 0x10000 + version[1] * 0x100 + version[2], true); + const writeHex = (offset: number, hex: string) => { + for (let i = 0; i < hex.length / 2; i++) { + header[offset + i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16); + } + }; + writeHex(0x200, payloadHash); + writeHex(0x240, headerHash); + return header; + }; + + test('keeps Protocol V2 firmware updates off the firmwareUpdateV3 path', async () => { + const method = new FirmwareUpdateV3({ + id: 1, + payload: { + method: 'firmwareUpdateV3', + }, + }); + (method as any).device = stubDevice({ + originalDescriptor: { protocolType: 'V2' }, + }); + + await expect(method.run()).rejects.toThrow('firmwareUpdateV4'); + }); + + test('uses Protocol V2 features after BLE final reconnect without V1 Initialize', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + coprocessorBinary: new Uint8Array([1, 2, 3]).buffer, + }, + }); + const acquire = jest.fn().mockResolvedValue({ uuid: 'ble-session' }); + const typedCall = jest.fn().mockImplementation((name: string) => { + if (name === 'DeviceInfoGet') { + return Promise.resolve({ + type: 'DeviceInfo', + message: { + fw: { + bootloader: { version: '0.0.0' }, + application: { version: '0.0.0' }, + }, + coprocessor: { + application: { version: '0.0.0' }, + }, + }, + }); + } + return Promise.reject(new Error(`unexpected call ${name}`)); + }); + const commands = { typedCall }; + + (method as any).isBleReconnect = jest.fn(() => true); + (method as any).device = stubDevice({ + originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' }, + deviceConnector: { acquire }, + getCommands: () => commands, + updateProtocolV2Features: jest.fn(() => ({ + bootloaderMode: false, + mode: 'normal', + firmwareVersion: '0.0.0', + bootloaderVersion: '0.0.0', + bleVersion: '0.0.0', + })), + }); + + const versions = await (method as any).waitForProtocolV2FinalFeatures(); + + expect(acquire).toHaveBeenCalledWith('ble-id', null, true, 'V2'); + // 更新完成判定使用 VERSIONS 请求(含 SE targets),scope 与请求内容一致 + expect(typedCall).toHaveBeenNthCalledWith( + 1, + 'DeviceInfoGet', + 'DeviceInfo', + { + targets: { + hw: true, + fw: true, + coprocessor: true, + se1: true, + se2: true, + se3: true, + se4: true, + status: true, + }, + types: { + version: true, + specific: true, + }, + }, + { timeoutMs: 5000 } + ); + expect(typedCall).toHaveBeenCalledTimes(1); + expect(typedCall).not.toHaveBeenCalledWith('Initialize', 'Features', {}); + expect(versions).toEqual({ + bootloaderVersion: '0.0.0', + bleVersion: '0.0.0', + firmwareVersion: '0.0.0', + }); + }); + + test('enters Protocol V2 bootloader before upload', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + }, + }); + method.init(); + + (method as any).device = stubDevice({ + originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' }, + features: { capabilities: [] }, + }); + (method as any).prepareBootloaderBinary = jest.fn().mockReturnValue(null); + (method as any).collectExplicitTargetBinaries = jest.fn().mockReturnValue([ + { + fileName: 'coprocessor.bin', + binary: new Uint8Array([1, 2, 3]).buffer, + targetId: 5, + }, + ]); + (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined); + (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined); + (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({ + bootloaderVersion: '0.2.0', + bleVersion: '4.5.6', + firmwareVersion: '1.2.3', + }); + (method as any).protocolV2Reboot = jest.fn(); + (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true); + method.postTipMessage = jest.fn(); + + await method.run(); + + expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1); + expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({ + fwBinaryMap: [ + { + fileName: 'coprocessor.bin', + binary: expect.any(ArrayBuffer), + targetId: 5, + }, + ], + bootloaderBinary: null, + }); + }); + + test('reboots Protocol V2 normal-mode device to bootloader before transfer', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + }, + }); + (method as any).device = stubDevice({ + originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' }, + features: { bootloader_mode: false, capabilities: [] }, + isBootloader: () => false, + }); + (method as any).protocolV2Reboot = jest.fn().mockResolvedValue({ + message: 'Device rebooted successfully', + }); + (method as any).checkDeviceToBootloader = jest.fn(); + const typedCall = jest.fn().mockResolvedValue({ + type: 'DeviceInfo', + message: protocolV2BootloaderDeviceInfo, + }); + const reconnectProtocolV2Device = jest.fn().mockImplementation(() => { + (method as any).device.isBootloader = () => true; + return Promise.resolve(); + }); + (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device; + (method as any).device.getCommands = () => ({ typedCall }); + method.postTipMessage = jest.fn(); + + await (method as any).enterProtocolV2BootloaderMode(); + + expect(method.postTipMessage).toHaveBeenCalledWith('AutoRebootToBootloader'); + expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DeviceRebootType.Bootloader); + expect(method.postTipMessage).toHaveBeenCalledWith('GoToBootloaderSuccess'); + expect((method as any).checkDeviceToBootloader).not.toHaveBeenCalled(); + expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1); + }); + + test('polls until Protocol V2 bootloader descriptor is ready after reboot', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + }, + }); + (method as any).device = stubDevice({ + originalDescriptor: { id: 'usb-id', path: 'app-path', protocolType: 'V2' }, + features: { bootloader_mode: false, capabilities: [] }, + isBootloader: () => false, + }); + const typedCall = jest.fn().mockResolvedValue({ + type: 'DeviceInfo', + message: protocolV2BootloaderDeviceInfo, + }); + const reconnectProtocolV2Device = jest + .fn() + .mockRejectedValueOnce(new Error('Device not found')) + .mockRejectedValueOnce(new Error('Device not found')) + .mockImplementation(() => { + (method as any).device.isBootloader = () => true; + return Promise.resolve(); + }); + (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device; + (method as any).device.getCommands = () => ({ typedCall }); + + await (method as any).waitForProtocolV2BootloaderMode(60 * 1000, 0); + + expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(3); + expect(typedCall).toHaveBeenCalledTimes(1); + }); + + test('reboots Protocol V2 firmware flow back to normal after install status is finished', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + }, + }); + method.postTipMessage = jest.fn(); + (method as any).protocolV2Reboot = jest.fn().mockResolvedValue({ + message: 'Device rebooted successfully', + }); + + await (method as any).exitProtocolV2BootloaderToNormal(); + + expect(method.postTipMessage).toHaveBeenCalledWith('SwitchFirmwareReconnectDevice'); + expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DeviceRebootType.Normal); + }); + + test('treats iOS BLE RxError 6 during Protocol V2 reboot as expected disconnect', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + }, + }); + const typedCall = jest + .fn() + .mockRejectedValue( + new Error("The operation couldn't be completed. (MultiplatformBleAdapter.RxError error 6.)") + ); + + (method as any).device = stubDevice({ + getCommands: () => ({ typedCall }), + }); + + await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({ + message: 'Device rebooted successfully', + }); + }); + + test('treats direct disconnect during Protocol V2 normal reboot as expected', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + }, + }); + const typedCall = jest + .fn() + .mockRejectedValue(new Error('Connection error has occured: Device disconnected')); + + (method as any).device = stubDevice({ + getCommands: () => ({ typedCall }), + }); + + await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({ + message: 'Device rebooted successfully', + }); + }); + + test('treats WebUSB open NotFoundError during Protocol V2 firmware update as expected disconnect', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + }, + }); + const typedCall = jest + .fn() + .mockRejectedValue( + new DOMException( + "Failed to execute 'open' on 'USBDevice': The device was disconnected", + 'NotFoundError' + ) + ); + + (method as any).device = stubDevice({ + getCommands: () => ({ typedCall }), + }); + method.postTipMessage = jest.fn(); + + await expect( + (method as any).protocolV2StartFirmwareUpdate({ + targets: [{ target_id: 4, path: 'vol1:firmware.bin' }], + }) + ).resolves.toBeUndefined(); + + expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating'); + }); + + test('continues Protocol V2 install polling when update request transfer times out', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + }, + }); + const typedCall = jest.fn().mockRejectedValue(new Error('LIBUSB_TRANSFER_TIMED_OUT')); + + (method as any).device = stubDevice({ + getCommands: () => ({ typedCall }), + }); + method.postTipMessage = jest.fn(); + + await expect( + (method as any).protocolV2StartFirmwareUpdate({ + targets: [{ target_id: 4, path: 'vol1:application_p1.bin' }], + }) + ).resolves.toBeUndefined(); + + expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating'); + }); + + test('continues Protocol V2 install polling through temporary expected V2 probe failures', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + }, + }); + const typedCall = jest.fn().mockImplementation((name: string) => { + if (name === 'DeviceFirmwareUpdateStatusGet') { + const callCount = typedCall.mock.calls.filter(call => call[0] === name).length; + if (callCount === 1) { + return Promise.reject( + new Error( + 'Device protocol mismatch: expected V2, but device did not respond to expected protocol' + ) + ); + } + return Promise.resolve({ + type: 'DeviceFirmwareUpdateStatus', + message: { + records: [{ target_id: 6, status: 2 }], + }, + }); + } + if (name === 'DeviceInfoGet') { + return Promise.reject(new Error('DeviceInfo not ready')); + } + if (name === 'Ping') { + return Promise.resolve({ type: 'Success', message: { message: 'ready' } }); + } + return Promise.reject(new Error(`unexpected call ${name}`)); + }); + const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined); + + (method as any).device = stubDevice({ + getCommands: () => ({ typedCall }), + }); + (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device; + method.postProgressMessage = jest.fn(); + + await (method as any).waitForProtocolV2FirmwareUpdateComplete([ + { target_id: 6, path: 'vol1:ble-firmware.bin' }, + ]); + + expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1); + expect(typedCall.mock.calls.map(call => call[0])).toEqual([ + 'DeviceFirmwareUpdateStatusGet', + 'DeviceInfoGet', + 'Ping', + 'DeviceFirmwareUpdateStatusGet', + ]); + }); + + test('treats Protocol V2 normal mode after reconnect as firmware update complete', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + }, + }); + const typedCall = jest.fn().mockImplementation((name: string) => { + if (name === 'DeviceFirmwareUpdateStatusGet') { + return Promise.reject(new Error('Device disconnected')); + } + if (name === 'DeviceInfoGet') { + return Promise.resolve({ + type: 'DeviceInfo', + message: { + hw: { serial_no: 'PR9999999999' }, + fw: { + bootloader: { version: '9.9.9' }, + application: { version: '9.9.9' }, + }, + status: { + device_id: 'PRO2-DEVICE-ID', + init_states: true, + }, + }, + }); + } + return Promise.reject(new Error(`unexpected call ${name}`)); + }); + const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined); + const updateProtocolV2Features = jest.fn((deviceInfo: ProtocolV2DeviceInfo) => + normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, deviceInfo) + ); + + (method as any).device = stubDevice({ + getCommands: () => ({ typedCall }), + updateProtocolV2Features, + }); + (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device; + method.postProgressMessage = jest.fn(); + + await expect( + (method as any).waitForProtocolV2FirmwareUpdateComplete([ + { target_id: 3, path: 'vol1:bootloader.bin' }, + { target_id: 4, path: 'vol1:application_p1.bin' }, + ]) + ).resolves.toBeUndefined(); + + expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1); + expect(updateProtocolV2Features).toHaveBeenCalledTimes(1); + expect(typedCall.mock.calls.map(call => call[0])).toEqual([ + 'DeviceFirmwareUpdateStatusGet', + 'DeviceInfoGet', + ]); + }); + + test('uses SDK decoded enum names for Protocol V2 install polling', () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + }, + }); + method.postProgressMessage = jest.fn(); + + const expectedTargetIds = new Set([4]); + + expect( + (method as any).assertProtocolV2TargetStatus([{ target_id: 4, status: 2 }], expectedTargetIds) + ).toBe(true); + expect( + (method as any).assertProtocolV2TargetStatus( + [{ target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED' }], + expectedTargetIds + ) + ).toBe(true); + expect( + (method as any).assertProtocolV2TargetStatus( + [ + { + target_id: 'FW_MGMT_TARGET_APPLICATION_P1', + status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED', + }, + { target_id: 'FW_MGMT_TARGET_SE04', status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED' }, + ], + new Set([4, 10]) + ) + ).toBe(true); + + expect( + (method as any).assertProtocolV2TargetStatus([{ target_id: 4, status: 1 }], expectedTargetIds) + ).toBe(false); + expect( + (method as any).assertProtocolV2TargetStatus( + [{ target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_IN_PROGRESS' }], + expectedTargetIds + ) + ).toBe(false); + expect(method.postProgressMessage).toHaveBeenCalledWith(99, 'installingFirmware'); + + try { + (method as any).assertProtocolV2TargetStatus( + [{ target_id: 4, status: 3 }], + expectedTargetIds + ); + throw new Error('Expected Protocol V2 failed firmware status to throw'); + } catch (error: any) { + expect(error.errorCode).toBe(HardwareErrorCode.FirmwareError); + } + + try { + (method as any).assertProtocolV2TargetStatus( + [{ target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_FAILED_VERIFY' }], + expectedTargetIds + ); + throw new Error('Expected Protocol V2 failed firmware status to throw'); + } catch (error: any) { + expect(error.errorCode).toBe(HardwareErrorCode.FirmwareError); + } + }); + + test('passes bootloader, coprocessor, SE and app files to DeviceFirmwareUpdate targets', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + }, + }); + + const writtenPaths: string[] = []; + method.postTipMessage = jest.fn(); + method.postProgressMessage = jest.fn(); + (method as any).protocolV2CommonUpdateProcess = jest.fn().mockImplementation(params => { + writtenPaths.push(params.filePath); + return Number(params.processedSize ?? 0) + Number(params.payload.byteLength); + }); + (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined); + (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined); + (method as any).waitForProtocolV2FirmwareUpdateComplete = jest + .fn() + .mockResolvedValue(undefined); + + await (method as any).executeProtocolV2Update({ + bootloaderBinary: new Uint8Array([4, 5]).buffer, + fwBinaryMap: [ + { + fileName: 'coprocessor.bin', + binary: new Uint8Array([6]).buffer, + targetId: 6, + }, + { + fileName: 'se01.bin', + binary: new Uint8Array([7]).buffer, + targetId: 7, + }, + { + fileName: 'application_p1.bin', + binary: new Uint8Array([8]).buffer, + targetId: 4, + }, + ], + }); + + expect(writtenPaths).toEqual([ + 'vol0:/bootloader.bin', + 'vol0:/coprocessor.bin', + 'vol0:/se01.bin', + 'vol0:/application_p1.bin', + ]); + expect((method as any).verifyProtocolV2StagedFile).toHaveBeenCalledTimes(4); + expect((method as any).verifyProtocolV2StagedFile).toHaveBeenNthCalledWith( + 2, + 'vol0:/coprocessor.bin', + 1 + ); + expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledTimes(1); + expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({ + targets: [ + { target_id: 3, path: 'vol0:/bootloader.bin' }, + { target_id: 6, path: 'vol0:/coprocessor.bin' }, + { target_id: 7, path: 'vol0:/se01.bin' }, + { target_id: 4, path: 'vol0:/application_p1.bin' }, + ], + }); + expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'transferData'); + expect((method as any).waitForProtocolV2FirmwareUpdateComplete).toHaveBeenCalled(); + }); + + test('announces one transfer when syncing resource bundles and staging firmware', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + }, + }); + + method.postTipMessage = jest.fn(); + method.postProgressMessage = jest.fn(); + (method as any).protocolV2CommonUpdateProcess = jest + .fn() + .mockImplementation(params => + Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength)) + ); + (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined); + (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined); + (method as any).waitForProtocolV2FirmwareUpdateComplete = jest + .fn() + .mockResolvedValue(undefined); + + await (method as any).executeProtocolV2Update({ + resourceBundles: [ + { + name: 'images.okpkg', + binary: new Uint8Array([1, 2]).buffer, + devicePath: 'vol0:/bundles/images/images.okpkg', + }, + ], + bootloaderBinary: null, + fwBinaryMap: [ + { + fileName: 'application_p1.bin', + binary: new Uint8Array([3]).buffer, + targetId: 3, + }, + ], + }); + + expect(method.postTipMessage).toHaveBeenCalledTimes(2); + expect(method.postTipMessage).toHaveBeenNthCalledWith(1, 'StartTransferData'); + expect(method.postTipMessage).toHaveBeenNthCalledWith(2, 'ConfirmOnDevice'); + expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ processedSize: 0, totalSize: 3 }) + ); + expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ processedSize: 2, totalSize: 3 }) + ); + }); + + test('does not request installation when the staged file size does not match', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + }, + }); + + method.postTipMessage = jest.fn(); + method.postProgressMessage = jest.fn(); + (method as any).protocolV2CommonUpdateProcess = jest.fn().mockResolvedValue(3); + (method as any).device = stubDevice({ + getCommands: () => ({ + typedCall: jest.fn().mockResolvedValue({ + type: 'FilesystemPathInfo', + message: { exist: true, directory: false, size: 2 }, + }), + }), + }); + (method as any).protocolV2StartFirmwareUpdate = jest.fn(); + + await expect( + (method as any).executeProtocolV2Update({ + bootloaderBinary: null, + fwBinaryMap: [ + { + fileName: 'coprocessor.bin', + binary: new Uint8Array([1, 2, 3]).buffer, + targetId: 5, + }, + ], + }) + ).rejects.toThrow(/staged file verification failed/); + expect((method as any).protocolV2StartFirmwareUpdate).not.toHaveBeenCalled(); + }); + + test('passes explicit per-target binaries through without file name heuristics', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + platform: 'web', + applicationP2Binary: new Uint8Array([2]).buffer, + se04Binary: new Uint8Array([3]).buffer, + }, + }); + method.init(); + + const explicit = (method as any).collectExplicitTargetBinaries(); + expect(explicit).toEqual([ + { fileName: 'application_p2.bin', binary: expect.anything(), targetId: 5 }, + { fileName: 'se04.bin', binary: expect.anything(), targetId: 10 }, + ]); + + method.postTipMessage = jest.fn(); + method.postProgressMessage = jest.fn(); + (method as any).protocolV2CommonUpdateProcess = jest + .fn() + .mockImplementation(params => + Promise.resolve(Number(params.processedSize ?? 0) + Number(params.payload.byteLength)) + ); + (method as any).verifyProtocolV2StagedFile = jest.fn().mockResolvedValue(undefined); + (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockResolvedValue(undefined); + (method as any).waitForProtocolV2FirmwareUpdateComplete = jest + .fn() + .mockResolvedValue(undefined); + + await (method as any).executeProtocolV2Update({ + bootloaderBinary: null, + fwBinaryMap: explicit, + }); + + expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({ + targets: [ + { target_id: 5, path: 'vol0:/application_p2.bin' }, + { target_id: 10, path: 'vol0:/se04.bin' }, + ], + }); + }); + + test('rejects romloaderBinary before sending a Protocol V2 update request', () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + platform: 'web', + romloaderBinary: new Uint8Array([1]).buffer, + }, + }); + method.init(); + + expect(() => (method as any).collectExplicitTargetBinaries()).toThrow( + 'FW_MGMT_TARGET_ROMLOADER is not accepted' + ); + }); + + test('maps Pro2 remote firmware-v1 components to Protocol V2 targets in installOrder', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + platform: 'web', + targetsToUpdate: [ + 'boot', + 'app_v1', + 'app_v2', + 'coprocessor', + 'se01', + 'se02', + 'se03', + 'se04', + ], + }, + }); + method.init(); + + const binaries = new Map( + [ + 'bootloader', + 'applicationP1', + 'applicationP2', + 'coprocessor', + 'se01', + 'se02', + 'se03', + 'se04', + ].map((key, index) => [ + `https://example.com/${key}.pp.bin`, + new Uint8Array([index + 1]).buffer, + ]) + ); + const getSysResourceBinarySpy = jest + .spyOn(firmwareBinaryApi, 'getSysResourceBinary') + .mockImplementation(url => + Promise.resolve({ + binary: binaries.get(url) ?? new Uint8Array([0]).buffer, + }) + ); + const getFirmwareLatestReleaseSpy = jest + .spyOn(DataManager, 'getFirmwareLatestRelease') + .mockReturnValue({ + required: false, + version: [1, 0, 0], + url: 'https://example.com/applicationP1.pp.bin', + bootloaderResource: 'https://example.com/bootloader.pp.bin', + bootloaderVersion: [1, 0, 0], + displayBootloaderVersion: [1, 0, 0], + upgradeType: 'payload-package-set', + installOrder: [ + 'bootloader', + 'applicationP1', + 'applicationP2', + 'coprocessor', + 'se01', + 'se02', + 'se03', + 'se04', + ], + components: { + bootloader: { + target: 'BOOTLOADER', + url: 'https://example.com/bootloader.pp.bin', + }, + applicationP1: { + target: 'APPLICATION_P1', + url: 'https://example.com/applicationP1.pp.bin', + }, + applicationP2: { + target: 'APPLICATION_P2', + url: 'https://example.com/applicationP2.pp.bin', + }, + coprocessor: { + target: 'COPROCESSOR', + url: 'https://example.com/coprocessor.pp.bin', + }, + se01: { target: 'SE01', url: 'https://example.com/se01.pp.bin' }, + se02: { target: 'SE02', url: 'https://example.com/se02.pp.bin' }, + se03: { target: 'SE03', url: 'https://example.com/se03.pp.bin' }, + se04: { target: 'SE04', url: 'https://example.com/se04.pp.bin' }, + }, + fingerprint: '', + changelog: { + 'zh-CN': '', + 'en-US': '', + }, + }); + + const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', { + deviceType: 'pro2', + firmwareVersion: '0.0.0', + }); + + expect(remoteBinaries.bootloaderBinary).toBe( + binaries.get('https://example.com/bootloader.pp.bin') + ); + expect(remoteBinaries.fwBinaryMap).toEqual([ + { fileName: 'application_p1.bin', binary: expect.anything(), targetId: 4 }, + { fileName: 'application_p2.bin', binary: expect.anything(), targetId: 5 }, + { fileName: 'coprocessor.bin', binary: expect.anything(), targetId: 6 }, + { fileName: 'se01.bin', binary: expect.anything(), targetId: 7 }, + { fileName: 'se02.bin', binary: expect.anything(), targetId: 8 }, + { fileName: 'se03.bin', binary: expect.anything(), targetId: 9 }, + { fileName: 'se04.bin', binary: expect.anything(), targetId: 10 }, + ]); + expect(getSysResourceBinarySpy.mock.calls.map(call => call[0])).toEqual([ + 'https://example.com/bootloader.pp.bin', + 'https://example.com/applicationP1.pp.bin', + 'https://example.com/applicationP2.pp.bin', + 'https://example.com/coprocessor.pp.bin', + 'https://example.com/se01.pp.bin', + 'https://example.com/se02.pp.bin', + 'https://example.com/se03.pp.bin', + 'https://example.com/se04.pp.bin', + ]); + + getSysResourceBinarySpy.mockRestore(); + getFirmwareLatestReleaseSpy.mockRestore(); + }); + + test('maps remote Pro2 resource bundles to direct-write descriptors', () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + platform: 'web', + targetsToUpdate: ['resource'], + }, + }); + method.init(); + + const getFirmwareLatestReleaseSpy = jest + .spyOn(DataManager, 'getFirmwareLatestRelease') + .mockReturnValue({ + required: false, + version: [1, 0, 0], + url: '', + resourceBundles: [ + { + name: 'images', + url: 'https://example.com/images.okpkg', + devicePath: 'vol0:/resource/images/images.okpkg', + version: [2, 0, 0], + payloadHash: 'ab'.repeat(64), + headerHash: 'cd'.repeat(64), + }, + ], + fingerprint: '', + changelog: { + 'zh-CN': '', + 'en-US': '', + }, + }); + + const bundles = (method as any).prepareProtocolV2ResourceBundles('universal', { + deviceType: 'pro2', + firmwareVersion: '1.0.0', + capabilities: [], + }); + + expect(bundles).toEqual([ + { + name: 'images', + binary: expect.any(ArrayBuffer), + devicePath: 'vol0:/resource/images/images.okpkg', + url: 'https://example.com/images.okpkg', + version: [2, 0, 0], + payloadHash: 'ab'.repeat(64), + headerHash: 'cd'.repeat(64), + }, + ]); + expect(bundles[0].binary.byteLength).toBe(0); + + getFirmwareLatestReleaseSpy.mockRestore(); + }); + + test('does not download Pro2 firmware components that App did not select', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + platform: 'web', + }, + }); + method.init(); + + const getSysResourceBinarySpy = jest + .spyOn(firmwareBinaryApi, 'getSysResourceBinary') + .mockResolvedValue({ binary: new Uint8Array([1]).buffer }); + const getFirmwareLatestReleaseSpy = jest + .spyOn(DataManager, 'getFirmwareLatestRelease') + .mockReturnValue({ + required: false, + version: [1, 0, 0], + url: 'https://example.com/applicationP1.pp.bin', + bootloaderVersion: [1, 0, 0], + upgradeType: 'payload-package-set', + installOrder: ['bootloader', 'applicationP1', 'applicationP2'], + components: { + bootloader: { + target: 'BOOTLOADER', + url: 'https://example.com/bootloader.pp.bin', + }, + applicationP1: { + target: 'APPLICATION_P1', + url: 'https://example.com/applicationP1.pp.bin', + }, + applicationP2: { + target: 'APPLICATION_P2', + url: 'https://example.com/applicationP2.pp.bin', + }, + }, + fingerprint: '', + changelog: { + 'zh-CN': '', + 'en-US': '', + }, + }); + + const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', { + deviceType: 'pro2', + firmwareVersion: '1.0.0', + bootloaderVersion: '1.0.0', + capabilities: [], + }); + + expect(remoteBinaries).toEqual({ + bootloaderBinary: null, + fwBinaryMap: [], + installItems: [], + }); + expect(getSysResourceBinarySpy).not.toHaveBeenCalled(); + + getSysResourceBinarySpy.mockRestore(); + getFirmwareLatestReleaseSpy.mockRestore(); + }); + + test('uses Pro2 remote components when firmwareUpdateV4 has no explicit binaries', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + platform: 'web', + }, + }); + method.init(); + + const remoteBinaries = { + bootloaderBinary: new Uint8Array([1]).buffer, + fwBinaryMap: [ + { + fileName: 'application_p1.bin', + binary: new Uint8Array([2]).buffer, + targetId: 3, + }, + ], + }; + + (method as any).device = stubDevice({ + originalDescriptor: { protocolType: 'V2' }, + features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] }, + }); + (method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue(remoteBinaries); + (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true); + (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined); + (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined); + (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({ + bootloaderVersion: '1.0.0', + bleVersion: '0.0.0', + firmwareVersion: '1.0.0', + }); + method.postTipMessage = jest.fn(); + + await method.run(); + + expect((method as any).prepareRemoteProtocolV2Binaries).toHaveBeenCalledTimes(1); + expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({ + bootloaderBinary: remoteBinaries.bootloaderBinary, + fwBinaryMap: remoteBinaries.fwBinaryMap, + }); + }); + + test('keeps explicit Protocol V2 binaries isolated from remote component auto-fill', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + platform: 'web', + coprocessorBinary: new Uint8Array([1]).buffer, + }, + }); + method.init(); + + (method as any).device = stubDevice({ + originalDescriptor: { protocolType: 'V2' }, + features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] }, + }); + (method as any).prepareRemoteProtocolV2Binaries = jest.fn(); + (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true); + (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined); + (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined); + (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({ + bootloaderVersion: '1.0.0', + bleVersion: '0.0.0', + firmwareVersion: '1.0.0', + }); + method.postTipMessage = jest.fn(); + + await method.run(); + + expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled(); + expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({ + bootloaderBinary: null, + fwBinaryMap: [{ fileName: 'coprocessor.bin', binary: expect.anything(), targetId: 6 }], + }); + }); + + test('treats manual resource bundles as explicit payload without remote firmware auto-fill', async () => { + const resourceBundle = new Uint8Array([1, 2, 3]).buffer; + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + platform: 'web', + resourceBundleFiles: [ + { + binary: resourceBundle, + devicePath: 'vol0:/resource/images/images.okpkg', + }, + ], + }, + }); + method.init(); + + (method as any).device = stubDevice({ + originalDescriptor: { protocolType: 'V2' }, + features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] }, + }); + (method as any).prepareRemoteProtocolV2Binaries = jest.fn(); + (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true); + (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined); + (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined); + (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({ + bootloaderVersion: '1.0.0', + bleVersion: '0.0.0', + firmwareVersion: '1.0.0', + }); + method.postTipMessage = jest.fn(); + + await method.run(); + + expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled(); + expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith( + expect.objectContaining({ + resourceBundles: [ + { + name: 'images.okpkg', + binary: resourceBundle, + devicePath: 'vol0:/resource/images/images.okpkg', + }, + ], + }) + ); + }); + + test('syncs resource bundles without sending a firmware install request', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + }, + }); + + method.postTipMessage = jest.fn(); + method.postProgressMessage = jest.fn(); + (method as any).protocolV2CommonUpdateProcess = jest.fn().mockResolvedValue(3); + (method as any).protocolV2StartFirmwareUpdate = jest.fn(); + (method as any).waitForProtocolV2FirmwareUpdateComplete = jest.fn(); + + await (method as any).executeProtocolV2Update({ + resourceBundles: [ + { + name: 'images.okpkg', + binary: new Uint8Array([1, 2, 3]).buffer, + devicePath: 'vol0:/resource/images/images.okpkg', + }, + ], + bootloaderBinary: null, + fwBinaryMap: [], + }); + + expect((method as any).protocolV2CommonUpdateProcess).toHaveBeenCalledTimes(1); + expect((method as any).protocolV2StartFirmwareUpdate).not.toHaveBeenCalled(); + expect((method as any).waitForProtocolV2FirmwareUpdateComplete).not.toHaveBeenCalled(); + }); + + test('uses absolute processed_byte offsets and disables append for firmware file writes', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + }, + }); + const typedCall = jest.fn( + ( + _name: string, + _resType: string, + params: { file: { offset: number; data: { byteLength: number } } } + ) => + Promise.resolve({ + type: 'FilesystemFile', + message: { + processed_byte: params.file.offset + params.file.data.byteLength, + }, + }) + ); + + (method as any).device = stubDevice({ + getCommands: () => ({ typedCall }), + }); + method.postProgressMessage = jest.fn(); + + await (method as any).protocolV2CommonUpdateProcess({ + payload: new Uint8Array(4097).buffer, + filePath: 'vol1:firmware.bin', + processedSize: 0, + totalSize: 4097, + }); + + const writePayloads = typedCall.mock.calls.map(call => call[2]); + expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 4000]); + expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([4000, 97]); + expect(writePayloads.map(payload => payload.overwrite)).toEqual([true, false]); + expect(writePayloads.every(payload => payload.append === false)).toBe(true); + expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 100]); + }); + + test('restarts the whole file from offset zero after an ambiguous chunk write failure', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + }, + }); + const writeOffsets: number[] = []; + let failedSecondChunk = false; + const typedCall = jest.fn( + ( + _name: string, + _resType: string, + params: { file: { offset: number; data: { byteLength: number } } } + ) => { + writeOffsets.push(params.file.offset); + if (params.file.offset === 4000 && !failedSecondChunk) { + failedSecondChunk = true; + return Promise.reject(new Error('response lost after device write')); + } + return Promise.resolve({ + type: 'FilesystemFile', + message: { + processed_byte: params.file.offset + params.file.data.byteLength, + }, + }); + } + ); + const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation((( + callback: (...args: any[]) => void + ) => { + callback(); + return 0 as any; + }) as typeof setTimeout); + + (method as any).device = stubDevice({ + getCommands: () => ({ typedCall }), + }); + method.postProgressMessage = jest.fn(); + + try { + await (method as any).protocolV2CommonUpdateProcess({ + payload: new Uint8Array(8001).buffer, + filePath: 'vol0:/firmware.bin', + processedSize: 0, + totalSize: 8001, + }); + } finally { + setTimeoutSpy.mockRestore(); + } + + expect(writeOffsets).toEqual([0, 4000, 0, 4000, 8000]); + }); + + test('continues to DeviceFirmwareUpdate when FilesystemFileWrite returns processed chunk length', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + }, + }); + const typedCall = jest.fn((name: string, _resType: string | string[], params: any) => { + if (name === 'FilesystemFileWrite') { + return Promise.resolve({ + type: 'FilesystemFile', + message: { + processed_byte: params.file.data.byteLength, + }, + }); + } + if (name === 'FilesystemPathInfoQuery') { + return Promise.resolve({ + type: 'FilesystemPathInfo', + message: { exist: true, directory: false, size: 4097 }, + }); + } + if (name === 'DeviceFirmwareUpdateRequest') { + return Promise.resolve({ type: 'Success', message: { message: 'ok' } }); + } + if (name === 'DeviceFirmwareUpdateStatusGet') { + return Promise.resolve({ + type: 'DeviceFirmwareUpdateStatus', + message: { + records: [{ target_id: 4, status: 2 }], + }, + }); + } + return Promise.reject(new Error(`unexpected call ${name}`)); + }); + + (method as any).device = stubDevice({ + getCommands: () => ({ typedCall }), + }); + method.postProgressMessage = jest.fn(); + method.postTipMessage = jest.fn(); + + await (method as any).executeProtocolV2Update({ + bootloaderBinary: null, + fwBinaryMap: [ + { + fileName: 'firmware.bin', + binary: new Uint8Array(4097).buffer, + targetId: 4, + }, + ], + }); + + expect(typedCall).toHaveBeenCalledWith( + 'DeviceFirmwareUpdateRequest', + ['Success', 'DeviceFirmwareUpdateStatus'], + { + targets: [{ target_id: 4, path: 'vol0:/firmware.bin' }], + }, + expect.objectContaining({ + timeoutMs: 3 * 60 * 1000, + }) + ); + }); + + test('caps native BLE firmware upload chunks below the WebUSB limit', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + }, + }); + const typedCall = jest.fn( + ( + _name: string, + _resType: string, + params: { file: { offset: number; data: { byteLength: number } } } + ) => + Promise.resolve({ + type: 'FilesystemFile', + message: { + processed_byte: params.file.offset + params.file.data.byteLength, + }, + }) + ); + + (method as any).params = { + platform: 'native', + chunkSize: 4096, + }; + (method as any).device = stubDevice({ + getCommands: () => ({ typedCall }), + }); + method.postProgressMessage = jest.fn(); + + await (method as any).protocolV2CommonUpdateProcess({ + payload: new Uint8Array(1801).buffer, + filePath: 'vol1:ble-firmware.bin', + processedSize: 0, + totalSize: 1801, + }); + + const writePayloads = typedCall.mock.calls.map(call => call[2]); + expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 1800]); + expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1800, 1]); + expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 100]); + }); + + test('consumes Protocol V2 install progress before final update success', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + }, + }); + const typedCall = jest.fn().mockResolvedValue({ type: 'Success', message: { message: 'ok' } }); + + (method as any).device = stubDevice({ + getCommands: () => ({ typedCall }), + }); + method.postProgressMessage = jest.fn(); + method.postTipMessage = jest.fn(); + + await (method as any).protocolV2StartFirmwareUpdate({ + targets: [{ target_id: 4, path: 'vol1:firmware.bin' }], + }); + + const callOptions = typedCall.mock.calls[0][3]; + expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DeviceFirmwareUpdateStatus']); + expect(callOptions.intermediateTypes).toEqual(['DeviceFirmwareUpdateStatus']); + callOptions.onIntermediateResponse({ + type: 'DeviceFirmwareUpdateStatus', + message: { records: [{ target_id: 4, status: 1 }] }, + }); + + expect(method.postProgressMessage).toHaveBeenCalledWith(99, 'installingFirmware'); + }); + + test('accepts Protocol V2 firmware update status as start response', async () => { + const method = new FirmwareUpdateV4({ + id: 1, + payload: { + method: 'firmwareUpdateV4', + }, + }); + const typedCall = jest.fn().mockResolvedValue({ + type: 'DeviceFirmwareUpdateStatus', + message: { records: [{ target_id: 4, status: 1 }] }, + }); + + (method as any).device = stubDevice({ + getCommands: () => ({ typedCall }), + }); + method.postProgressMessage = jest.fn(); + method.postTipMessage = jest.fn(); + + await (method as any).protocolV2StartFirmwareUpdate({ + targets: [{ target_id: 4, path: 'vol1:firmware.bin' }], + }); + + expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DeviceFirmwareUpdateStatus']); + expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating'); + }); +}); + +describe('Protocol V2 firmware update method', () => { + test('returns DeviceFirmwareUpdateStatus from low-level update trigger', async () => { + const method = new DeviceFirmwareUpdate({ + id: 1, + payload: { + method: 'deviceFirmwareUpdate', + targetId: 4, + path: 'vol0:firmware.bin', + }, + }); + method.init(); + + const typedCall = jest.fn().mockResolvedValue({ + type: 'DeviceFirmwareUpdateStatus', + message: { records: [{ target_id: 4, status: 1 }] }, + }); + + (method as any).device = stubDevice({ + commands: { typedCall }, + }); + + await expect(method.run()).resolves.toEqual({ + records: [{ target_id: 4, status: 1 }], + }); + expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DeviceFirmwareUpdateStatus']); + expect(typedCall.mock.calls[0][2]).toEqual({ + targets: [{ target_id: 4, path: 'vol0:firmware.bin' }], + }); + }); + + test('treats WebUSB open NotFoundError from low-level update trigger as expected disconnect', async () => { + const method = new DeviceFirmwareUpdate({ + id: 1, + payload: { + method: 'deviceFirmwareUpdate', + targetId: 4, + path: 'vol0:firmware.bin', + }, + }); + method.init(); + + const typedCall = jest + .fn() + .mockRejectedValue( + new DOMException( + "Failed to execute 'open' on 'USBDevice': The device was disconnected", + 'NotFoundError' + ) + ); + + (method as any).device = stubDevice({ + commands: { typedCall }, + }); + + await expect(method.run()).resolves.toEqual({ + message: 'Device firmware update started', + }); + }); + + test('rejects missing or invalid firmware targets before transport call', async () => { + const typedCall = jest.fn(); + const method = new DeviceFirmwareUpdate({ + id: 1, + payload: { + method: 'deviceFirmwareUpdate', + target_id: -1, + path: 'vol0:firmware.bin', + }, + }); + method.init(); + (method as any).device = stubDevice({ + commands: { typedCall }, + }); + + await expect(method.run()).rejects.toMatchObject({ + errorCode: HardwareErrorCode.CallMethodInvalidParameter, + }); + expect(typedCall).not.toHaveBeenCalled(); + }); + + test.each([0, 2])( + 'rejects firmware target %s because the Pro2 bootloader cannot install it', + async targetId => { + const typedCall = jest.fn(); + const method = new DeviceFirmwareUpdate({ + id: 1, + payload: { + method: 'deviceFirmwareUpdate', + targetId, + path: 'vol0:firmware.bin', + }, + }); + method.init(); + (method as any).device = stubDevice({ + commands: { typedCall }, + }); + + await expect(method.run()).rejects.toMatchObject({ + errorCode: HardwareErrorCode.CallMethodInvalidParameter, + }); + expect(typedCall).not.toHaveBeenCalled(); + } + ); + + test('accepts targetId alias inside firmware targets', async () => { + const typedCall = jest.fn().mockResolvedValue({ message: {} }); + const method = new DeviceFirmwareUpdate({ + id: 1, + payload: { + method: 'deviceFirmwareUpdate', + targets: [ + { + target_id: undefined, + targetId: 1, + path: 'vol0:resource.crate.okpkg', + }, + ], + } as any, + }); + method.init(); + (method as any).device = stubDevice({ + commands: { typedCall }, + }); + + await method.run(); + expect(typedCall.mock.calls[0][2]).toEqual({ + targets: [{ target_id: 1, path: 'vol0:resource.crate.okpkg' }], + }); + }); + + test('accepts firmware target enum names from low-level update params', async () => { + const typedCall = jest.fn().mockResolvedValue({ message: {} }); + const method = new DeviceFirmwareUpdate({ + id: 1, + payload: { + method: 'deviceFirmwareUpdate', + targetId: 'FW_MGMT_TARGET_APPLICATION_P1', + path: 'vol0:application_p1.bin', + }, + }); + method.init(); + (method as any).device = stubDevice({ + commands: { typedCall }, + }); + + await method.run(); + expect(typedCall.mock.calls[0][2]).toEqual({ + targets: [{ target_id: 4, path: 'vol0:application_p1.bin' }], + }); + }); + + test('passes firmware update status fields through to Protocol V2', async () => { + const typedCall = jest.fn().mockResolvedValue({ + message: { + records: [{ target_id: 4, status: 2, payload_version: 1, path: 'vol1:application_p1.bin' }], + }, + }); + const method = new DeviceGetFirmwareUpdateStatus({ + id: 1, + payload: { + method: 'deviceGetFirmwareUpdateStatus', + fields: { + status: true, + payload_version: true, + path: true, + }, + }, + }); + method.init(); + (method as any).device = stubDevice({ + commands: { typedCall }, + }); + + await expect(method.run()).resolves.toEqual({ + records: [{ target_id: 4, status: 2, payload_version: 1, path: 'vol1:application_p1.bin' }], + }); + expect(typedCall).toHaveBeenCalledWith( + 'DeviceFirmwareUpdateStatusGet', + 'DeviceFirmwareUpdateStatus', + { + fields: { + status: true, + payload_version: true, + path: true, + }, + } + ); + }); +}); + +describe('Protocol V2 reboot methods', () => { + test('sends DeviceReboot from deviceReboot', async () => { + const typedCall = jest.fn().mockResolvedValue({ message: { message: 'ok' } }); + const method = new DeviceReboot({ + id: 1, + payload: { + method: 'deviceReboot', + rebootType: 2, + }, + }); + method.init(); + (method as any).device = stubDevice({ commands: { typedCall } }); + + await method.run(); + + expect(typedCall).toHaveBeenCalledWith('DeviceReboot', 'Success', { + reboot_type: 2, + }); + }); +}); + +describe('Protocol V2 protected method execution', () => { + const deviceLockedError = () => + Object.assign(new Error('Device locked'), { + errorCode: HardwareErrorCode.DeviceLocked, + }); + + test('unlocks and retries an opted-in Protocol V2 method once', async () => { + const calls: string[] = []; + const method = { + unlockPolicy: 'retry-on-locked', + run: jest + .fn() + .mockImplementationOnce(() => { + calls.push('run-1'); + return Promise.reject(deviceLockedError()); + }) + .mockImplementationOnce(() => { + calls.push('run-2'); + return Promise.resolve({ message: 'ok' }); + }), + }; + const device = { + isProtocolV2: () => true, + unlockDevice: jest.fn(() => { + calls.push('unlock'); + return Promise.resolve(); + }), + }; + + await expect(runMethodWithUnlockRetry(method as any, device as any)).resolves.toEqual({ + message: 'ok', + }); + expect(calls).toEqual(['run-1', 'unlock', 'run-2']); + }); + + test('does not unlock a Protocol V1 device or a method without the policy', async () => { + for (const [isProtocolV2, unlockPolicy] of [ + [false, 'retry-on-locked'], + [true, 'none'], + ] as const) { + const error = deviceLockedError(); + const method = { + unlockPolicy, + run: jest.fn().mockRejectedValue(error), + }; + const device = { + isProtocolV2: () => isProtocolV2, + unlockDevice: jest.fn(), + }; + + await expect(runMethodWithUnlockRetry(method as any, device as any)).rejects.toBe(error); + expect(device.unlockDevice).not.toHaveBeenCalled(); + expect(method.run).toHaveBeenCalledTimes(1); + } + }); + + test('does not retry when unlock fails or when the retry is still locked', async () => { + const initialError = deviceLockedError(); + const unlockError = new Error('PIN cancelled'); + const unlockFailMethod = { + unlockPolicy: 'retry-on-locked', + run: jest.fn().mockRejectedValue(initialError), + }; + const unlockFailDevice = { + isProtocolV2: () => true, + unlockDevice: jest.fn().mockRejectedValue(unlockError), + }; + + await expect( + runMethodWithUnlockRetry(unlockFailMethod as any, unlockFailDevice as any) + ).rejects.toBe(unlockError); + expect(unlockFailMethod.run).toHaveBeenCalledTimes(1); + + const retryError = deviceLockedError(); + const retryFailMethod = { + unlockPolicy: 'retry-on-locked', + run: jest.fn().mockRejectedValueOnce(initialError).mockRejectedValueOnce(retryError), + }; + const retryFailDevice = { + isProtocolV2: () => true, + unlockDevice: jest.fn().mockResolvedValue(undefined), + }; + + await expect( + runMethodWithUnlockRetry(retryFailMethod as any, retryFailDevice as any) + ).rejects.toBe(retryError); + expect(retryFailMethod.run).toHaveBeenCalledTimes(2); + expect(retryFailDevice.unlockDevice).toHaveBeenCalledTimes(1); + }); +}); + +describe('Protocol V2 current low-level methods', () => { + test('sends ProtocolInfoRequest from protocolInfoRequest', async () => { + const typedCall = jest.fn().mockResolvedValue({ message: { version: 1 } }); + const method = new ProtocolInfoRequest({ + id: 1, + payload: { method: 'protocolInfoRequest' }, + }); + method.init(); + (method as any).device = stubDevice({ commands: { typedCall } }); + + await method.run(); + + expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {}); + }); + + test('sends DeviceFactoryInfoSet and DeviceFactoryInfoGet', async () => { + const typedCall = jest.fn().mockResolvedValue({ message: {} }); + const setMethod = new DeviceFactoryInfoSet({ + id: 1, + payload: { + method: 'deviceFactoryInfoSet', + serial_number: 'PR2SERIAL', + burn_in_completed: true, + }, + }); + setMethod.init(); + (setMethod as any).device = stubDevice({ commands: { typedCall } }); + + await setMethod.run(); + + expect(typedCall).toHaveBeenCalledWith('DeviceFactoryInfoSet', 'Success', { + info: { + version: undefined, + serial_number: 'PR2SERIAL', + burn_in_completed: true, + factory_test_completed: undefined, + manufacture_time: undefined, + }, + }); + + typedCall.mockResolvedValueOnce({ message: { serial_number: 'PR2SERIAL' } }); + const getMethod = new DeviceFactoryInfoGet({ + id: 2, + payload: { method: 'deviceFactoryInfoGet' }, + }); + getMethod.init(); + (getMethod as any).device = stubDevice({ commands: { typedCall } }); + + await getMethod.run(); + + expect(typedCall).toHaveBeenLastCalledWith('DeviceFactoryInfoGet', 'DeviceFactoryInfo', {}); + }); + + test('gets Protocol V2 device settings', async () => { + const typedCall = jest.fn().mockResolvedValue({ message: { brightness: 80 } }); + const method = new DeviceSettingsGet({ + id: 1, + payload: { method: 'deviceSettingsGet' }, + }); + method.init(); + (method as any).device = stubDevice({ commands: { typedCall } }); + + await expect(method.run()).resolves.toEqual({ brightness: 80 }); + expect(typedCall).toHaveBeenCalledWith('DeviceSettingsGet', 'DeviceSettings', {}); + }); + + test('sets Protocol V2 device settings without passphrase fields', async () => { + const typedCall = jest.fn().mockResolvedValue({ message: { message: 'ok' } }); + const method = new DeviceSettingsSet({ + id: 1, + payload: { + method: 'deviceSettingsSet', + settings: { + label: 'My Pro 2', + brightness: 80, + airgap_mode: true, + passphrase_enable: true, + }, + }, + }); + method.init(); + (method as any).device = stubDevice({ commands: { typedCall } }); + + await method.run(); + + expect(typedCall).toHaveBeenCalledWith('DeviceSettingsSet', 'Success', { + settings: { + label: 'My Pro 2', + brightness: 80, + }, + }); + }); + + test('opens non-passphrase Protocol V2 settings pages', async () => { + const typedCall = jest.fn().mockResolvedValue({ message: { message: 'ok' } }); + const method = new DeviceSettingsPageShow({ + id: 1, + payload: { + method: 'deviceSettingsPageShow', + page: 'DeviceAirgap', + fieldName: 'airgap_mode', + }, + }); + method.init(); + (method as any).device = stubDevice({ commands: { typedCall } }); + + await method.run(); + + expect(typedCall).toHaveBeenCalledWith('DeviceSettingsPageShow', 'Success', { + page: 3, + field_name: 'airgap_mode', + }); + }); + + test('opens the Protocol V2 passphrase settings page', async () => { + const typedCall = jest.fn().mockResolvedValue({ message: { message: 'ok' } }); + const method = new DeviceSettingsPageShow({ + id: 1, + payload: { + method: 'deviceSettingsPageShow', + page: 'DevicePassphrase', + }, + }); + method.init(); + (method as any).device = stubDevice({ commands: { typedCall } }); + + await method.run(); + + expect(typedCall).toHaveBeenCalledWith('DeviceSettingsPageShow', 'Success', { + page: 2, + field_name: undefined, + }); + }); + + test('marks Protocol V2 settings methods for unlock-on-locked retry', () => { + const methods = [ + new DeviceSettingsGet({ id: 1, payload: { method: 'deviceSettingsGet' } }), + new DeviceSettingsSet({ + id: 2, + payload: { method: 'deviceSettingsSet', settings: { brightness: 80 } }, + }), + new DeviceSettingsPageShow({ + id: 3, + payload: { method: 'deviceSettingsPageShow', page: 'DevicePassphrase' }, + }), + ]; + + methods.forEach(method => { + method.init(); + expect(method.unlockPolicy).toBe('retry-on-locked'); + }); + }); + + test('sends FilesystemPermissionFix from filesystemPermissionFix', async () => { + const typedCall = jest.fn().mockResolvedValue({ message: {} }); + const method = new FilesystemPermissionFix({ + id: 1, + payload: { method: 'filesystemPermissionFix' }, + }); + method.init(); + (method as any).device = stubDevice({ commands: { typedCall } }); + + await method.run(); + + expect(typedCall).toHaveBeenCalledWith('FilesystemPermissionFix', 'Success', {}); + }); + + test('sends required FilesystemFormat partition flags', async () => { + const typedCall = jest.fn().mockResolvedValue({ message: {} }); + const method = new FilesystemFormat({ + id: 1, + payload: { method: 'filesystemFormat' }, + }); + method.init(); + (method as any).device = stubDevice({ commands: { typedCall } }); + + await method.run(); + + expect(typedCall).toHaveBeenCalledWith('FilesystemFormat', 'Success', { + data: true, + user: true, + }); + }); +}); + +describe('Protocol V2 raw device info method', () => { + const buildMethod = (payload: Record = {}) => { + const method = new DeviceInfoGet({ + id: 1, + payload: { + method: 'deviceInfoGet', + ...payload, + }, + }); + method.init(); + return method; + }; + + test('passes requested targets/types through and returns the raw DeviceInfo message', async () => { + const method = buildMethod({ + targets: { hw: true, se1: true }, + types: { version: true, hash: true }, + }); + const typedCall = jest.fn().mockResolvedValue({ + type: 'DeviceInfo', + message: { protocol_version: 1, hw: { serial_no: 'PR2SERIAL' } }, + }); + (method as any).device = stubDevice({ + originalDescriptor: { protocolType: 'V2' }, + commands: { typedCall }, + }); + + await expect(method.run()).resolves.toEqual({ + protocol_version: 1, + hw: { serial_no: 'PR2SERIAL' }, + }); + expect(typedCall).toHaveBeenCalledWith( + 'DeviceInfoGet', + 'DeviceInfo', + { + targets: { hw: true, se1: true }, + types: { version: true, hash: true }, + }, + { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS } + ); + }); + + test('rejects removed target/type names before transport call', () => { + expect(() => + buildMethod({ + targets: { hw: true, bt: true }, + }) + ).toThrow('targets'); + + expect(() => + buildMethod({ + types: { version: true, digest: true }, + }) + ).toThrow('types'); + }); + + test('defaults to the basic targets/types when none are given', async () => { + const method = buildMethod(); + const typedCall = jest.fn().mockResolvedValue({ type: 'DeviceInfo', message: {} }); + (method as any).device = stubDevice({ + originalDescriptor: { protocolType: 'V2' }, + commands: { typedCall }, + }); + + await method.run(); + + expect(typedCall).toHaveBeenCalledWith( + 'DeviceInfoGet', + 'DeviceInfo', + { + targets: { hw: true, fw: true, coprocessor: true, status: true }, + types: { version: true, specific: true }, + }, + { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS } + ); + }); + + test('rejects on Protocol V1 devices instead of sending an unknown message', async () => { + const method = buildMethod(); + const typedCall = jest.fn(); + (method as any).device = stubDevice({ + originalDescriptor: { protocolType: 'V1' }, + commands: { typedCall }, + }); + + await expect(method.run()).rejects.toThrow(); + expect(typedCall).not.toHaveBeenCalled(); + }); +}); + +describe('Protocol V2 file write method', () => { + test('rejects invalid write parameters before transport call', () => { + expect(() => { + const method = new FileWrite({ + id: 1, + payload: { + method: 'fileWrite', + path: 'vol1:test.bin', + offset: -1, + data: new Uint8Array([1]), + }, + }); + method.init(); + }).toThrow( + expect.objectContaining({ errorCode: HardwareErrorCode.CallMethodInvalidParameter }) + ); + + expect(() => { + const method = new FileWrite({ + id: 1, + payload: { + method: 'fileWrite', + path: 'vol1:test.bin', + } as any, + }); + method.init(); + }).toThrow( + expect.objectContaining({ errorCode: HardwareErrorCode.CallMethodInvalidParameter }) + ); + }); + + test('uses demo-aligned overwrite and append defaults', async () => { + const typedCall = jest.fn().mockResolvedValue({ message: { processed_byte: 1 } }); + const method = new FileWrite({ + id: 1, + payload: { + method: 'fileWrite', + path: 'vol1:test.bin', + offset: 1, + totalSize: 2, + data: new Uint8Array([1]), + }, + }); + (method as any).device = stubDevice({ commands: { typedCall } }); + method.postMessage = jest.fn(); + + method.init(); + await method.run(); + + expect(typedCall).toHaveBeenCalledWith( + 'FilesystemFileWrite', + 'FilesystemFile', + { + file: { + path: 'vol1:test.bin', + offset: 1, + total_size: 2, + data: new Uint8Array([1]), + }, + overwrite: false, + append: false, + ui_percentage: 100, + }, + { timeoutMs: undefined } + ); + expect(method.postMessage).toHaveBeenCalledWith({ + event: 'UI_EVENT', + type: UI_REQUEST.DEVICE_PROGRESS, + payload: expect.objectContaining({ + progress: 100, + transferredBytes: 1, + totalBytes: 1, + elapsedMs: expect.any(Number), + }), + }); + }); + + test('splits data larger than the Protocol V2 file payload limit', async () => { + const data = new Uint8Array(4097); + const typedCall = jest.fn().mockResolvedValue({ message: {} }); + const method = new FileWrite({ + id: 1, + payload: { + method: 'fileWrite', + path: 'vol1:test.bin', + offset: 0, + totalSize: 4097, + data, + }, + }); + (method as any).device = stubDevice({ commands: { typedCall } }); + method.postMessage = jest.fn(); + + method.init(); + const result = await method.run(); + + expect(typedCall).toHaveBeenCalledTimes(2); + expect(typedCall).toHaveBeenNthCalledWith( + 1, + 'FilesystemFileWrite', + 'FilesystemFile', + { + file: { + path: 'vol1:test.bin', + offset: 0, + total_size: 4097, + data: data.slice(0, 4000), + }, + overwrite: true, + append: false, + ui_percentage: 0, + }, + { timeoutMs: undefined } + ); + expect(typedCall).toHaveBeenNthCalledWith( + 2, + 'FilesystemFileWrite', + 'FilesystemFile', + { + file: { + path: 'vol1:test.bin', + offset: 4000, + total_size: 4097, + data: data.slice(4000), + }, + overwrite: false, + append: false, + ui_percentage: 100, + }, + { timeoutMs: undefined } + ); + expect(result).toMatchObject({ + path: 'vol1:test.bin', + processed_byte: 4097, + chunks: 2, + }); + expect(method.postMessage).toHaveBeenNthCalledWith(1, { + event: 'UI_EVENT', + type: UI_REQUEST.DEVICE_PROGRESS, + payload: expect.objectContaining({ + progress: 97, + transferredBytes: 4000, + totalBytes: 4097, + elapsedMs: expect.any(Number), + }), + }); + expect(method.postMessage).toHaveBeenNthCalledWith(2, { + event: 'UI_EVENT', + type: UI_REQUEST.DEVICE_PROGRESS, + payload: expect.objectContaining({ + progress: 100, + transferredBytes: 4097, + totalBytes: 4097, + elapsedMs: expect.any(Number), + }), + }); + }); + + test('uses the BLE chunk limit by default in BLE environments', async () => { + const getSettingsSpy = jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native'); + const data = new Uint8Array(1801); + const typedCall = jest.fn().mockResolvedValue({ message: {} }); + const method = new FileWrite({ + id: 1, + payload: { + method: 'fileWrite', + path: 'vol1:test.bin', + offset: 0, + totalSize: 1801, + data, + }, + }); + (method as any).device = stubDevice({ commands: { typedCall } }); + method.postMessage = jest.fn(); + + try { + method.init(); + await method.run(); + } finally { + getSettingsSpy.mockRestore(); + } + + expect(typedCall).toHaveBeenCalledTimes(2); + expect(typedCall.mock.calls[0][2].file.data.byteLength).toBe(1800); + expect(typedCall.mock.calls[1][2].file.offset).toBe(1800); + expect(typedCall.mock.calls[1][2].file.data.byteLength).toBe(1); + }); +}); + +describe('Protocol V2 file read method', () => { + test('uses a 900-byte chunk limit by default in BLE environments', async () => { + const getSettingsSpy = jest.spyOn(DataManager, 'getSettings').mockReturnValue('react-native'); + const typedCall = jest + .fn() + .mockResolvedValueOnce({ message: { data: new Uint8Array(900) } }) + .mockResolvedValueOnce({ message: { data: new Uint8Array(1) } }); + const method = new FileRead({ + id: 1, + payload: { + method: 'fileRead', + path: 'vol1:test.bin', + offset: 0, + totalSize: 901, + }, + }); + (method as any).device = stubDevice({ commands: { typedCall } }); + + try { + method.init(); + await method.run(); + } finally { + getSettingsSpy.mockRestore(); + } + + expect(typedCall).toHaveBeenCalledTimes(2); + expect(typedCall.mock.calls[0][2].chunk_len).toBe(900); + expect(typedCall.mock.calls[1][2].file.offset).toBe(900); + expect(typedCall.mock.calls[1][2].chunk_len).toBe(1); + }); + + test('rejects invalid read and directory parameters before transport call', () => { + expect(() => { + const method = new FileRead({ + id: 1, + payload: { + method: 'fileRead', + path: '', + offset: 0, + }, + }); + method.init(); + }).toThrow( + expect.objectContaining({ errorCode: HardwareErrorCode.CallMethodInvalidParameter }) + ); + + expect(() => { + const method = new FileRead({ + id: 1, + payload: { + method: 'fileRead', + path: 'vol1:test.bin', + totalSize: -1, + }, + }); + method.init(); + }).toThrow( + expect.objectContaining({ errorCode: HardwareErrorCode.CallMethodInvalidParameter }) + ); + + expect(() => { + const method = new DirList({ + id: 1, + payload: { + method: 'dirList', + path: 'vol1:', + depth: -1, + }, + }); + method.init(); + }).toThrow( + expect.objectContaining({ errorCode: HardwareErrorCode.CallMethodInvalidParameter }) + ); + }); + + test('reads full file in chunks when read length is 0', async () => { + const firstChunk = new Uint8Array(64).fill(1); + const typedCall = jest + .fn() + .mockResolvedValueOnce({ message: { exist: true, size: 65, directory: false } }) + .mockResolvedValueOnce({ message: { data: firstChunk } }) + .mockResolvedValueOnce({ message: { data: new Uint8Array([2]) } }); + const method = new FileRead({ + id: 1, + payload: { + method: 'fileRead', + path: 'vol1:test.bin', + offset: 0, + totalSize: 0, + chunkLen: 64, + }, + }); + (method as any).device = stubDevice({ commands: { typedCall } }); + + method.init(); + const result = await method.run(); + + expect(typedCall).toHaveBeenNthCalledWith(1, 'FilesystemPathInfoQuery', 'FilesystemPathInfo', { + path: 'vol1:test.bin', + }); + expect(typedCall).toHaveBeenNthCalledWith(2, 'FilesystemFileRead', 'FilesystemFile', { + file: { + path: 'vol1:test.bin', + offset: 0, + total_size: 0, + }, + chunk_len: 64, + ui_percentage: 0, + }); + expect(typedCall).toHaveBeenNthCalledWith(3, 'FilesystemFileRead', 'FilesystemFile', { + file: { + path: 'vol1:test.bin', + offset: 64, + total_size: 0, + }, + chunk_len: 1, + ui_percentage: 100, + }); + expect(result.data.byteLength).toBe(65); + expect(result.data[0]).toBe(1); + expect(result.data[64]).toBe(2); + expect(result).toMatchObject({ + path: 'vol1:test.bin', + offset: 0, + total_size: 65, + chunks: 2, + }); + }); + + test('decodes protobuf bytes hex string returned by transport', async () => { + const typedCall = jest.fn().mockResolvedValue({ + message: { + data: '0102ff', + }, + }); + const method = new FileRead({ + id: 1, + payload: { + method: 'fileRead', + path: 'vol0:test.bin', + offset: 0, + totalSize: 3, + chunkLen: 512, + }, + }); + (method as any).device = stubDevice({ commands: { typedCall } }); + + method.init(); + const result = await method.run(); + + expect(result.data).toEqual(new Uint8Array([1, 2, 255])); + }); +}); diff --git a/packages/core/__tests__/protocolV2FileWrite.test.ts b/packages/core/__tests__/protocolV2FileWrite.test.ts new file mode 100644 index 000000000..3f1fe8ef1 --- /dev/null +++ b/packages/core/__tests__/protocolV2FileWrite.test.ts @@ -0,0 +1,55 @@ +import { HardwareErrorCode } from '@onekeyfe/hd-shared'; + +import { writeProtocolV2File } from '../src/api/helpers/protocolV2FileWrite'; + +jest.mock('../src/data/config', () => ({ + getSDKVersion: jest.fn(() => '1.0.0'), + DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/', +})); + +describe('writeProtocolV2File', () => { + test('按分片写入并只在首片设置 overwrite', async () => { + const data = new Uint8Array(4097); + const typedCall = jest.fn().mockResolvedValue({ message: {} }); + const onProgress = jest.fn(); + + const result = await writeProtocolV2File({ + commands: { typedCall } as any, + path: 'vol0:/wallpapers/user/test.bin', + data, + totalSize: data.byteLength, + overwrite: true, + onProgress, + }); + + expect(typedCall).toHaveBeenCalledTimes(2); + expect(typedCall.mock.calls[0][2]).toMatchObject({ + file: { offset: 0, total_size: 4097, data: data.slice(0, 4000) }, + overwrite: true, + append: false, + ui_percentage: 0, + }); + expect(typedCall.mock.calls[1][2]).toMatchObject({ + file: { offset: 4000, total_size: 4097, data: data.slice(4000) }, + overwrite: false, + append: false, + ui_percentage: 100, + }); + expect(result).toMatchObject({ processed_byte: 4097, chunks: 2 }); + expect(onProgress).toHaveBeenLastCalledWith( + expect.objectContaining({ progress: 100, transferredBytes: 4097, totalBytes: 4097 }) + ); + }); + + test('拒绝越过文件末尾的 processed_byte', async () => { + const typedCall = jest.fn().mockResolvedValue({ message: { processed_byte: 10 } }); + + await expect( + writeProtocolV2File({ + commands: { typedCall } as any, + path: 'vol0:/wallpapers/user/test.bin', + data: new Uint8Array([1]), + }) + ).rejects.toMatchObject({ errorCode: HardwareErrorCode.RuntimeError }); + }); +}); diff --git a/packages/core/src/api/BaseMethod.ts b/packages/core/src/api/BaseMethod.ts index 68f923a2e..fd58e64c6 100644 --- a/packages/core/src/api/BaseMethod.ts +++ b/packages/core/src/api/BaseMethod.ts @@ -1,23 +1,17 @@ import semver from 'semver'; import { + ERRORS, + HardwareErrorCode, createDeviceNotSupportMethodError, createNeedUpgradeFirmwareHardwareError, } from '@onekeyfe/hd-shared'; -import { supportInputPinOnSoftware, supportModifyHomescreen } from '../utils/deviceFeaturesUtils'; import { createDeviceMessage } from '../events/device'; import { UI_REQUEST } from '../constants/ui-request'; import { DEVICE, FIRMWARE, createFirmwareMessage, createUiMessage } from '../events'; import { getHDPath, toHardened } from './helpers/pathUtils'; import { getBleFirmwareReleaseInfo, getFirmwareReleaseInfo } from './firmware/releaseHelper'; -import { - LoggerNames, - getDeviceFirmwareVersion, - getDeviceType, - getFirmwareType, - getLogger, - getMethodVersionRange, -} from '../utils'; +import { LoggerNames, getFirmwareType, getLogger, isMethodVersionRangeUnsupported } from '../utils'; import { generateInstanceId } from '../utils/tracing'; import { DeviceModelToTypes } from '../types'; @@ -28,6 +22,8 @@ import type { CoreMessage } from '../events'; import type { RequestContext } from '../utils/tracing'; import type { CoreContext } from '../core'; +export type UnlockPolicy = 'none' | 'retry-on-locked'; + const Log = getLogger(LoggerNames.Method); const isEvmLedgerLegacyPathWithHighIndex = (path: unknown) => { @@ -56,6 +52,8 @@ const isEvmLedgerLegacyPathWithHighIndex = (path: unknown) => { const EVM_LEDGER_LEGACY_METHODS = ['evmGetAddress', 'evmGetPublicKey']; export abstract class BaseMethod { + abortSignal?: AbortSignal; + responseID: number; // @ts-expect-error @@ -162,6 +160,26 @@ export abstract class BaseMethod { */ strictCheckDeviceSupport = false; + /** + * 方法是否仅支持 Protocol V2 设备(Pro2)。 + * core 调度在设备协议确定(acquire + initialize)后统一守卫, + * 非 V2 设备直接抛出 NotSupport 错误,方法内部不必重复判断。 + * @default false + */ + requireProtocolV2 = false; + + /** Protocol V2 设备锁定时是否允许 Core 自动解锁并重试一次。 */ + unlockPolicy: UnlockPolicy = 'none'; + + protected throwIfAborted() { + if (this.abortSignal?.aborted) { + throw ERRORS.TypedError( + HardwareErrorCode.CallQueueActionCancelled, + 'Hardware operation cancelled' + ); + } + } + // @ts-expect-error: strictPropertyInitialization postMessage: (message: CoreMessage) => void; @@ -225,7 +243,10 @@ export abstract class BaseMethod { } checkFirmwareRelease() { - if (!this.device || !this.device.features) return; + // 固件 release 元数据(DataManager remote config)目前只覆盖 Protocol V1 设备; + // Pro2 的更新流程由 firmwareUpdateV4 自行管理,这里显式跳过而不是依赖 features 为空。 + if (!this.device || this.device.isProtocolV2()) return; + if (!this.device.features) return; const firmwareType = getFirmwareType(this.device.features); const releaseInfo = getFirmwareReleaseInfo(this.device.features, firmwareType); this.postMessage( @@ -244,9 +265,9 @@ export abstract class BaseMethod { } checkDeviceSupportFeature() { - if (!this.device || !this.device.features) return; - const inputPinOnSoftware = supportInputPinOnSoftware(this.device.features); - const modifyHomescreen = supportModifyHomescreen(this.device.features); + if (!this.device || this.device.isUnacquired()) return; + const inputPinOnSoftware = this.device.supportInputPinOnSoftware(); + const modifyHomescreen = this.device.supportModifyHomescreen(); this.postMessage( createDeviceMessage(DEVICE.SUPPORT_FEATURES, { @@ -268,15 +289,16 @@ export abstract class BaseMethod { return; } - const firmwareVersion = getDeviceFirmwareVersion(this.device.features)?.join('.'); - const versionRange = getMethodVersionRange( - this.device.features, - type => getVersionRange()[type] - ); + const firmwareVersion = this.device.getCurrentFirmwareVersionString() ?? '0.0.0'; + const versionRange = this.device.getCurrentMethodVersionRange(type => getVersionRange()[type]); + + if (isMethodVersionRangeUnsupported(versionRange)) { + throw createDeviceNotSupportMethodError(this.name, this.device.getCurrentFirmwareType()); + } if (!versionRange) { if (options?.strictCheckDeviceSupport) { - throw createDeviceNotSupportMethodError(this.name, getFirmwareType(this.device.features)); + throw createDeviceNotSupportMethodError(this.name, this.device.getCurrentFirmwareType()); } // Equipment that does not need to be repaired return; @@ -287,7 +309,7 @@ export abstract class BaseMethod { currentVersion: firmwareVersion, requireVersion: versionRange.min, methodName: this.name, - firmwareType: getFirmwareType(this.device.features), + firmwareType: this.device.getCurrentFirmwareType(), }); } } @@ -297,7 +319,7 @@ export abstract class BaseMethod { return false; } - const deviceType = getDeviceType(this.device.features); + const deviceType = this.device.getCurrentDeviceType(); if (!DeviceModelToTypes.model_touch.includes(deviceType)) { return false; } @@ -329,7 +351,7 @@ export abstract class BaseMethod { if (this.shouldPromptSafetyCheckForEvmLedgerLegacyPath()) { checkFlag = true; } - if (checkFlag && this.device.features?.safety_checks === 'Strict') { + if (checkFlag && this.device.getCurrentSafetyChecks() === 'Strict') { Log.debug('will change safety_checks level'); await this.device.commands.typedCall('ApplySettings', 'Success', { safety_checks: 'PromptTemporarily', diff --git a/packages/core/src/api/ClearSessionCache.ts b/packages/core/src/api/ClearSessionCache.ts new file mode 100644 index 000000000..02265f431 --- /dev/null +++ b/packages/core/src/api/ClearSessionCache.ts @@ -0,0 +1,28 @@ +import { BaseMethod } from './BaseMethod'; +import { deviceWalletSessionStore } from '../device/DeviceWalletSessionStore'; + +import type { ClearSessionCacheParams } from '../types/api/sessionCache'; + +export default class ClearSessionCache extends BaseMethod { + init() { + this.useDevice = false; + this.useDevicePassphraseState = false; + this.skipForceUpdateCheck = true; + this.params = { + deviceId: this.payload.deviceId, + passphraseState: this.payload.passphraseState, + }; + } + + run() { + const { deviceId, passphraseState } = this.params; + if (!deviceId) { + deviceWalletSessionStore.clear(); + } else if (!passphraseState) { + deviceWalletSessionStore.deleteDevice(deviceId); + } else { + deviceWalletSessionStore.delete(deviceId, passphraseState); + } + return Promise.resolve({ cleared: true as const }); + } +} diff --git a/packages/core/src/api/DirList.ts b/packages/core/src/api/DirList.ts new file mode 100644 index 000000000..4125226bd --- /dev/null +++ b/packages/core/src/api/DirList.ts @@ -0,0 +1,31 @@ +import { BaseMethod } from './BaseMethod'; +import { + validateNonEmptyString, + validateOptionalNonNegativeInteger, +} from './helpers/filesystemValidation'; + +export type DirListParams = { + path: string; + depth?: number; +}; + +export default class DirList extends BaseMethod { + init() { + // Protocol V2 (Pro2) 专属方法,core 调度层统一做非 V2 设备守卫 + this.requireProtocolV2 = true; + this.skipForceUpdateCheck = true; + this.useDevicePassphraseState = false; + this.params = { + path: validateNonEmptyString(this.payload.path, 'path'), + depth: validateOptionalNonNegativeInteger(this.payload.depth, 'depth'), + }; + } + + async run() { + const res = await this.device.commands.typedCall('FilesystemDirList', 'FilesystemDir', { + path: this.params.path, + depth: this.params.depth, + }); + return Promise.resolve(res.message); + } +} diff --git a/packages/core/src/api/DirMake.ts b/packages/core/src/api/DirMake.ts new file mode 100644 index 000000000..a6dc455f1 --- /dev/null +++ b/packages/core/src/api/DirMake.ts @@ -0,0 +1,23 @@ +import { BaseMethod } from './BaseMethod'; +import { validateNonEmptyString } from './helpers/filesystemValidation'; + +export type DirMakeParams = { + path: string; +}; + +export default class DirMake extends BaseMethod { + init() { + // Protocol V2 (Pro2) 专属方法,core 调度层统一做非 V2 设备守卫 + this.requireProtocolV2 = true; + this.skipForceUpdateCheck = true; + this.useDevicePassphraseState = false; + this.params = { path: validateNonEmptyString(this.payload.path, 'path') }; + } + + async run() { + const res = await this.device.commands.typedCall('FilesystemDirMake', 'Success', { + path: this.params.path, + }); + return Promise.resolve(res.message); + } +} diff --git a/packages/core/src/api/DirRemove.ts b/packages/core/src/api/DirRemove.ts new file mode 100644 index 000000000..94421afd1 --- /dev/null +++ b/packages/core/src/api/DirRemove.ts @@ -0,0 +1,23 @@ +import { BaseMethod } from './BaseMethod'; +import { validateNonEmptyString } from './helpers/filesystemValidation'; + +export type DirRemoveParams = { + path: string; +}; + +export default class DirRemove extends BaseMethod { + init() { + // Protocol V2 (Pro2) 专属方法,core 调度层统一做非 V2 设备守卫 + this.requireProtocolV2 = true; + this.skipForceUpdateCheck = true; + this.useDevicePassphraseState = false; + this.params = { path: validateNonEmptyString(this.payload.path, 'path') }; + } + + async run() { + const res = await this.device.commands.typedCall('FilesystemDirRemove', 'Success', { + path: this.params.path, + }); + return Promise.resolve(res.message); + } +} diff --git a/packages/core/src/api/FileDelete.ts b/packages/core/src/api/FileDelete.ts new file mode 100644 index 000000000..c797890b1 --- /dev/null +++ b/packages/core/src/api/FileDelete.ts @@ -0,0 +1,23 @@ +import { BaseMethod } from './BaseMethod'; +import { validateNonEmptyString } from './helpers/filesystemValidation'; + +export type FileDeleteParams = { + path: string; +}; + +export default class FileDelete extends BaseMethod { + init() { + // Protocol V2 (Pro2) 专属方法,core 调度层统一做非 V2 设备守卫 + this.requireProtocolV2 = true; + this.skipForceUpdateCheck = true; + this.useDevicePassphraseState = false; + this.params = { path: validateNonEmptyString(this.payload.path, 'path') }; + } + + async run() { + const res = await this.device.commands.typedCall('FilesystemFileDelete', 'Success', { + path: this.params.path, + }); + return Promise.resolve(res.message); + } +} diff --git a/packages/core/src/api/FileRead.ts b/packages/core/src/api/FileRead.ts new file mode 100644 index 000000000..fdbe0665d --- /dev/null +++ b/packages/core/src/api/FileRead.ts @@ -0,0 +1,183 @@ +import { + PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE, + PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE, +} from '@onekeyfe/hd-transport'; +import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared'; + +import { BaseMethod } from './BaseMethod'; +import { + validateNonEmptyString, + validateNonNegativeInteger, + validateOptionalNonNegativeInteger, + validateOptionalPercentage, +} from './helpers/filesystemValidation'; +import { hexToBytes, isHexString, stripHexPrefix } from './helpers/hexUtils'; +import { DataManager } from '../data-manager'; + +export type FileReadParams = { + path: string; + offset?: number; + totalSize?: number; + chunkLen?: number; + uiPercentage?: number; +}; + +const MIN_FILE_READ_CHUNK_SIZE = 64; + +function getProtocolV2FileReadChunkLimit() { + const env = DataManager.getSettings('env'); + if (env && DataManager.isBleConnect(env)) { + return PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE; + } + return PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE; +} + +function normalizeChunkSize(value: unknown, maxChunkSize: number): number { + const numeric = Number(value); + if (!Number.isFinite(numeric) || numeric <= 0) return maxChunkSize; + return Math.min(Math.max(Math.floor(numeric), MIN_FILE_READ_CHUNK_SIZE), maxChunkSize); +} + +function toFiniteNumber(value: unknown): number | undefined { + if (typeof value === 'number' && Number.isFinite(value)) return value; + if (typeof value === 'string') { + const numeric = Number(value); + return Number.isFinite(numeric) ? numeric : undefined; + } + if (value && typeof value === 'object') { + const longLike = value as { toNumber?: () => number }; + if (typeof longLike.toNumber === 'function') { + const numeric = longLike.toNumber(); + return Number.isFinite(numeric) ? numeric : undefined; + } + } + return undefined; +} + +function toUint8Array(value: unknown): Uint8Array { + if (value instanceof Uint8Array) return value; + if (value instanceof ArrayBuffer) return new Uint8Array(value); + if (ArrayBuffer.isView(value)) { + return new Uint8Array(value.buffer, value.byteOffset, value.byteLength); + } + if (typeof value === 'string') { + const hex = stripHexPrefix(value); + if (!hex) return new Uint8Array(0); + if (!isHexString(hex) || hex.length % 2 !== 0) return new Uint8Array(0); + return hexToBytes(hex); + } + return new Uint8Array(0); +} + +function getDeviceTransferProgress( + bytesBeforeChunk: number, + bytesAfterChunk: number, + totalBytes: number +) { + if (!Number.isFinite(totalBytes) || totalBytes <= 0) { + return 100; + } + if (bytesBeforeChunk <= 0 && bytesAfterChunk < totalBytes) { + return 0; + } + if (bytesAfterChunk >= totalBytes) { + return 100; + } + return Math.min(Math.max(Math.ceil((bytesAfterChunk / totalBytes) * 100), 1), 99); +} + +export default class FileRead extends BaseMethod { + init() { + // Protocol V2 (Pro2) 专属方法,core 调度层统一做非 V2 设备守卫 + this.requireProtocolV2 = true; + this.skipForceUpdateCheck = true; + this.useDevicePassphraseState = false; + const path = validateNonEmptyString(this.payload.path, 'path'); + this.params = { + path, + offset: validateNonNegativeInteger(this.payload.offset, 'offset', 0), + totalSize: validateNonNegativeInteger(this.payload.totalSize, 'totalSize', 0), + chunkLen: validateOptionalNonNegativeInteger(this.payload.chunkLen, 'chunkLen'), + uiPercentage: validateOptionalPercentage(this.payload.uiPercentage, 'uiPercentage'), + }; + } + + async run() { + const startOffset = this.params.offset ?? 0; + const requestedLength = Number(this.params.totalSize); + const chunkSize = normalizeChunkSize(this.params.chunkLen, getProtocolV2FileReadChunkLimit()); + let totalLength = Number.isFinite(requestedLength) && requestedLength > 0 ? requestedLength : 0; + + if (totalLength === 0) { + const pathInfoRes = await this.device.commands.typedCall( + 'FilesystemPathInfoQuery', + 'FilesystemPathInfo', + { + path: this.params.path, + } + ); + const fileSize = toFiniteNumber(pathInfoRes.message?.size); + if (!pathInfoRes.message?.exist || pathInfoRes.message?.directory) { + throw ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + `FilesystemFileRead path is not a file: ${this.params.path}` + ); + } + if (fileSize === undefined || fileSize < startOffset) { + throw ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + `FilesystemFileRead invalid offset ${startOffset} for ${this.params.path}` + ); + } + totalLength = fileSize - startOffset; + } + + const chunks: Uint8Array[] = []; + let read = 0; + let lastMessage: Record | undefined; + + while (read < totalLength) { + const readLen = Math.min(chunkSize, totalLength - read); + const offset = startOffset + read; + const progress = + this.params.uiPercentage ?? getDeviceTransferProgress(read, read + readLen, totalLength); + const res = await this.device.commands.typedCall('FilesystemFileRead', 'FilesystemFile', { + file: { + path: this.params.path, + offset, + total_size: 0, + }, + chunk_len: readLen, + ui_percentage: progress, + }); + const data = toUint8Array(res.message?.data); + lastMessage = res.message; + + if (data.byteLength === 0) { + throw ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + `FilesystemFileRead received empty data at offset ${offset}` + ); + } + + chunks.push(data); + read += data.byteLength; + } + + const combined = new Uint8Array(read); + let cursor = 0; + chunks.forEach(chunk => { + combined.set(chunk, cursor); + cursor += chunk.byteLength; + }); + + return Promise.resolve({ + ...lastMessage, + path: this.params.path, + offset: startOffset, + total_size: totalLength, + data: combined, + chunks: chunks.length, + }); + } +} diff --git a/packages/core/src/api/FileWrite.ts b/packages/core/src/api/FileWrite.ts new file mode 100644 index 000000000..e0a724c0e --- /dev/null +++ b/packages/core/src/api/FileWrite.ts @@ -0,0 +1,69 @@ +import { BaseMethod } from './BaseMethod'; +import { + validateNonEmptyString, + validateNonNegativeInteger, + validateOptionalNonNegativeInteger, + validateOptionalPercentage, + validateRequiredData, +} from './helpers/filesystemValidation'; +import { writeProtocolV2File } from './helpers/protocolV2FileWrite'; +import { UI_REQUEST, createUiMessage } from '../events/ui-request'; + +export type FileWriteParams = { + path: string; + offset?: number; + totalSize?: number; + data: ArrayBuffer | Uint8Array | Blob | string; + chunkSize?: number; + chunkLen?: number; + overwrite?: boolean; + append?: boolean; + uiPercentage?: number; + timeoutMs?: number | string; +}; + +export default class FileWrite extends BaseMethod { + init() { + // Protocol V2 (Pro2) 专属方法,core 调度层统一做非 V2 设备守卫 + this.requireProtocolV2 = true; + this.skipForceUpdateCheck = true; + this.useDevicePassphraseState = false; + validateRequiredData(this.payload.data, 'data'); + const path = validateNonEmptyString(this.payload.path, 'path'); + const offset = validateNonNegativeInteger(this.payload.offset, 'offset', 0); + this.params = { + path, + offset, + totalSize: validateNonNegativeInteger(this.payload.totalSize, 'totalSize', 0), + data: this.payload.data, + chunkSize: validateOptionalNonNegativeInteger(this.payload.chunkSize, 'chunkSize'), + chunkLen: validateOptionalNonNegativeInteger(this.payload.chunkLen, 'chunkLen'), + overwrite: this.payload.overwrite ?? offset === 0, + append: this.payload.append ?? false, + uiPercentage: validateOptionalPercentage(this.payload.uiPercentage, 'uiPercentage'), + timeoutMs: validateOptionalNonNegativeInteger(this.payload.timeoutMs, 'timeoutMs'), + }; + } + + async run() { + return writeProtocolV2File({ + commands: this.device.commands, + path: this.params.path, + data: this.params.data, + offset: this.params.offset, + totalSize: this.params.totalSize, + chunkSize: this.params.chunkSize, + chunkLen: this.params.chunkLen, + overwrite: this.params.overwrite, + append: this.params.append, + uiPercentage: this.params.uiPercentage, + timeoutMs: this.params.timeoutMs === undefined ? undefined : Number(this.params.timeoutMs), + throwIfAborted: () => this.throwIfAborted(), + onProgress: payload => { + if (typeof this.postMessage === 'function') { + this.postMessage(createUiMessage(UI_REQUEST.DEVICE_PROGRESS, payload)); + } + }, + }); + } +} diff --git a/packages/core/src/api/FirmwareUpdate.ts b/packages/core/src/api/FirmwareUpdate.ts index baf771ff6..1ba0a346c 100644 --- a/packages/core/src/api/FirmwareUpdate.ts +++ b/packages/core/src/api/FirmwareUpdate.ts @@ -14,7 +14,7 @@ import { uploadFirmware } from './firmware/uploadFirmware'; import { createUiMessage } from '../events'; import { DeviceModelToTypes, type KnownDevice } from '../types'; import { isEnteredManuallyBoot } from './firmware/bootloaderHelper'; -import { LoggerNames, getDeviceType, getDeviceUUID, getLogger, wait } from '../utils'; +import { LoggerNames, getDeviceUUID, getLogger, wait } from '../utils'; import { DataManager } from '../data-manager'; import { DevicePool } from '../device/DevicePool'; @@ -92,7 +92,7 @@ export default class FirmwareUpdate extends BaseMethod { true ); await this.device.initialize(); - if (this.device.features?.bootloader_mode) { + if (this.device.isBootloader()) { clearInterval(intervalTimer); this.checkPromise?.resolve(true); } @@ -105,7 +105,7 @@ export default class FirmwareUpdate extends BaseMethod { const devicesDescriptor = deviceDiff?.descriptors ?? []; const { deviceList } = await DevicePool.getDevices(devicesDescriptor, connectId); - if (deviceList.length === 1 && deviceList[0]?.features?.bootloader_mode) { + if (deviceList.length === 1 && deviceList[0]?.isBootloader()) { // should update current device from cache // because device was reboot and had some new requests this.device.updateFromCache(deviceList[0]); @@ -131,9 +131,17 @@ export default class FirmwareUpdate extends BaseMethod { async run() { const { device, params } = this; const { features, commands } = device; - const deviceType = getDeviceType(features); + const deviceType = device.getCurrentDeviceType(); + + // Protocol V2 (Pro2) 固件升级走 DeviceFirmwareUpdate 流程,禁止进入 legacy 入口 + if (device.isProtocolV2()) { + throw ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + 'Protocol V2 firmware update must use firmwareUpdateV4' + ); + } - if (!features?.bootloader_mode && features) { + if (!device.isBootloader() && features) { const uuid = getDeviceUUID(features); // should go to bootloader mode manually if (isEnteredManuallyBoot(features, params.updateType)) { diff --git a/packages/core/src/api/FirmwareUpdateV2.ts b/packages/core/src/api/FirmwareUpdateV2.ts index d99d1f67c..759a0f11a 100644 --- a/packages/core/src/api/FirmwareUpdateV2.ts +++ b/packages/core/src/api/FirmwareUpdateV2.ts @@ -17,10 +17,9 @@ import { getBinary, getInfo, getSysResourceBinary } from './firmware/getBinary'; import { updateResources, uploadFirmware } from './firmware/uploadFirmware'; import { LoggerNames, + getDeviceBootloaderVersion, getDeviceFirmwareVersion, - getDeviceType, getDeviceUUID, - getFirmwareType, getLogger, wait, } from '../utils'; @@ -30,7 +29,6 @@ import { DataManager } from '../data-manager'; import { DEVICE } from '../events'; import type { Features, KnownDevice } from '../types'; -import type { Device } from '../device/Device'; type Params = { binary?: ArrayBuffer; @@ -102,7 +100,7 @@ export default class FirmwareUpdateV2 extends BaseMethod { ); }; - private async _promptDeviceInBootloaderForWebDevice({ device }: { device: Device }) { + private async _promptDeviceInBootloaderForWebDevice() { return new Promise((resolve, reject) => { if (this.device.listenerCount(DEVICE.SELECT_DEVICE_IN_BOOTLOADER_FOR_WEB_DEVICE) > 0) { this.device.emit( @@ -133,9 +131,8 @@ export default class FirmwareUpdateV2 extends BaseMethod { // eslint-disable-next-line prefer-const let timeoutTimer: ReturnType | undefined; - const isTouchOrProDevice = - getDeviceType(this?.device?.features) === EDeviceType.Touch || - getDeviceType(this?.device?.features) === EDeviceType.Pro; + const deviceType = this.device?.getCurrentDeviceType(); + const isTouchOrProDevice = deviceType === EDeviceType.Touch || deviceType === EDeviceType.Pro; const intervalTimer: ReturnType | undefined = setInterval( async () => { @@ -157,9 +154,7 @@ export default class FirmwareUpdateV2 extends BaseMethod { try { this.postTipMessage(FirmwareUpdateTipMessage.SelectDeviceInBootloaderForWebDevice); - const confirmed = await this._promptDeviceInBootloaderForWebDevice({ - device: this.device, - }); + const confirmed = await this._promptDeviceInBootloaderForWebDevice(); if (confirmed) { await this._checkDeviceInBootloaderMode(connectId, intervalTimer, timeoutTimer); } @@ -181,7 +176,7 @@ export default class FirmwareUpdateV2 extends BaseMethod { true ); await this.device.initialize(); - if (this.device.features?.bootloader_mode) { + if (this.device.isBootloader()) { clearInterval(intervalTimer); this.checkPromise?.resolve(true); } @@ -214,7 +209,7 @@ export default class FirmwareUpdateV2 extends BaseMethod { const devicesDescriptor = deviceDiff?.descriptors ?? []; const { deviceList } = await DevicePool.getDevices(devicesDescriptor, connectId); - if (deviceList.length === 1 && deviceList[0]?.features?.bootloader_mode) { + if (deviceList.length === 1 && deviceList[0]?.isBootloader()) { // should update current device from cache // because device was reboot and had some new requests this.device.updateFromCache(deviceList[0]); @@ -229,19 +224,19 @@ export default class FirmwareUpdateV2 extends BaseMethod { } isEnteredManuallyBoot(features: Features) { - const deviceType = getDeviceType(features); + const deviceType = this.device.getCurrentDeviceType(); const isMini = deviceType === EDeviceType.Mini; const isBoot183ClassicUpBle = this.params.updateType === 'firmware' && deviceType === EDeviceType.Classic && - features.bootloader_version === '1.8.3'; + getDeviceBootloaderVersion(features).join('.') === '1.8.3'; return isMini || isBoot183ClassicUpBle; } isSupportResourceUpdate(features: Features, updateType: string) { if (updateType !== 'firmware') return false; - const deviceType = getDeviceType(features); + const deviceType = this.device.getCurrentDeviceType(); const isTouchMode = deviceType === EDeviceType.Touch || deviceType === EDeviceType.Pro; const currentVersion = getDeviceFirmwareVersion(features).join('.'); @@ -254,7 +249,7 @@ export default class FirmwareUpdateV2 extends BaseMethod { */ checkVersionForCopyTouchResource(features: Features | undefined, firmwareType: EFirmwareType) { if (!features) return; - const deviceType = getDeviceType(features); + const deviceType = this.device.getCurrentDeviceType(); const currentVersion = getDeviceFirmwareVersion(features).join('.'); const targetVersion = this.params.version?.join('.'); const { updateType } = this.params; @@ -279,14 +274,22 @@ export default class FirmwareUpdateV2 extends BaseMethod { async run() { const { device, params } = this; const { features, commands } = device; - const deviceType = getDeviceType(features); + const deviceType = device.getCurrentDeviceType(); - const deviceFirmwareType = getFirmwareType(device.features); + // Protocol V2 (Pro2) 固件升级走 DeviceFirmwareUpdate 流程,禁止进入 legacy 入口 + if (device.isProtocolV2()) { + throw ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + 'Protocol V2 firmware update must use firmwareUpdateV4' + ); + } + + const deviceFirmwareType = device.getCurrentFirmwareType(); const firmwareType = params.firmwareType ?? deviceFirmwareType; this.checkVersionForCopyTouchResource(features, firmwareType); - if (!features?.bootloader_mode && features) { + if (!device.isBootloader() && features) { const uuid = getDeviceUUID(features); // should go to bootloader mode manually if (this.isEnteredManuallyBoot(features)) { diff --git a/packages/core/src/api/FirmwareUpdateV3.ts b/packages/core/src/api/FirmwareUpdateV3.ts index 6297afb6c..7d69dc0df 100644 --- a/packages/core/src/api/FirmwareUpdateV3.ts +++ b/packages/core/src/api/FirmwareUpdateV3.ts @@ -18,6 +18,7 @@ import { DataManager } from '../data-manager'; import { FirmwareUpdateBaseMethod } from './firmware/FirmwareUpdateBaseMethod'; import { DevicePool } from '../device/DevicePool'; import { DEVICE } from '../events'; +import { buildProtocolV1FeaturesPayload } from '../deviceProfile'; import type { FirmwareUpdateV3Params } from '../types/api/firmwareUpdate'; import type { Deferred, EFirmwareType } from '@onekeyfe/hd-shared'; @@ -79,6 +80,20 @@ export default class FirmwareUpdateV3 extends FirmwareUpdateBaseMethod { + if (!Number.isFinite(totalBytes) || totalBytes <= 0) { + return 100; + } + if (bytesBeforeChunk <= 0 && bytesAfterChunk < totalBytes) { + return 0; + } + if (bytesAfterChunk >= totalBytes) { + return 100; + } + return Math.min(Math.max(Math.ceil((bytesAfterChunk / totalBytes) * 100), 1), 99); +}; + +const formatProtocolV2TransferSpeed = (bytes: number, elapsedMs: number) => { + const safeElapsedMs = Math.max(elapsedMs, 1); + return (bytes / 1024 / (safeElapsedMs / 1000)).toFixed(2); +}; + +type ProtocolV2FirmwareUpdateStatusTarget = { + target_id: number | string; + status?: number | string; + payload_version?: number; + path?: string; +}; + +type ProtocolV2FirmwareUpdateStartResponse = + | TypedResponseMessage<'Success'> + | TypedResponseMessage<'DeviceFirmwareUpdateStatus'> + | undefined; + +type ProtocolV2TargetBinary = { fileName: string; binary: ArrayBuffer; targetId: number }; +type ProtocolV2InstallItem = ProtocolV2TargetBinary & { + kind: ProtocolV2RemoteComponentTarget['kind']; +}; +type ProtocolV2InstallTarget = ProtocolV2InstallItem & { + path: string; +}; + +type ProtocolV2FileTransferParams = PROTO.FirmwareUpload & { + filePath: string; + processedSize?: number; + totalSize?: number; + onTransferredBytes?: (transferredBytes: number) => void; +}; + +/** RESC bundle okpkg(FileWrite 直写模式),每个独立同步到 devicePath */ +type ProtocolV2ResourceBundleBinary = { + name: string; + binary: ArrayBuffer; + devicePath: string; + /** 远端配置模式下的下载 URL(手动模式不填) */ + url?: string; + version?: IVersionArray; + payloadHash?: string; + headerHash?: string; +}; + +type ProtocolV2RemoteComponentBinary = ProtocolV2RemoteComponentTarget & { + binary: ArrayBuffer; +}; + +type ProtocolV2RemoteComponentTarget = { + fileName: string; + targetId: number; + kind: 'bootloader' | 'firmware'; +}; + +type ProtocolV2OkppHeader = { + type: string; + version: IVersionArray; + payloadHash: string; + headerHash: string; +}; + +const PROTOCOL_V2_REMOTE_COMPONENT_TARGETS: Readonly< + Record +> = { + BOOTLOADER: { + fileName: 'bootloader.bin', + targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER, + kind: 'bootloader', + }, + APPLICATION_P1: { + fileName: 'application_p1.bin', + targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1, + kind: 'firmware', + }, + APPLICATION_P2: { + fileName: 'application_p2.bin', + targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2, + kind: 'firmware', + }, + COPROCESSOR: { + fileName: 'coprocessor.bin', + targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_COPROCESSOR, + kind: 'firmware', + }, + SE01: { + fileName: 'se01.bin', + targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE01, + kind: 'firmware', + }, + SE02: { + fileName: 'se02.bin', + targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE02, + kind: 'firmware', + }, + SE03: { + fileName: 'se03.bin', + targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE03, + kind: 'firmware', + }, + SE04: { + fileName: 'se04.bin', + targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE04, + kind: 'firmware', + }, +}; + +const PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID = new Map([ + [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER, 'boot'], + [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1, 'app_v1'], + [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2, 'app_v2'], + [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_COPROCESSOR, 'coprocessor'], + [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE01, 'se01'], + [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE02, 'se02'], + [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE03, 'se03'], + [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE04, 'se04'], +]); + +const PROTOCOL_V2_ROMLOADER_UNSUPPORTED_MESSAGE = + 'FW_MGMT_TARGET_ROMLOADER is not accepted by the current Pro2 bootloader update request. Flash romloader with the loader-specific flow instead of firmwareUpdateV4.'; + +// hd-transport 的历史 decode 行为会把单值 enum 输出为枚举名字符串; +// Protocol V2 沿用这个 SDK 语义,内部比较前再映射回固件协议数值。 +const PROTOCOL_V2_TARGET_ID_BY_DECODED_NAME = new Map( + Object.entries(ProtocolV2FirmwareTargetType).map(([key, value]) => [key, value]) +); +const PROTOCOL_V2_TARGET_STATUS_BY_DECODED_NAME = new Map([ + ['FW_MGMT_UPDATER_TASK_STATUS_PENDING', PROTOCOL_V2_TARGET_STATUS_PENDING], + ['FW_MGMT_UPDATER_TASK_STATUS_IN_PROGRESS', PROTOCOL_V2_TARGET_STATUS_IN_PROGRESS], + ['FW_MGMT_UPDATER_TASK_STATUS_FINISHED', PROTOCOL_V2_TARGET_STATUS_FINISHED], + ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_FILE_NOT_FOUND', 3], + ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_FILE_READ', 4], + ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_FILE_WRITE', 5], + ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_VERIFY', 6], + ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_INSTALL', 7], + ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_ABORT', 8], + ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_BUSY', 9], + ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_ENTRY_OUT_OF_BOUNDS', 10], +]); + +const isProtocolV2ReconnectProbeError = (error: unknown) => { + const message = getProtocolV2UnknownErrorText(error).toLowerCase(); + return ( + (message.includes('device protocol mismatch') && message.includes('expected v2')) || + message.includes('did not respond to expected protocol') + ); +}; + +const isProtocolV2PollingTransientError = (error: unknown) => { + const message = getProtocolV2UnknownErrorText(error).toLowerCase(); + return ( + isProtocolV2DeviceDisconnectedError(error) || + isProtocolV2ReconnectProbeError(error) || + message.includes('libusb_transfer_timed_out') || + (message.includes('response timeout') && message.includes('devicefirmwareupdatestatusget')) || + message.includes('device not found') || + message.includes('transportnotfound') + ); +}; + +const isProtocolV2StartUpdateTransientError = (error: unknown) => { + const message = getProtocolV2UnknownErrorText(error).toLowerCase(); + return ( + isProtocolV2DeviceDisconnectedError(error) || + isProtocolV2ReconnectProbeError(error) || + message.includes('libusb_transfer_timed_out') || + (message.includes('response timeout') && message.includes('devicefirmwareupdaterequest')) + ); +}; + +const isProtocolV2TargetStatusFinished = (status: ProtocolV2FirmwareUpdateStatusTarget['status']) => + normalizeProtocolV2TargetStatus(status) === PROTOCOL_V2_TARGET_STATUS_FINISHED; + +const isProtocolV2TargetStatusInProgress = ( + status: ProtocolV2FirmwareUpdateStatusTarget['status'] +) => + normalizeProtocolV2TargetStatus(status) === PROTOCOL_V2_TARGET_STATUS_PENDING || + normalizeProtocolV2TargetStatus(status) === PROTOCOL_V2_TARGET_STATUS_IN_PROGRESS; + +const isProtocolV2TargetStatusFailed = (status: ProtocolV2FirmwareUpdateStatusTarget['status']) => { + const normalizedStatus = normalizeProtocolV2TargetStatus(status); + return ( + typeof normalizedStatus === 'number' && normalizedStatus >= PROTOCOL_V2_TARGET_STATUS_FAILED_MIN + ); +}; + +const normalizeProtocolV2TargetId = (targetId: number | string) => { + if (typeof targetId === 'number') { + return targetId; + } + return PROTOCOL_V2_TARGET_ID_BY_DECODED_NAME.get(targetId); +}; + +const normalizeProtocolV2TargetStatus = ( + status: ProtocolV2FirmwareUpdateStatusTarget['status'] +) => { + if (typeof status === 'number') { + return status; + } + if (typeof status === 'string') { + return PROTOCOL_V2_TARGET_STATUS_BY_DECODED_NAME.get(status); + } + return undefined; +}; + +const normalizeProtocolV2Hex = (value?: string) => value?.replace(/^0x/i, '').toLowerCase(); + +const versionArrayToNumber = (version?: IVersionArray) => { + if (!version) return undefined; + return version[0] * 0x10000 + version[1] * 0x100 + version[2]; +}; + +const compareProtocolV2Versions = (current?: IVersionArray, target?: IVersionArray) => { + const currentNumber = versionArrayToNumber(current); + const targetNumber = versionArrayToNumber(target); + if (currentNumber === undefined || targetNumber === undefined) return undefined; + return currentNumber - targetNumber; +}; + +const bytesToHex = (bytes: Uint8Array) => + Array.from(bytes) + .map(byte => byte.toString(16).padStart(2, '0')) + .join(''); + +const hexToProtocolV2Bytes = (hex: string) => { + const normalized = hex.replace(/^0x/i, ''); + if (!normalized || normalized.length % 2 !== 0 || /[^0-9a-f]/i.test(normalized)) { + return new Uint8Array(0); + } + const bytes = new Uint8Array(normalized.length / 2); + for (let i = 0; i < bytes.length; i++) { + bytes[i] = Number.parseInt(normalized.slice(i * 2, i * 2 + 2), 16); + } + return bytes; +}; + +const toProtocolV2Bytes = (value: unknown): Uint8Array => { + if (value instanceof Uint8Array) return value; + if (value instanceof ArrayBuffer) return new Uint8Array(value); + if (ArrayBuffer.isView(value)) { + return new Uint8Array(value.buffer, value.byteOffset, value.byteLength); + } + if (typeof value === 'string') return hexToProtocolV2Bytes(value); + return new Uint8Array(0); +}; + +const toProtocolV2FiniteNumber = (value: unknown) => { + if (typeof value === 'number' && Number.isFinite(value)) return value; + if (typeof value === 'string') { + const numeric = Number(value); + return Number.isFinite(numeric) ? numeric : undefined; + } + if (value && typeof value === 'object') { + const longLike = value as { toNumber?: () => number }; + if (typeof longLike.toNumber === 'function') { + const numeric = longLike.toNumber(); + return Number.isFinite(numeric) ? numeric : undefined; + } + } + return undefined; +}; + +const readProtocolV2Ascii = (bytes: Uint8Array, offset: number, length: number) => + Array.from(bytes.slice(offset, offset + length)) + .map(byte => String.fromCharCode(byte)) + .join(''); + +const parseProtocolV2OkppHeader = (bytes: Uint8Array): ProtocolV2OkppHeader | null => { + if (bytes.byteLength < PROTOCOL_V2_OKPP_HEADER_SIZE) return null; + if (readProtocolV2Ascii(bytes, 0, 4) !== 'OKPP') return null; + + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const headerLen = view.getUint32(0x0c, true); + if (headerLen !== PROTOCOL_V2_OKPP_HEADER_SIZE) return null; + + const packedVersion = view.getUint32(0x10, true); + return { + type: readProtocolV2Ascii(bytes, 0x08, 4), + version: [ + Math.floor(packedVersion / 0x10000) % 0x100, + Math.floor(packedVersion / 0x100) % 0x100, + packedVersion % 0x100, + ], + payloadHash: bytesToHex( + bytes.slice( + PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET, + PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET + PROTOCOL_V2_OKPP_HASH_SIZE + ) + ), + headerHash: bytesToHex( + bytes.slice( + PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET, + PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET + PROTOCOL_V2_OKPP_HASH_SIZE + ) + ), + }; +}; + +/** + * FirmwareUpdateV4 is the complete Protocol V2 firmware update flow. + * + * It intentionally does not fall back to FirmwareUpdateV3/V1 behavior: + * - upload uses FilesystemFileWrite + * - install uses DeviceFirmwareUpdateRequest + * - completion waits for target status to finish, reboots to normal, then polls DeviceInfo + */ +export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod { + init() { + this.allowDeviceMode = [UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE]; + this.requireDeviceMode = []; + this.useDevicePassphraseState = false; + this.skipForceUpdateCheck = true; + + const { payload } = this; + + if (typeof payload.retryCount !== 'number') { + payload.retryCount = PROTOCOL_V2_CONNECT_RETRY_COUNT; + } + if (typeof payload.pollIntervalTime !== 'number') { + payload.pollIntervalTime = PROTOCOL_V2_CONNECT_POLL_INTERVAL; + } + if (typeof payload.timeout !== 'number') { + payload.timeout = PROTOCOL_V2_CONNECT_SINGLE_TIMEOUT; + } + if (typeof payload.protocolV2DeviceInfoTimeoutMs !== 'number') { + payload.protocolV2DeviceInfoTimeoutMs = PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT; + } + + validateParams(payload, [ + { name: 'chunkSize', type: 'number' }, + { name: 'forcedUpdateRes', type: 'boolean' }, + { name: 'bootloaderBinary', type: 'buffer' }, + { name: 'romloaderBinary', type: 'buffer' }, + { name: 'applicationP1Binary', type: 'buffer' }, + { name: 'applicationP2Binary', type: 'buffer' }, + { name: 'coprocessorBinary', type: 'buffer' }, + { name: 'se01Binary', type: 'buffer' }, + { name: 'se02Binary', type: 'buffer' }, + { name: 'se03Binary', type: 'buffer' }, + { name: 'se04Binary', type: 'buffer' }, + { name: 'firmwareType', type: 'string' }, + { name: 'targetsToUpdate', type: 'array', allowEmpty: true }, + { name: 'platform', type: 'string' }, + { name: 'resourceBundleFiles', type: 'array', allowEmpty: true }, + ]); + + this.params = { + chunkSize: payload.chunkSize, + forcedUpdateRes: payload.forcedUpdateRes, + bootloaderBinary: payload.bootloaderBinary, + romloaderBinary: payload.romloaderBinary, + applicationP1Binary: payload.applicationP1Binary, + applicationP2Binary: payload.applicationP2Binary, + coprocessorBinary: payload.coprocessorBinary, + se01Binary: payload.se01Binary, + se02Binary: payload.se02Binary, + se03Binary: payload.se03Binary, + se04Binary: payload.se04Binary, + resourceBundleFiles: payload.resourceBundleFiles, + firmwareType: payload.firmwareType, + targetsToUpdate: payload.targetsToUpdate, + platform: payload.platform, + }; + } + + private getProtocolV2FirmwareChunkSize() { + const payloadChunkSize = Number(this.params?.chunkSize); + const env = DataManager.getSettings('env'); + const maxChunkSize = + this.params?.platform === 'native' || (env && DataManager.isBleConnect(env)) + ? PROTOCOL_V2_BLE_FILE_CHUNK_SIZE + : PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE; + if (!Number.isFinite(payloadChunkSize) || payloadChunkSize <= 0) { + return maxChunkSize; + } + return Math.min( + Math.max(Math.floor(payloadChunkSize), PROTOCOL_V2_MIN_FILE_CHUNK_SIZE), + maxChunkSize + ); + } + + async run() { + if (!this.device.isProtocolV2()) { + throw ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + 'firmwareUpdateV4 requires a Protocol V2 device' + ); + } + + Log.debug('FirmwareUpdateV4 strategy: Protocol V2'); + return this.runProtocolV2(); + } + + private async runProtocolV2() { + const deviceFeatures = await this.getProtocolV2DeviceFeatures(); + const deviceFirmwareType = getFirmwareType(deviceFeatures); + const firmwareType = this.params.firmwareType ?? deviceFirmwareType; + + let fwBinaryMap: ProtocolV2TargetBinary[] = []; + let bootloaderBinary: ArrayBuffer | null = null; + let installItems: ProtocolV2InstallItem[] | undefined; + try { + this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware); + fwBinaryMap = this.collectExplicitTargetBinaries(); + bootloaderBinary = this.prepareBootloaderBinary(); + if (!this.hasExplicitProtocolV2Payload(fwBinaryMap)) { + const remoteBinaries = await this.prepareRemoteProtocolV2Binaries( + firmwareType, + deviceFeatures + ); + bootloaderBinary = remoteBinaries.bootloaderBinary; + fwBinaryMap = remoteBinaries.fwBinaryMap; + installItems = remoteBinaries.installItems; + } + this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware); + } catch (err) { + throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateDownloadFailed, err.message ?? err); + } + + const resourceBundles = this.prepareProtocolV2ResourceBundles(firmwareType, deviceFeatures); + + if (!bootloaderBinary && fwBinaryMap.length === 0 && !resourceBundles?.length) { + throw ERRORS.TypedError( + HardwareErrorCode.FirmwareUpdateDownloadFailed, + 'No firmware to update' + ); + } + + await this.enterProtocolV2BootloaderMode(); + + await this.executeProtocolV2Update({ + fwBinaryMap, + bootloaderBinary, + ...(installItems ? { installItems } : undefined), + ...(resourceBundles?.length ? { resourceBundles } : undefined), + }); + + await this.exitProtocolV2BootloaderToNormal(); + + const versions = await this.waitForProtocolV2FinalFeatures(); + this.postTipMessage(FirmwareUpdateTipMessage.FirmwareUpdateCompleted); + DevicePool.resetState(); + + return versions; + } + + private async getProtocolV2DeviceFeatures(): Promise { + if (this.device.features) { + return this.device.features; + } + if (typeof this.device.getFeatures === 'function') { + const features = await this.device.getFeatures(); + if (features) return features; + } + throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Device features not available'); + } + + private prepareBootloaderBinary(): ArrayBuffer | null { + return this.params.bootloaderBinary ?? null; + } + + private hasExplicitProtocolV2Payload(fwBinaryMap: ProtocolV2TargetBinary[]) { + return ( + !!this.params.resourceBundleFiles?.length || + !!this.params.bootloaderBinary || + fwBinaryMap.length > 0 + ); + } + + private buildProtocolV2InstallItems({ + bootloaderBinary, + fwBinaryMap, + }: { + bootloaderBinary: ArrayBuffer | null; + fwBinaryMap: ProtocolV2TargetBinary[]; + }): ProtocolV2InstallItem[] { + const installItems: ProtocolV2InstallItem[] = []; + + if (bootloaderBinary) { + installItems.push({ + fileName: 'bootloader.bin', + binary: bootloaderBinary, + targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER, + kind: 'bootloader', + }); + } + + installItems.push(...fwBinaryMap.map(item => ({ ...item, kind: 'firmware' as const }))); + return installItems; + } + + private getRemoteComponentEntries(release: IFirmwareReleaseInfo) { + const { components } = release; + if (!components) return []; + + const orderedKeys = [ + ...(release.installOrder ?? []), + ...Object.keys(components).filter(key => !release.installOrder?.includes(key)), + ]; + + return orderedKeys + .map(key => { + const component = components[key]; + return component ? ([key, component] as const) : undefined; + }) + .filter((entry): entry is readonly [string, IProtocolV2FirmwareComponent] => !!entry); + } + + private getRemoteComponentTarget(key: string, component: IProtocolV2FirmwareComponent) { + const targetName = component.target?.toUpperCase(); + if (targetName === 'ROMLOADER') { + throw ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + PROTOCOL_V2_ROMLOADER_UNSUPPORTED_MESSAGE + ); + } + const target = PROTOCOL_V2_REMOTE_COMPONENT_TARGETS[targetName]; + if (!target) { + throw ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + `Unsupported Protocol V2 firmware component target: ${key}/${component.target}` + ); + } + return target; + } + + private getProtocolV2ResourceFilePath(path: string) { + if (path.startsWith('vol')) return path; + if (path.startsWith('/')) return `vol0:${path}`; + return `vol0:/${path}`; + } + + private async readProtocolV2DeviceFileHeader(path: string) { + const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands()); + const filePath = this.getProtocolV2ResourceFilePath(path); + const pathInfoRes = await typedCall('FilesystemPathInfoQuery', 'FilesystemPathInfo', { + path: filePath, + }); + const fileSize = toProtocolV2FiniteNumber(pathInfoRes.message?.size); + if ( + !pathInfoRes.message?.exist || + pathInfoRes.message?.directory || + fileSize === undefined || + fileSize < PROTOCOL_V2_OKPP_HEADER_SIZE + ) { + return null; + } + + const chunkSize = this.getProtocolV2FirmwareChunkSize(); + const chunks: Uint8Array[] = []; + let offset = 0; + while (offset < PROTOCOL_V2_OKPP_HEADER_SIZE) { + const readLen = Math.min(chunkSize, PROTOCOL_V2_OKPP_HEADER_SIZE - offset); + const res = await typedCall('FilesystemFileRead', 'FilesystemFile', { + file: { + path: filePath, + offset, + total_size: 0, + }, + chunk_len: readLen, + ui_percentage: undefined, + }); + const data = toProtocolV2Bytes(res.message?.data); + if (data.byteLength === 0) return null; + chunks.push(data); + offset += data.byteLength; + } + + const headerBytes = new Uint8Array(offset); + let cursor = 0; + chunks.forEach(chunk => { + headerBytes.set(chunk, cursor); + cursor += chunk.byteLength; + }); + return parseProtocolV2OkppHeader(headerBytes); + } + + private async downloadRemoteProtocolV2Component( + key: string, + component: IProtocolV2FirmwareComponent + ): Promise { + const target = this.getRemoteComponentTarget(key, component); + if (!component.url) { + throw ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + `Missing Protocol V2 firmware component url: ${key}/${component.target}` + ); + } + + const { binary } = await getSysResourceBinary(component.url); + return { + ...target, + binary, + }; + } + + private async prepareRemoteProtocolV2Binaries(firmwareType: EFirmwareType, features: Features) { + const release = DataManager.getFirmwareLatestRelease(features, firmwareType); + + let bootloaderBinary: ArrayBuffer | null = null; + const fwBinaryMap: ProtocolV2TargetBinary[] = []; + const installItems: ProtocolV2InstallItem[] = []; + + if (!release) { + return { + bootloaderBinary, + fwBinaryMap, + installItems, + }; + } + + const entries = this.getRemoteComponentEntries(release); + const targetsToUpdate = new Set(this.params.targetsToUpdate ?? []); + + for (const [key, component] of entries) { + const target = this.getRemoteComponentTarget(key, component); + const updateTarget = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(target.targetId); + const shouldInstall = updateTarget ? targetsToUpdate.has(updateTarget) : false; + if (shouldInstall) { + const remoteBinary = await this.downloadRemoteProtocolV2Component(key, component); + if (remoteBinary.kind === 'bootloader') { + bootloaderBinary = remoteBinary.binary; + installItems.push({ + fileName: remoteBinary.fileName, + binary: remoteBinary.binary, + targetId: remoteBinary.targetId, + kind: remoteBinary.kind, + }); + } else { + const binaryEntry = { + fileName: remoteBinary.fileName, + binary: remoteBinary.binary, + targetId: remoteBinary.targetId, + }; + fwBinaryMap.push(binaryEntry); + installItems.push({ ...binaryEntry, kind: remoteBinary.kind }); + } + } + } + + return { + bootloaderBinary, + fwBinaryMap, + installItems, + }; + } + + // ============================================================ + // RESC bundle 直写增量同步(FileRead 比对 + FilesystemFileWrite 直写) + // ============================================================ + + /** + * 准备 RESC bundle 列表。 + * + * 两种模式(同 FirmwareUpdateV3 的 binary vs version 模式): + * - 用户传了 resourceBundleFiles(binary 数组):直接用,不做版本比对。 + * - 用户没传:从远端 config.json 的 release.resourceBundles 拉取, + * 此时 syncProtocolV2ResourceBundles 会按需下载 + 比对设备已有 header 跳过。 + */ + private prepareProtocolV2ResourceBundles( + firmwareType: EFirmwareType, + features: Features + ): ProtocolV2ResourceBundleBinary[] | undefined { + // 模式1:用户手动传入 binary,直接安装,不比对 + if (this.params.resourceBundleFiles?.length) { + return this.params.resourceBundleFiles.map(file => ({ + name: file.devicePath.split('/').pop() ?? file.devicePath, + binary: file.binary, + devicePath: file.devicePath, + })); + } + + if (!this.params.targetsToUpdate?.includes('resource')) { + return undefined; + } + + // 模式2:远端配置模式,后续按需下载 + 比对 + const release = DataManager.getFirmwareLatestRelease(features, firmwareType); + if (!release?.resourceBundles?.length) return undefined; + + return release.resourceBundles.map(bundle => ({ + name: bundle.name, + binary: new ArrayBuffer(0), + devicePath: bundle.devicePath, + url: bundle.url, + version: bundle.version, + payloadHash: bundle.payloadHash, + headerHash: bundle.headerHash, + })) as ProtocolV2ResourceBundleBinary[]; + } + + /** + * 同步 RESC bundles 到设备。 + * + * 手动传入模式(有 binary):直接 FileWrite 直写,不比对。 + * 远端配置模式(无 binary,有 url):按需下载 + FileRead 比对跳过已更新的。 + */ + private async syncProtocolV2ResourceBundles( + bundles: ProtocolV2ResourceBundleBinary[], + firmwareSize: number + ): Promise<{ processedSize: number; totalSize: number }> { + const transferStartTime = Date.now(); + const transferTransport = this.getProtocolV2FirmwareTransferTransport(); + + const isManualMode = bundles.every(b => b.binary.byteLength > 0); + let bundlesToSync = bundles; + + // 远端配置模式:按需下载 + 比对跳过 + if (!isManualMode) { + const filtered: ProtocolV2ResourceBundleBinary[] = []; + for (const bundle of bundles) { + // 下载 binary + if (bundle.binary.byteLength === 0 && bundle.url) { + Log.log(`[FirmwareUpdateV4] downloading RESC bundle ${bundle.name} from ${bundle.url}`); + const { binary } = await getSysResourceBinary(bundle.url); + bundle.binary = binary; + } + // 比对设备上已有 header + const upToDate = await this.isProtocolV2ResourceBundleUpToDate(bundle); + if (upToDate) { + Log.log(`[FirmwareUpdateV4] skip RESC bundle ${bundle.name}; already up to date`); + } else { + filtered.push(bundle); + } + } + bundlesToSync = filtered; + } + + if (bundlesToSync.length === 0) { + Log.log('[FirmwareUpdateV4] all RESC bundles up to date, nothing to sync'); + return { processedSize: 0, totalSize: firmwareSize }; + } + + // FileWrite 直写到设备 + let totalSize = 0; + for (const b of bundlesToSync) totalSize += b.binary.byteLength; + + const transferTotalSize = totalSize + firmwareSize; + let processedSize = 0; + for (const bundle of bundlesToSync) { + Log.log( + `[FirmwareUpdateV4] syncing RESC bundle ${bundle.name} -> ${bundle.devicePath} bytes=${bundle.binary.byteLength}` + ); + processedSize = await this.protocolV2CommonUpdateProcess({ + payload: bundle.binary, + filePath: bundle.devicePath, + processedSize, + totalSize: transferTotalSize, + }); + } + + const elapsedMs = Date.now() - transferStartTime; + Log.log( + `[FirmwareUpdateV4] RESC bundle sync finished transport=${transferTransport} bytes=${totalSize} elapsed=${( + elapsedMs / 1000 + ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(totalSize, elapsedMs)} KB/s` + ); + return { processedSize, totalSize: transferTotalSize }; + } + + /** + * 比对设备上已有 okpkg 的 OKPP header(仅远端配置模式使用)。 + */ + private async isProtocolV2ResourceBundleUpToDate( + bundle: ProtocolV2ResourceBundleBinary + ): Promise { + if (this.params.forcedUpdateRes) return false; + if (!bundle.version && !bundle.payloadHash) return false; + + try { + const header = await this.readProtocolV2DeviceFileHeader(bundle.devicePath); + if (!header) return false; + + if (bundle.version) { + const cmp = compareProtocolV2Versions(header.version, bundle.version); + if (cmp === undefined || cmp !== 0) return false; + } + if (bundle.payloadHash) { + const expected = normalizeProtocolV2Hex(bundle.payloadHash); + if (expected && header.payloadHash !== expected) return false; + } + if (bundle.headerHash) { + const expected = normalizeProtocolV2Hex(bundle.headerHash); + if (expected && header.headerHash !== expected) return false; + } + return true; + } catch (error) { + Log.log(`[FirmwareUpdateV4] RESC bundle ${bundle.name} header check failed: `, error); + return false; + } + } + + private isProtocolV2BootloaderMode() { + if (typeof this.device.isBootloader === 'function') { + return this.device.isBootloader(); + } + return !!this.device.features?.bootloaderMode; + } + + async enterProtocolV2BootloaderMode() { + if (this.isProtocolV2BootloaderMode()) { + Log.debug('Protocol V2 device is already in bootloader mode, skip reboot to bootloader'); + return false; + } + + try { + this.postTipMessage(FirmwareUpdateTipMessage.AutoRebootToBootloader); + await this.protocolV2Reboot(DeviceRebootType.Bootloader); + this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess); + await wait(1000); + await this.waitForProtocolV2BootloaderMode(); + return true; + } catch (error) { + if (error instanceof HardwareError) { + throw error; + } + Log.log('Protocol V2 auto go to bootloader mode failed: ', error); + throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateAutoEnterBootFailure); + } + } + + private async waitForProtocolV2BootloaderMode( + timeout = PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT, + retryInterval = 1000 + ) { + const startTime = Date.now(); + let lastError: unknown; + + while (Date.now() - startTime < timeout) { + try { + await this.reconnectProtocolV2Device(); + const deviceInfo = await requestProtocolV2DeviceInfo({ + commands: this.device.getCommands(), + timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT, + }); + const features = this.device.updateProtocolV2Features(deviceInfo); + if (features?.bootloaderMode) { + return features; + } + lastError = new Error('Protocol V2 device is reachable but is not in bootloader mode'); + } catch (error) { + lastError = error; + Log.log('Protocol V2 bootloader mode not ready, polling reconnect: ', error); + } + await wait(retryInterval); + } + + throw ERRORS.TypedError( + HardwareErrorCode.FirmwareUpdateAutoEnterBootFailure, + `Protocol V2 bootloader not ready within ${timeout / 1000}s: ${this.normalizeErrorMessage( + lastError + )}` + ); + } + + /** + * 收集按 DeviceFirmwareTargetType 拆分的显式目标二进制。 + * 文件名仅用于 staging 路径展示,target_id 已显式给定。 + */ + private collectExplicitTargetBinaries() { + const entries: ProtocolV2TargetBinary[] = []; + const push = (binary: ArrayBuffer | undefined, fileName: string, targetId: number) => { + if (binary) entries.push({ fileName, binary, targetId }); + }; + + if (this.params.romloaderBinary) { + throw ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + PROTOCOL_V2_ROMLOADER_UNSUPPORTED_MESSAGE + ); + } + push( + this.params.applicationP1Binary, + 'application_p1.bin', + ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1 + ); + push( + this.params.applicationP2Binary, + 'application_p2.bin', + ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2 + ); + push( + this.params.coprocessorBinary, + 'coprocessor.bin', + ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_COPROCESSOR + ); + push(this.params.se01Binary, 'se01.bin', ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE01); + push(this.params.se02Binary, 'se02.bin', ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE02); + push(this.params.se03Binary, 'se03.bin', ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE03); + push(this.params.se04Binary, 'se04.bin', ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE04); + return entries; + } + + private async executeProtocolV2Update({ + fwBinaryMap, + bootloaderBinary, + installItems, + resourceBundles, + }: { + fwBinaryMap?: ProtocolV2TargetBinary[]; + bootloaderBinary?: ArrayBuffer | null; + installItems?: ProtocolV2InstallItem[]; + resourceBundles?: ProtocolV2ResourceBundleBinary[]; + }) { + const orderedInstallItems = + installItems ?? + this.buildProtocolV2InstallItems({ + bootloaderBinary: bootloaderBinary ?? null, + fwBinaryMap: fwBinaryMap ?? [], + }); + + let firmwareSize = 0; + for (const item of orderedInstallItems) firmwareSize += item.binary.byteLength; + + this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData); + + let processedSize = 0; + let totalSize = firmwareSize; + if (resourceBundles?.length) { + const resourceTransfer = await this.syncProtocolV2ResourceBundles( + resourceBundles, + firmwareSize + ); + processedSize = resourceTransfer.processedSize; + totalSize = resourceTransfer.totalSize; + } + + // 只有 RESC bundle、没有固件 target 时跳过 staging/install 阶段 + if (orderedInstallItems.length === 0) { + Log.log('[FirmwareUpdateV4] no firmware targets to install (RESC bundles only)'); + if (totalSize > 0) { + this.postProgressMessage(100, 'transferData'); + } + return; + } + let transferredSize = processedSize; + const transferStartTime = Date.now(); + const transferTransport = this.getProtocolV2FirmwareTransferTransport(); + const chunkSize = this.getProtocolV2FirmwareChunkSize(); + const onTransferredBytes = (bytes: number) => { + transferredSize = bytes; + }; + Log.log( + `[FirmwareUpdateV4] transfer started transport=${transferTransport} total=${totalSize} bytes chunk=${chunkSize} bytes` + ); + + const stagedInstallTargets: ProtocolV2InstallTarget[] = []; + + try { + for (const item of orderedInstallItems) { + const filePath = this.getProtocolV2InstallItemStagingPath(item); + Log.log( + `[FirmwareUpdateV4] staging ${item.kind} via FilesystemFileWrite target=${item.targetId} path=${filePath} source=${item.fileName} bytes=${item.binary.byteLength}` + ); + processedSize = await this.protocolV2CommonUpdateProcess({ + payload: item.binary, + filePath, + processedSize, + totalSize, + onTransferredBytes, + }); + transferredSize = processedSize; + await this.verifyProtocolV2StagedFile(filePath, item.binary.byteLength); + + stagedInstallTargets.push({ + ...item, + path: filePath, + }); + } + + if (totalSize > 0) { + this.postProgressMessage(100, 'transferData'); + } + + const elapsedMs = Date.now() - transferStartTime; + Log.log( + `[FirmwareUpdateV4] transfer finished transport=${transferTransport} bytes=${totalSize} elapsed=${( + elapsedMs / 1000 + ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(totalSize, elapsedMs)} KB/s` + ); + } catch (error) { + const elapsedMs = Date.now() - transferStartTime; + Log.warn( + `[FirmwareUpdateV4] transfer failed transport=${transferTransport} bytes=${transferredSize}/${totalSize} elapsed=${( + elapsedMs / 1000 + ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(transferredSize, elapsedMs)} KB/s` + ); + throw error; + } + + this.postTipMessage(FirmwareUpdateTipMessage.ConfirmOnDevice); + + const allTargets = stagedInstallTargets.map(item => ({ + target_id: item.targetId, + path: item.path, + })); + Log.log(`[FirmwareUpdateV4] DeviceFirmwareUpdateRequest targets=${JSON.stringify(allTargets)}`); + const startResponse = await this.protocolV2StartFirmwareUpdate({ targets: allTargets }); + await this.waitForProtocolV2FirmwareUpdateComplete(allTargets, startResponse); + } + + private getProtocolV2InstallItemStagingPath(item: ProtocolV2InstallItem) { + return `${PROTOCOL_V2_FIRMWARE_STAGING_VOLUME}${item.fileName}`; + } + + private async verifyProtocolV2StagedFile(path: string, expectedSize: number) { + const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands()); + const response = await typedCall('FilesystemPathInfoQuery', 'FilesystemPathInfo', { path }); + const actualSize = toProtocolV2FiniteNumber(response.message?.size); + if (!response.message?.exist || response.message?.directory || actualSize !== expectedSize) { + throw ERRORS.TypedError( + HardwareErrorCode.EmmcFileWriteFirmwareError, + `staged file verification failed: path=${path} exist=${!!response.message + ?.exist} expected=${expectedSize} actual=${actualSize ?? 'unknown'}` + ); + } + } + + private async queryProtocolV2FirmwareUpdateStatus() { + const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands()); + return typedCall( + 'DeviceFirmwareUpdateStatusGet', + 'DeviceFirmwareUpdateStatus', + {}, + { + timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT, + } + ); + } + + private async pingProtocolV2Device() { + const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands()); + await typedCall( + 'Ping', + 'Success', + { message: 'firmware-update' }, + { + timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT, + } + ); + } + + private isProtocolV2NormalModeFeatures(features?: Features | null) { + return !!features && !features.bootloaderMode && features.mode !== 'bootloader'; + } + + private async probeProtocolV2NormalMode() { + const deviceInfo = await requestProtocolV2DeviceInfo({ + commands: this.device.getCommands(), + timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT, + request: PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST, + }); + const features = this.device.updateProtocolV2Features(deviceInfo); + if (this.isProtocolV2NormalModeFeatures(features)) { + Log.log('Protocol V2 firmware install finished; device is back in normal mode'); + return true; + } + return false; + } + + private assertProtocolV2TargetStatus( + statusTargets: ProtocolV2FirmwareUpdateStatusTarget[], + expectedTargetIds: Set + ) { + const failedTarget = statusTargets.find( + target => + expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1) && + isProtocolV2TargetStatusFailed(target.status) + ); + if (failedTarget) { + throw ERRORS.TypedError( + HardwareErrorCode.FirmwareError, + `Protocol V2 firmware target ${failedTarget.target_id} failed` + ); + } + + const completedTargets = statusTargets.filter( + target => + expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1) && + isProtocolV2TargetStatusFinished(target.status) + ); + if (completedTargets.length === expectedTargetIds.size && expectedTargetIds.size > 0) { + return true; + } + + const inProgressTarget = statusTargets.find( + target => + expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1) && + isProtocolV2TargetStatusInProgress(target.status) + ); + if (inProgressTarget) { + this.postProgressMessage(99, 'installingFirmware'); + } + + return false; + } + + private async waitForProtocolV2FirmwareUpdateComplete( + targets: Array<{ target_id: number; path: string }>, + startResponse?: ProtocolV2FirmwareUpdateStartResponse + ) { + const expectedTargetIds = new Set(targets.map(target => target.target_id)); + if (startResponse?.type === 'DeviceFirmwareUpdateStatus') { + const statusTargets = (startResponse.message.records ?? + []) as ProtocolV2FirmwareUpdateStatusTarget[]; + if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds)) { + return; + } + } + + const startTime = Date.now(); + let lastError: unknown; + + while (Date.now() - startTime < PROTOCOL_V2_INSTALL_TIMEOUT) { + try { + const statusRes = await this.queryProtocolV2FirmwareUpdateStatus(); + const statusTargets = (statusRes.message.records ?? + []) as ProtocolV2FirmwareUpdateStatusTarget[]; + if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds)) { + return; + } + } catch (error) { + lastError = error; + if (error instanceof HardwareError && error.errorCode === HardwareErrorCode.FirmwareError) { + throw error; + } + Log.log('Protocol V2 firmware install status polling failed: ', error); + if (isProtocolV2PollingTransientError(error)) { + try { + await this.reconnectProtocolV2Device(); + if (await this.probeProtocolV2NormalMode()) { + return; + } + } catch (reconnectError) { + lastError = reconnectError; + Log.log( + 'Protocol V2 firmware install reconnect/normal-mode probe failed: ', + reconnectError + ); + } + try { + await this.pingProtocolV2Device(); + Log.log('Protocol V2 firmware status unavailable, Ping is ready'); + } catch (pingError) { + lastError = pingError; + Log.log('Protocol V2 firmware install Ping polling failed: ', pingError); + } + } + } + await wait(1000); + } + + throw ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + `Protocol V2 firmware update status timeout: ${this.normalizeErrorMessage(lastError)}` + ); + } + + private async exitProtocolV2BootloaderToNormal() { + this.postTipMessage(FirmwareUpdateTipMessage.SwitchFirmwareReconnectDevice); + try { + await this.reconnectProtocolV2Device(); + if (await this.probeProtocolV2NormalMode()) { + Log.log('Protocol V2 device is already in normal mode, skip normal reboot'); + return; + } + } catch (error) { + Log.log('Protocol V2 normal-mode probe before reboot failed: ', error); + } + await this.protocolV2Reboot(DeviceRebootType.Normal); + } + + private async waitForProtocolV2FinalFeatures() { + const features = await this.waitForProtocolV2ReconnectAndFeatures( + PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT + ); + + const bootloaderVersion = getDeviceBootloaderVersion(features).join('.'); + const bleVersion = getDeviceBLEFirmwareVersion(features).join('.'); + const firmwareVersion = getDeviceFirmwareVersion(features).join('.'); + if (firmwareVersion === '0.0.0') { + Log.warn( + 'Protocol V2 firmware update finished but app firmware version is still 0.0.0. This is allowed for Pro2 debug BLE-only update flows.' + ); + } + + return { + bootloaderVersion, + bleVersion, + firmwareVersion, + }; + } + + private async waitForProtocolV2ReconnectAndFeatures(timeout: number) { + const startTime = Date.now(); + let lastError: unknown; + + while (Date.now() - startTime < timeout) { + try { + await this.reconnectProtocolV2Device(); + const deviceInfo = await requestProtocolV2DeviceInfo({ + commands: this.device.getCommands(), + timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT, + // 更新完成判定只需要各 target 版本号;scope 与请求内容保持一致 + request: PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST, + }); + const features = this.device.updateProtocolV2Features(deviceInfo); + if (features.bootloaderMode || features.mode === 'bootloader') { + throw ERRORS.TypedError( + HardwareErrorCode.DeviceNotFound, + 'Protocol V2 device is still in bootloader mode' + ); + } + return features; + } catch (error) { + lastError = error; + Log.log('Protocol V2 normal mode not ready, polling Ping: ', error); + await wait(1000); + } + } + + throw ERRORS.TypedError( + HardwareErrorCode.DeviceNotFound, + `Protocol V2 final features not ready within ${timeout / 1000}s: ${this.normalizeErrorMessage( + lastError + )}` + ); + } + + private async reconnectProtocolV2Device() { + if (this.isBleReconnect()) { + await this.acquireProtocolV2BleDevice(); + return; + } + + const deviceDiff = await this.device.deviceConnector?.enumerate(); + const devicesDescriptor = deviceDiff?.descriptors ?? []; + + if ( + DataManager.isBrowserWebUsb(DataManager.getSettings('env')) && + devicesDescriptor.length === 1 + ) { + this.device.updateDescriptor( + { + ...devicesDescriptor[0], + protocolType: PROTOCOL_V2_CONNECT_PROTOCOL, + }, + true + ); + await this.device.acquire(PROTOCOL_V2_CONNECT_PROTOCOL, { throwOnRunPromiseError: true }); + this.device.commands.disposed = false; + this.device.getCommands().mainId = this.device.mainId ?? ''; + await this.device.initialize(); + return; + } + + // App 与 bootloader 序列号暂时可能不一致。V4 升级重连阶段只接受唯一枚举设备, + // 避免继续按旧 app connectId 查缓存导致反复输出 path mismatch 日志。 + const { deviceList } = await DevicePool.getDevices(devicesDescriptor, undefined, { + connectProtocol: PROTOCOL_V2_CONNECT_PROTOCOL, + }); + if (deviceList.length !== 1) { + throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound); + } + + Log.debug( + 'Protocol V2 firmware reconnect using single enumerated device:', + deviceList[0].getConnectId() + ); + this.device.updateFromCache(deviceList[0]); + await this.device.acquire(PROTOCOL_V2_CONNECT_PROTOCOL, { throwOnRunPromiseError: true }); + this.device.commands.disposed = false; + this.device.getCommands().mainId = this.device.mainId ?? ''; + await this.device.initialize(); + } + + private async protocolV2CommonUpdateProcess(params: ProtocolV2FileTransferParams) { + let lastError: unknown; + for (let attempt = 1; attempt <= PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT; attempt += 1) { + try { + return await this.protocolV2WriteWholeFile(params); + } catch (error) { + lastError = error; + Log.error( + `Protocol V2 file transfer failed path=${params.filePath} attempt=${attempt}/${PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT}; restarting from offset 0`, + error + ); + if (attempt === PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) { + break; + } + await this.recoverProtocolV2FileTransfer(); + } + } + + throw ERRORS.TypedError( + HardwareErrorCode.EmmcFileWriteFirmwareError, + `transfer data error: ${getProtocolV2UnknownErrorText(lastError)}` + ); + } + + private async protocolV2WriteWholeFile({ + payload, + filePath, + processedSize, + totalSize, + onTransferredBytes, + }: ProtocolV2FileTransferParams) { + const chunkSize = this.getProtocolV2FirmwareChunkSize(); + let offset = 0; + const getUploadProgress = (fileOffset: number) => { + if (totalSize !== undefined && processedSize !== undefined) { + return Math.min(Math.ceil(((processedSize + fileOffset) / totalSize) * 100), 99); + } + return Math.min(Math.ceil((fileOffset / payload.byteLength) * 100), 99); + }; + + while (offset < payload.byteLength) { + const chunkEnd = Math.min(offset + chunkSize, payload.byteLength); + const chunkLength = chunkEnd - offset; + const chunk = payload.slice(offset, chunkEnd); + const overwrite = offset === 0; + const progress = getProtocolV2DeviceTransferProgress( + (processedSize ?? 0) + offset, + (processedSize ?? 0) + chunkEnd, + totalSize ?? payload.byteLength + ); + + const writeRes = await this.fileWriteChunk( + filePath, + payload.byteLength, + offset, + chunk, + overwrite, + progress + ); + const processedByte = Number(writeRes.message.processed_byte); + const nextOffset = + Number.isFinite(processedByte) && processedByte > offset + ? processedByte + : offset + chunkLength; + if (nextOffset <= offset || nextOffset > payload.byteLength) { + throw ERRORS.TypedError( + HardwareErrorCode.EmmcFileWriteFirmwareError, + `invalid processed_byte ${writeRes.message.processed_byte} for offset ${offset}` + ); + } + offset = nextOffset; + onTransferredBytes?.((processedSize ?? 0) + offset); + this.postProgressMessage(getUploadProgress(offset), 'transferData'); + } + + return totalSize !== undefined ? (processedSize ?? 0) + payload.byteLength : 0; + } + + private getProtocolV2FirmwareTransferTransport() { + const env = DataManager.getSettings('env'); + if (env && DataManager.isBleConnect(env)) { + return 'BLE'; + } + if ( + env && + (DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env) || env === 'web') + ) { + return 'WebUSB'; + } + return env ?? 'unknown'; + } + + private async fileWriteChunk( + filePath: string, + totalFileSize: number, + offset: number, + chunk: ArrayBuffer | Buffer, + overwrite: boolean, + progress: number | null + ): Promise> { + const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands()); + const writeRes = await typedCall('FilesystemFileWrite', 'FilesystemFile', { + file: { + path: filePath, + offset, + total_size: totalFileSize, + data: chunk, + }, + overwrite, + append: false, + ui_percentage: progress ?? undefined, + }); + if (writeRes.type !== 'FilesystemFile') { + if ((writeRes as any).type === 'CallMethodError') { + if (((writeRes as any).message.error ?? '').indexOf(SESSION_ERROR) > -1) { + throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, SESSION_ERROR); + } + } + throw ERRORS.TypedError(HardwareErrorCode.EmmcFileWriteFirmwareError, 'transfer data error'); + } + return writeRes; + } + + private async recoverProtocolV2FileTransfer() { + const env = DataManager.getSettings('env'); + if (DataManager.isBleConnect(env)) { + await wait(3000); + await this.acquireProtocolV2BleDevice(); + await this.device.initialize(); + } + await wait(2000); + } + + private async acquireProtocolV2BleDevice() { + await this.device.deviceConnector?.acquire( + this.device.originalDescriptor.id, + null, + true, + PROTOCOL_V2_CONNECT_PROTOCOL + ); + } + + private async protocolV2StartFirmwareUpdate({ + targets, + }: { + targets: Array<{ target_id: number; path: string }>; + }) { + const commands = this.device.getCommands(); + let response: ProtocolV2FirmwareUpdateStartResponse; + try { + response = await commands.typedCall( + 'DeviceFirmwareUpdateRequest', + PROTOCOL_V2_FIRMWARE_UPDATE_RESPONSE_TYPES, + { + targets, + }, + { + intermediateTypes: ['DeviceFirmwareUpdateStatus'], + timeoutMs: PROTOCOL_V2_START_UPDATE_TIMEOUT, + onIntermediateResponse: (response: { type?: string }) => { + if (response.type === 'DeviceFirmwareUpdateStatus') { + this.postProgressMessage(99, 'installingFirmware'); + } + }, + } + ); + } catch (error) { + if (isProtocolV2StartUpdateTransientError(error)) { + Log.log( + 'Protocol V2 firmware update request did not return; continue status polling', + error + ); + } else { + throw error; + } + } + this.postTipMessage(FirmwareUpdateTipMessage.FirmwareUpdating); + return response; + } + + private async protocolV2Reboot(rebootType: DeviceRebootType) { + const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands()); + try { + const res = await typedCall('DeviceReboot', 'Success', { + reboot_type: rebootType, + }); + return res.message; + } catch (error) { + if (isProtocolV2DeviceDisconnectedError(error) || isProtocolV2ReconnectProbeError(error)) { + return { message: 'Device rebooted successfully' }; + } + throw error; + } + } + + private normalizeErrorMessage(error: unknown): string { + if (!error) { + return ''; + } + return getProtocolV2UnknownErrorText(error); + } +} diff --git a/packages/core/src/api/GetDeviceInfo.ts b/packages/core/src/api/GetDeviceInfo.ts new file mode 100644 index 000000000..ff26bab9d --- /dev/null +++ b/packages/core/src/api/GetDeviceInfo.ts @@ -0,0 +1,145 @@ +import semver from 'semver'; +import { EDeviceType, ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared'; + +import { UI_REQUEST } from '../constants/ui-request'; +import { + PROTOCOL_V2_DEVICE_INFO_REQUEST, + PROTOCOL_V2_FEATURES_DEVICE_INFO_REQUEST, + PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST, +} from '../protocols/protocol-v2'; +import { requestProtocolV2DeviceInfo } from '../protocols/protocol-v2/features'; +import { buildProfileFromProtocolV1, buildProfileFromProtocolV2 } from '../deviceProfile'; +import { getDeviceType } from '../utils'; +import { fixVersion } from '../utils/deviceFeaturesUtils'; +import { BaseMethod } from './BaseMethod'; + +import type { + DeviceInfoScope, + DeviceInfoSource, + GetDeviceInfoParams, +} from '../types/api/getDeviceInfo'; +import type { Features, OnekeyFeatures } from '../types'; + +const DEVICE_INFO_SCOPES: readonly DeviceInfoScope[] = ['basic', 'versions', 'verify', 'full']; + +function isDeviceInfoScope(scope: unknown): scope is DeviceInfoScope { + return typeof scope === 'string' && DEVICE_INFO_SCOPES.includes(scope as DeviceInfoScope); +} + +function normalizeScope(scope: unknown): GetDeviceInfoParams['scope'] { + if (scope === undefined || scope === null) return 'basic'; + if (isDeviceInfoScope(scope)) { + return scope; + } + throw ERRORS.TypedError( + HardwareErrorCode.CallMethodInvalidParameter, + `Invalid getDeviceInfo scope: ${String(scope)}` + ); +} + +function resolveProtocolV2DeviceInfoRequest(params: GetDeviceInfoParams) { + if (params.scope === 'verify' || params.scope === 'full') { + return PROTOCOL_V2_DEVICE_INFO_REQUEST; + } + if (params.scope === 'versions') { + return PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST; + } + return PROTOCOL_V2_FEATURES_DEVICE_INFO_REQUEST; +} + +function shouldReadOnekeyFeatures(params: GetDeviceInfoParams) { + return ( + params.includeRaw === true || + params.scope === 'versions' || + params.scope === 'verify' || + params.scope === 'full' + ); +} + +function supportOnekeyFeatures(features?: Features) { + if (!features || features.bootloaderMode) return false; + + const deviceType = getDeviceType(features); + return ![ + EDeviceType.Unknown, + EDeviceType.Classic1s, + EDeviceType.ClassicPure, + EDeviceType.Pro2, + ].includes(deviceType); +} + +function normalizeOnekeyFeatures(message: OnekeyFeatures) { + if (message.onekey_firmware_version && !semver.valid(message.onekey_firmware_version)) { + message.onekey_firmware_version = fixVersion(message.onekey_firmware_version); + } + return message; +} + +export default class GetDeviceInfo extends BaseMethod { + init() { + this.allowDeviceMode = [ + ...this.allowDeviceMode, + UI_REQUEST.NOT_INITIALIZE, + UI_REQUEST.BOOTLOADER, + ]; + this.useDevicePassphraseState = false; + this.skipForceUpdateCheck = true; + this.params = { + scope: normalizeScope(this.payload.scope), + refresh: this.payload.refresh, + includeRaw: this.payload.includeRaw, + }; + } + + async run() { + if (this.device.isProtocolV2()) { + return this.runProtocolV2(); + } + return this.runProtocolV1(); + } + + private async runProtocolV2() { + const sources: DeviceInfoSource[] = ['deviceInfo']; + const protocolV2DeviceInfo = await requestProtocolV2DeviceInfo({ + commands: this.device.commands, + request: resolveProtocolV2DeviceInfoRequest(this.params), + }); + const profile = buildProfileFromProtocolV2({ + deviceInfo: protocolV2DeviceInfo, + sources, + scope: this.params.scope, + includeRaw: this.params.includeRaw, + }); + this.device.updateProtocolV2Features(protocolV2DeviceInfo); + return profile; + } + + private async runProtocolV1() { + if (this.params.refresh === true) { + await this.device.getFeatures(); + } + + const sources: DeviceInfoSource[] = ['features']; + const { features } = this.device; + let protocolV1OneKeyFeatures: OnekeyFeatures | undefined; + + if (shouldReadOnekeyFeatures(this.params) && supportOnekeyFeatures(features)) { + const { message } = await this.device.commands.typedCall( + 'OnekeyGetFeatures', + 'OnekeyFeatures' + ); + protocolV1OneKeyFeatures = normalizeOnekeyFeatures(message); + sources.push('protocolV1OneKeyFeatures'); + } + + const profile = buildProfileFromProtocolV1({ + protocol: 'V1', + features, + protocolV1OneKeyFeatures, + sources, + scope: this.params.scope, + includeRaw: this.params.includeRaw, + }); + return profile; + } +} diff --git a/packages/core/src/api/GetFeatures.ts b/packages/core/src/api/GetFeatures.ts index cfe6ed3f9..59bec3bfe 100644 --- a/packages/core/src/api/GetFeatures.ts +++ b/packages/core/src/api/GetFeatures.ts @@ -14,10 +14,13 @@ export default class GetFeatures extends BaseMethod { this.skipForceUpdateCheck = true; } - run() { - if (this.payload?.detectBootloaderDevice && this.device.features?.bootloader_mode) { + async run() { + if (this.payload?.detectBootloaderDevice && this.device.isBootloader()) { return Promise.reject(ERRORS.TypedError(HardwareErrorCode.DeviceDetectInBootloaderMode)); } + if (this.device.isProtocolV2()) { + return this.device.getFeatures(); + } return Promise.resolve(this.device.features); } } diff --git a/packages/core/src/api/GetOnekeyFeatures.ts b/packages/core/src/api/GetOnekeyFeatures.ts index ef744b003..81e18ddbc 100644 --- a/packages/core/src/api/GetOnekeyFeatures.ts +++ b/packages/core/src/api/GetOnekeyFeatures.ts @@ -2,8 +2,80 @@ import semver from 'semver'; import { UI_REQUEST } from '../constants/ui-request'; import { fixVersion } from '../utils/deviceFeaturesUtils'; +import { PROTOCOL_V2_DEVICE_INFO_REQUEST } from '../protocols/protocol-v2'; +import { requestProtocolV2DeviceInfo } from '../protocols/protocol-v2/features'; import { BaseMethod } from './BaseMethod'; +import type { OnekeyFeatures } from '../types'; + +const ONEKEY_FEATURE_KEYS: Array = [ + 'onekey_device_type', + 'onekey_board_version', + 'onekey_boot_version', + 'onekey_firmware_version', + 'onekey_board_hash', + 'onekey_boot_hash', + 'onekey_firmware_hash', + 'onekey_board_build_id', + 'onekey_boot_build_id', + 'onekey_firmware_build_id', + 'onekey_serial_no', + 'onekey_ble_name', + 'onekey_ble_version', + 'onekey_ble_build_id', + 'onekey_ble_hash', + 'onekey_se_type', + 'onekey_se01_state', + 'onekey_se02_state', + 'onekey_se03_state', + 'onekey_se04_state', + 'onekey_se01_version', + 'onekey_se02_version', + 'onekey_se03_version', + 'onekey_se04_version', + 'onekey_se01_hash', + 'onekey_se02_hash', + 'onekey_se03_hash', + 'onekey_se04_hash', + 'onekey_se01_build_id', + 'onekey_se02_build_id', + 'onekey_se03_build_id', + 'onekey_se04_build_id', + 'onekey_se01_boot_version', + 'onekey_se02_boot_version', + 'onekey_se03_boot_version', + 'onekey_se04_boot_version', + 'onekey_se01_boot_hash', + 'onekey_se02_boot_hash', + 'onekey_se03_boot_hash', + 'onekey_se04_boot_hash', + 'onekey_se01_boot_build_id', + 'onekey_se02_boot_build_id', + 'onekey_se03_boot_build_id', + 'onekey_se04_boot_build_id', +]; + +function normalizeOnekeyFirmwareVersion(message: OnekeyFeatures) { + if (message.onekey_firmware_version && !semver.valid(message.onekey_firmware_version)) { + message.onekey_firmware_version = fixVersion(message.onekey_firmware_version); + } +} + +function pickOnekeyFeatures(features?: OnekeyFeatures | null): OnekeyFeatures { + const message: OnekeyFeatures = {}; + if (!features) return message; + + for (const key of ONEKEY_FEATURE_KEYS) { + const value = features[key]; + if (value !== undefined && value !== null) { + (message as Record)[key] = value; + } + } + + normalizeOnekeyFirmwareVersion(message); + return message; +} + export default class GetOnekeyFeatures extends BaseMethod { init() { this.allowDeviceMode = [ @@ -16,10 +88,19 @@ export default class GetOnekeyFeatures extends BaseMethod { } async run() { - const { message } = await this.device.commands.typedCall('OnekeyGetFeatures', 'OnekeyFeatures'); - if (!!message.onekey_firmware_version && !semver.valid(message.onekey_firmware_version)) { - message.onekey_firmware_version = fixVersion(message.onekey_firmware_version); + if (this.device.isProtocolV2()) { + // V2 没有 OnekeyGetFeatures 消息: + // 取完整 DeviceInfoGet(含 SE/hash/build_id)后刷新结构化 features 缓存。 + const deviceInfo = await requestProtocolV2DeviceInfo({ + commands: this.device.commands, + request: PROTOCOL_V2_DEVICE_INFO_REQUEST, + }); + const features = this.device.updateProtocolV2Features(deviceInfo); + return pickOnekeyFeatures(features as OnekeyFeatures); } + + const { message } = await this.device.commands.typedCall('OnekeyGetFeatures', 'OnekeyFeatures'); + normalizeOnekeyFirmwareVersion(message); return Promise.resolve(message); } } diff --git a/packages/core/src/api/GetPassphraseState.ts b/packages/core/src/api/GetPassphraseState.ts index cba0608f0..50653ee9f 100644 --- a/packages/core/src/api/GetPassphraseState.ts +++ b/packages/core/src/api/GetPassphraseState.ts @@ -1,4 +1,4 @@ -import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared'; +import { EDeviceType } from '@onekeyfe/hd-shared'; import { UI_REQUEST } from '../constants/ui-request'; import { getPassphraseStateWithRefreshDeviceInfo } from '../utils/deviceFeaturesUtils'; @@ -11,18 +11,18 @@ export default class GetPassphraseState extends BaseMethod { } async run() { - if (!this.device.features) - return Promise.reject(ERRORS.TypedError(HardwareErrorCode.DeviceInitializeFailed)); + const { passphraseState } = await getPassphraseStateWithRefreshDeviceInfo(this.device, { + expectPassphraseState: this.payload.passphraseState, + onlyMainPin: this.payload.useEmptyPassphrase, + initSession: this.payload.initSession, + }); - const { passphraseState } = await getPassphraseStateWithRefreshDeviceInfo(this.device); + const passphraseProtection = this.device.getCurrentPassphraseProtection(); + const deviceType = this.device.getCurrentDeviceType(); + const isProSeries = deviceType === EDeviceType.Pro || deviceType === EDeviceType.Pro2; - const { features } = this.device; - - // refresh device info - if (features && features.passphrase_protection === true) { - return Promise.resolve(passphraseState); - } - - return Promise.resolve(undefined); + return Promise.resolve( + isProSeries || passphraseProtection === true ? passphraseState : undefined + ); } } diff --git a/packages/core/src/api/PathInfo.ts b/packages/core/src/api/PathInfo.ts new file mode 100644 index 000000000..cd4aec203 --- /dev/null +++ b/packages/core/src/api/PathInfo.ts @@ -0,0 +1,39 @@ +import { BaseMethod } from './BaseMethod'; +import { + validateNonEmptyString, + validateOptionalNonNegativeInteger, +} from './helpers/filesystemValidation'; + +export type PathInfoParams = { + path: string; + timeoutMs?: number | string; +}; + +export default class PathInfo extends BaseMethod { + init() { + // Protocol V2 (Pro2) 专属方法,core 调度层统一做非 V2 设备守卫 + this.requireProtocolV2 = true; + this.skipForceUpdateCheck = true; + this.useDevicePassphraseState = false; + this.params = { + path: validateNonEmptyString(this.payload.path, 'path'), + timeoutMs: validateOptionalNonNegativeInteger(this.payload.timeoutMs, 'timeoutMs'), + }; + } + + async run() { + const timeoutMs = + this.params.timeoutMs === undefined ? undefined : Number(this.params.timeoutMs); + const res = await this.device.commands.typedCall( + 'FilesystemPathInfoQuery', + 'FilesystemPathInfo', + { + path: this.params.path, + }, + { + timeoutMs, + } + ); + return Promise.resolve(res.message); + } +} diff --git a/packages/core/src/api/PromptWebDeviceAccess.ts b/packages/core/src/api/PromptWebDeviceAccess.ts index e5a7d680d..7315d01f5 100644 --- a/packages/core/src/api/PromptWebDeviceAccess.ts +++ b/packages/core/src/api/PromptWebDeviceAccess.ts @@ -51,9 +51,19 @@ export default class PromptWebDeviceAccess extends BaseMethod { } if (isWebUsbEnv) { + const usbDevice = device as USBDevice; + let path = usbDevice.serialNumber ?? ''; + if (!path) { + // 早期 Pro2 工程板 USB descriptor 没有 serial number。 + // 授权后重新枚举,transport 会为空 serial 设备生成会话内稳定的 mock path, + // 这里按 USBDevice 对象身份找回该 path,保证后续 acquire 能匹配。 + const diff = await this.connector?.enumerate(); + const matched = diff?.descriptors?.find(d => (d as any).device === usbDevice); + path = matched?.path ?? ''; + } devicesDescriptor = [ { - path: (device as USBDevice).serialNumber ?? '', + path, device, debug: true, }, diff --git a/packages/core/src/api/SearchDevices.ts b/packages/core/src/api/SearchDevices.ts index f15e917e8..67a56ff03 100644 --- a/packages/core/src/api/SearchDevices.ts +++ b/packages/core/src/api/SearchDevices.ts @@ -34,17 +34,22 @@ export default class SearchDevices extends BaseMethod { const lowerId = device.id?.toLowerCase(); if (!seenIds.has(lowerId)) { seenIds.add(lowerId); + const bleName = + device.name ?? (device as unknown as { localName?: string }).localName ?? ''; devices.push({ ...device, connectId: device.id, - deviceType: getDeviceTypeByBleName(device.name ?? ''), + name: bleName || device.name, + deviceType: getDeviceTypeByBleName(bleName), }); } } return devices; } - const { deviceList } = await DevicePool.getDevices(devicesDescriptor); + const { deviceList } = await DevicePool.getDevices(devicesDescriptor, undefined, { + connectProtocol: this.payload.connectProtocol, + }); return deviceList.map(device => device.toMessageObject()); } } diff --git a/packages/core/src/api/UploadPortfolio.ts b/packages/core/src/api/UploadPortfolio.ts new file mode 100644 index 000000000..6b1ec59ac --- /dev/null +++ b/packages/core/src/api/UploadPortfolio.ts @@ -0,0 +1,37 @@ +import FileWrite from './FileWrite'; + +export type UploadPortfolioParams = { + packageBytes: ArrayBuffer | Uint8Array | Blob; + operationId?: string; + timeoutMs?: number | string; +}; + +const PORTFOLIO_PENDING_PATH = 'vol1:/portfolio/portfolio.pfol.pending'; +const PORTFOLIO_CHUNK_SIZE = 2048; + +export default class UploadPortfolio extends FileWrite { + init() { + const { packageBytes, timeoutMs } = this.payload as UploadPortfolioParams; + this.payload = { + ...this.payload, + path: PORTFOLIO_PENDING_PATH, + offset: 0, + data: packageBytes, + chunkSize: PORTFOLIO_CHUNK_SIZE, + overwrite: true, + append: false, + timeoutMs, + }; + super.init(); + } + + async run() { + const stagedFile = await super.run(); + this.throwIfAborted(); + await this.device.commands.typedCall('PortfolioUpdate', 'Success', {}); + return { + ...stagedFile, + portfolioUpdated: true, + }; + } +} diff --git a/packages/core/src/api/alephium/AlephiumGetAddress.ts b/packages/core/src/api/alephium/AlephiumGetAddress.ts index 0d7c15d0e..9dcad51de 100644 --- a/packages/core/src/api/alephium/AlephiumGetAddress.ts +++ b/packages/core/src/api/alephium/AlephiumGetAddress.ts @@ -4,7 +4,7 @@ import { BaseMethod } from '../BaseMethod'; import { validateParams, validateResult } from '../helpers/paramsValidator'; import type { AlephiumGetAddress as HardwareAlephiumGetAddress } from '@onekeyfe/hd-transport'; -import type { AlephiumAddress, AlephiumGetAddressParams } from '../../types'; +import type { AlephiumAddress, AlephiumGetAddressParams, DeviceFirmwareRange } from '../../types'; export default class AlephiumGetAddress extends BaseMethod { hasBundle = false; @@ -43,8 +43,12 @@ export default class AlephiumGetAddress extends BaseMethod { init() { @@ -30,8 +31,12 @@ export default class AlephiumSignMessage extends BaseMethod { @@ -39,8 +40,12 @@ export default class AlephiumSignTransaction extends BaseMethod => { + // 设备可能在最后一个 AlephiumTxRequest(无 data_length)里返回签名, + // 该消息没有 address 字段,返回类型如实声明为联合类型 + ): Promise => { + const responseType = res.type; if (res.type === 'AlephiumSignedTx') { return res.message; } @@ -109,7 +117,10 @@ export default class AlephiumSignTransaction extends BaseMethod method.getVersionRange()[type] - ); - const currentVersion = getDeviceFirmwareVersion(device.features).join('.'); + const versionRange = device.getCurrentMethodVersionRange(type => method.getVersionRange()[type]); + const currentVersion = device.getCurrentFirmwareVersionString() ?? '0.0.0'; + + if (isMethodVersionRangeUnsupported(versionRange)) { + throw ERRORS.createDeviceNotSupportMethodError(method.name, device.getCurrentFirmwareType()); + } if ( versionRange && @@ -478,10 +479,10 @@ function preCheckDeviceSupport(device: Device, method: BaseMethod) { currentVersion, requireVersion: versionRange.min, methodName: method.name, - firmwareType: getFirmwareType(device.features), + firmwareType: device.getCurrentFirmwareType(), }); } else if (method.strictCheckDeviceSupport && !versionRange) { - throw ERRORS.createDeviceNotSupportMethodError(method.name, getFirmwareType(device.features)); + throw ERRORS.createDeviceNotSupportMethodError(method.name, device.getCurrentFirmwareType()); } } @@ -503,11 +504,18 @@ function handleSkippableHardwareError( e.message?.includes('Failure_UnexpectedMessage') || e.message?.includes('Failure_UnknownMessage') ) { - const versionRange = getMethodVersionRange( - device.features, + const versionRange = device.getCurrentMethodVersionRange( type => method.getVersionRange()[type] ); - const currentVersion = getDeviceFirmwareVersion(device.features).join('.'); + const currentVersion = device.getCurrentFirmwareVersionString() ?? '0.0.0'; + + if (isMethodVersionRangeUnsupported(versionRange)) { + error = ERRORS.createDeviceNotSupportMethodError( + method.name, + device.getCurrentFirmwareType() + ); + return error; + } if ( versionRange && @@ -518,12 +526,12 @@ function handleSkippableHardwareError( currentVersion, requireVersion: versionRange.min, methodName: method.name, - firmwareType: getFirmwareType(device.features), + firmwareType: device.getCurrentFirmwareType(), }); } else { error = ERRORS.createDeviceNotSupportMethodError( method.name, - getFirmwareType(device.features) + device.getCurrentFirmwareType() ); } } else if ( diff --git a/packages/core/src/api/aptos/AptosGetAddress.ts b/packages/core/src/api/aptos/AptosGetAddress.ts index 3a711b08a..213a2f685 100644 --- a/packages/core/src/api/aptos/AptosGetAddress.ts +++ b/packages/core/src/api/aptos/AptosGetAddress.ts @@ -5,9 +5,8 @@ import { UI_REQUEST } from '../../constants/ui-request'; import { serializedPath, validatePath } from '../helpers/pathUtils'; import { BaseMethod } from '../BaseMethod'; import { validateParams, validateResult } from '../helpers/paramsValidator'; -import { supportBatchPublicKey } from '../../utils/deviceFeaturesUtils'; import { hexToBytes } from '../helpers/hexUtils'; -import { batchGetPublickeys } from '../helpers/batchGetPublickeys'; +import { batchGetPublickeys, supportBatchPublicKeyByDevice } from '../helpers/batchGetPublickeys'; import type { AptosAddress, AptosGetAddressParams } from '../../types'; import type { AptosGetAddress as HardwareAptosGetAddress } from '@onekeyfe/hd-transport'; @@ -66,7 +65,7 @@ export default class AptosGetAddress extends BaseMethod { @@ -47,8 +46,12 @@ export default class BenfenGetAddress extends BaseMethod { hasBundle = false; @@ -38,8 +38,12 @@ export default class BenfenGetPublicKey extends BaseMethod { }); } - getVersionRange() { + getVersionRange(): DeviceFirmwareRange { return { + pro2: { + min: '0.0.0', + unsupported: true, + }, pro: { min: '4.12.0', }, diff --git a/packages/core/src/api/benfen/BenfenSignMessage.ts b/packages/core/src/api/benfen/BenfenSignMessage.ts index ac7243e75..aec3a73ac 100644 --- a/packages/core/src/api/benfen/BenfenSignMessage.ts +++ b/packages/core/src/api/benfen/BenfenSignMessage.ts @@ -5,6 +5,7 @@ import { validateParams } from '../helpers/paramsValidator'; import { stripHexPrefix } from '../helpers/hexUtils'; import type { BenfenSignMessage as HardwareBenfenSignMessage } from '@onekeyfe/hd-transport'; +import type { DeviceFirmwareRange } from '../../types'; export default class BenfenSignMessage extends BaseMethod { init() { @@ -26,8 +27,12 @@ export default class BenfenSignMessage extends BaseMethod { init() { @@ -33,8 +34,12 @@ export default class BenfenSignTransaction extends BaseMethod { }; } - getVersionRange() { + getVersionRange(): DeviceFirmwareRange { return { + pro2: { + min: '0.0.0', + unsupported: true, + }, pro: { min: '4.12.0', }, diff --git a/packages/core/src/api/btc/BTCGetPublicKey.ts b/packages/core/src/api/btc/BTCGetPublicKey.ts index 2a838eca0..7b7213b27 100644 --- a/packages/core/src/api/btc/BTCGetPublicKey.ts +++ b/packages/core/src/api/btc/BTCGetPublicKey.ts @@ -78,7 +78,8 @@ export default class BTCGetPublicKey extends BaseMethod { } for (const param of this.params) { - const versionBytes = getVersionBytes(param.coin_name, param.script_type); + // init() 必然设置 coin_name;生成类型里该字段为 optional,这里兜底空串 + const versionBytes = getVersionBytes(param.coin_name ?? '', param.script_type); if (!versionBytes) { throw new Error( `Invalid coinName, not support generate xpub for scriptType: ${param.script_type}` @@ -101,7 +102,7 @@ export default class BTCGetPublicKey extends BaseMethod { const path = serializedPath(param.address_n); - const xpub = createExtendedPublicKey(node, param.coin_name, param.script_type); + const xpub = createExtendedPublicKey(node, param.coin_name ?? '', param.script_type); const rootFingerprint = res.root_fingerprint; diff --git a/packages/core/src/api/btc/BTCSignPsbt.ts b/packages/core/src/api/btc/BTCSignPsbt.ts index ce504a143..b56d1773f 100644 --- a/packages/core/src/api/btc/BTCSignPsbt.ts +++ b/packages/core/src/api/btc/BTCSignPsbt.ts @@ -5,7 +5,6 @@ import { BaseMethod } from '../BaseMethod'; import { validateParams } from '../helpers/paramsValidator'; import { formatAnyHex } from '../helpers/hexUtils'; import { getCoinInfo } from './helpers/btcParamsUtils'; -import { getDeviceType } from '../../utils'; import type { SignPsbt } from '@onekeyfe/hd-transport'; @@ -50,7 +49,7 @@ export default class BTCSignPsbt extends BaseMethod { } catch (error) { const { message } = error; - const deviceType = getDeviceType(this.device.features); + const deviceType = this.device.getCurrentDeviceType(); if ( message.includes('PSBT parse failed') && (deviceType === EDeviceType.Classic1s || deviceType === EDeviceType.ClassicPure) diff --git a/packages/core/src/api/btc/helpers/versionLimit.ts b/packages/core/src/api/btc/helpers/versionLimit.ts index ee1c67522..d79d8d8ac 100644 --- a/packages/core/src/api/btc/helpers/versionLimit.ts +++ b/packages/core/src/api/btc/helpers/versionLimit.ts @@ -1,3 +1,5 @@ +import type { DeviceFirmwareRange } from '../../../types'; + function isCoinNameInList(coinName: string, coinNames: (string | undefined)[]) { for (let i = 0; i < coinNames.length; i++) { const coin_name = coinNames[i]; @@ -8,9 +10,13 @@ function isCoinNameInList(coinName: string, coinNames: (string | undefined)[]) { return false; } -export function getBitcoinForkVersionRange(params: (string | undefined)[]) { +export function getBitcoinForkVersionRange(params: (string | undefined)[]): DeviceFirmwareRange { if (isCoinNameInList('Neurai', params)) { return { + pro2: { + min: '0.0.0', + unsupported: true, + }, model_mini: { min: '3.7.0', }, diff --git a/packages/core/src/api/cardano/CardanoSignMessage.ts b/packages/core/src/api/cardano/CardanoSignMessage.ts index b13aaecef..22216e4c8 100644 --- a/packages/core/src/api/cardano/CardanoSignMessage.ts +++ b/packages/core/src/api/cardano/CardanoSignMessage.ts @@ -14,7 +14,7 @@ export default class CardanoSignMessage extends BaseMethod { return; } - const firmwareVersion = getDeviceFirmwareVersion(this.device.features)?.join('.'); + const firmwareVersion = this.device.getCurrentFirmwareVersionString() ?? '0.0.0'; - const versionRange = getMethodVersionRange( - this.device.features, + const versionRange = this.device.getCurrentMethodVersionRange( type => this.supportConwayVersionRange()[type] ); diff --git a/packages/core/src/api/conflux/ConfluxSignTransaction.ts b/packages/core/src/api/conflux/ConfluxSignTransaction.ts index af2d8b66a..3b2c0d959 100644 --- a/packages/core/src/api/conflux/ConfluxSignTransaction.ts +++ b/packages/core/src/api/conflux/ConfluxSignTransaction.ts @@ -35,7 +35,7 @@ export default class ConfluxSignTransaction extends BaseMethod { // check if transaction is valid const schema: SchemaParam[] = [ - { name: 'to', type: 'hexString', required: true }, + { name: 'to', type: 'string', required: true }, { name: 'value', type: 'hexString', required: true }, { name: 'gasLimit', type: 'hexString', required: true }, { name: 'gasPrice', type: 'hexString', required: true }, @@ -48,7 +48,10 @@ export default class ConfluxSignTransaction extends BaseMethod { validateParams(tx, schema); - this.formattedTx = formatAnyHex(tx); + this.formattedTx = { + ...formatAnyHex(tx), + to: tx.to, + }; } processTxRequest = async (request: ConfluxTxRequest, data: string): Promise => { diff --git a/packages/core/src/api/device/DeviceFullyUploadResource.ts b/packages/core/src/api/device/DeviceFullyUploadResource.ts index e8313cdae..d51d5ebdb 100644 --- a/packages/core/src/api/device/DeviceFullyUploadResource.ts +++ b/packages/core/src/api/device/DeviceFullyUploadResource.ts @@ -5,7 +5,7 @@ import { UI_REQUEST } from '../../constants/ui-request'; import { BaseMethod } from '../BaseMethod'; import { getSysResourceBinary } from '../firmware/getBinary'; import { updateResources } from '../firmware/uploadFirmware'; -import { getDeviceFirmwareVersion, getDeviceType, getFirmwareType } from '../../utils'; +import { getDeviceFirmwareVersion, getFirmwareType } from '../../utils'; import { createUiMessage } from '../../events/ui-request'; import { DataManager } from '../../data-manager'; @@ -36,7 +36,7 @@ export default class DeviceFullyUploadResource extends BaseMethod { isSupportResourceUpdate(features: Features, updateType: string) { if (updateType !== 'firmware') return false; - const deviceType = getDeviceType(features); + const deviceType = this.device.getCurrentDeviceType(); const isTouchMode = deviceType === EDeviceType.Touch || deviceType === EDeviceType.Pro; const currentVersion = getDeviceFirmwareVersion(features).join('.'); @@ -52,7 +52,7 @@ export default class DeviceFullyUploadResource extends BaseMethod { const deviceFirmwareType = getFirmwareType(features); const firmwareType = payload.firmwareType ?? deviceFirmwareType; - if (!features?.bootloader_mode && features) { + if (!device.isBootloader() && features) { // check & upgrade firmware resource if (features) { let { binary } = this.payload; diff --git a/packages/core/src/api/device/DeviceLock.ts b/packages/core/src/api/device/DeviceLock.ts index da17e36f3..78189b1dc 100644 --- a/packages/core/src/api/device/DeviceLock.ts +++ b/packages/core/src/api/device/DeviceLock.ts @@ -8,8 +8,6 @@ export default class DeviceLock extends BaseMethod { } async run() { - const res = await this.device.commands.typedCall('LockDevice', 'Success'); - - return Promise.resolve(res.message); + return this.device.lockDevice(); } } diff --git a/packages/core/src/api/device/DeviceRebootToBoardloader.ts b/packages/core/src/api/device/DeviceRebootToBoardloader.ts index d5dcc9c7a..f5e3cc74a 100644 --- a/packages/core/src/api/device/DeviceRebootToBoardloader.ts +++ b/packages/core/src/api/device/DeviceRebootToBoardloader.ts @@ -1,3 +1,5 @@ +import { DeviceRebootType } from '@onekeyfe/hd-transport'; + import { BaseMethod } from '../BaseMethod'; import type { RebootToBoardloaderParams } from '../../types/api/deviceRebootToBoardloader'; @@ -21,10 +23,17 @@ export default class DeviceRebootToBoardloader extends BaseMethod { } checkUploadNFTSupport() { - const deviceType = getDeviceType(this.device.features); - const currentVersion = getDeviceFirmwareVersion(this.device.features).join('.'); + const deviceType = this.device.getCurrentDeviceType(); + const currentVersion = this.device.getCurrentFirmwareVersionString() ?? '0.0.0'; if (!DeviceModelToTypes.model_touch.includes(deviceType)) { throw ERRORS.TypedError(HardwareErrorCode.CallMethodError, 'Device Not Support Upload NFT'); } @@ -138,8 +137,8 @@ export default class DeviceUploadResource extends BaseMethod { }; response.applyScreen = true; - const firmwareVersion = getDeviceFirmwareVersion(this.device.features).join('.'); - const deviceType = getDeviceType(this.device.features); + const firmwareVersion = this.device.getCurrentFirmwareVersionString() ?? '0.0.0'; + const deviceType = this.device.getCurrentDeviceType(); if (deviceType === EDeviceType.Pro && semver.gte(firmwareVersion, '4.17.0')) { response.applyScreen = false; } diff --git a/packages/core/src/api/device/DeviceVerify.ts b/packages/core/src/api/device/DeviceVerify.ts index 751a420b9..04a1ccfec 100644 --- a/packages/core/src/api/device/DeviceVerify.ts +++ b/packages/core/src/api/device/DeviceVerify.ts @@ -5,7 +5,6 @@ import { bytesToHex } from '@noble/hashes/utils'; import { formatAnyHex } from '../helpers/hexUtils'; import { BaseMethod } from '../BaseMethod'; import { validateParams, validateResult } from '../helpers/paramsValidator'; -import { getDeviceType } from '../../utils'; import { DeviceModelToTypes } from '../../types'; import type { BixinVerifyDeviceRequest } from '@onekeyfe/hd-transport'; @@ -26,7 +25,7 @@ export default class DeviceVerify extends BaseMethod { async run() { // For Classic、Mini device we use EthereumSignTypedData - const deviceType = getDeviceType(this.device.features); + const deviceType = this.device.getCurrentDeviceType(); let response: DeviceVerifySignature | undefined; if (DeviceModelToTypes.model_classic.includes(deviceType)) { diff --git a/packages/core/src/api/dynex/DnxGetAddress.ts b/packages/core/src/api/dynex/DnxGetAddress.ts index f8ec99026..c5a795fb9 100644 --- a/packages/core/src/api/dynex/DnxGetAddress.ts +++ b/packages/core/src/api/dynex/DnxGetAddress.ts @@ -1,3 +1,5 @@ +import { createDeviceNotSupportMethodError } from '@onekeyfe/hd-shared'; + import { UI_REQUEST } from '../../constants/ui-request'; import { serializedPath, validatePath } from '../helpers/pathUtils'; import { BaseMethod } from '../BaseMethod'; @@ -47,6 +49,10 @@ export default class DnxGetAddress extends BaseMethod { } async run() { + if (this.device.isProtocolV2()) { + throw createDeviceNotSupportMethodError(this.name, this.device.getCurrentFirmwareType()); + } + const responses: DnxAddress[] = []; for (let i = 0; i < this.params.length; i++) { diff --git a/packages/core/src/api/dynex/DnxSignTransaction.ts b/packages/core/src/api/dynex/DnxSignTransaction.ts index 5d9087b87..8989c8380 100644 --- a/packages/core/src/api/dynex/DnxSignTransaction.ts +++ b/packages/core/src/api/dynex/DnxSignTransaction.ts @@ -1,3 +1,5 @@ +import { createDeviceNotSupportMethodError } from '@onekeyfe/hd-shared'; + import { UI_REQUEST } from '../../constants/ui-request'; import { serializedPath, validatePath } from '../helpers/pathUtils'; import { BaseMethod } from '../BaseMethod'; @@ -116,6 +118,10 @@ export default class DnxSignTransaction extends BaseMethod { } async run() { + if (this.device.isProtocolV2()) { + throw createDeviceNotSupportMethodError(this.name, this.device.getCurrentFirmwareType()); + } + const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands()); const res = await this.device.commands.typedCall('DnxSignTx', 'DnxInputRequest', { diff --git a/packages/core/src/api/evm/EVMGetAddress.ts b/packages/core/src/api/evm/EVMGetAddress.ts index 3ee604fa9..1d726b9e4 100644 --- a/packages/core/src/api/evm/EVMGetAddress.ts +++ b/packages/core/src/api/evm/EVMGetAddress.ts @@ -44,7 +44,7 @@ export default class EvmGetAddress extends BaseMethod true, + () => this.getVersionRange(), + { + strictCheckDeviceSupport: true, + } + ); + const res = await this.device.commands.typedCall( 'EthereumSignMessageEIP712', 'EthereumMessageSignature', diff --git a/packages/core/src/api/evm/EVMSignTransaction.ts b/packages/core/src/api/evm/EVMSignTransaction.ts index 0a2e0535e..85ae8a541 100644 --- a/packages/core/src/api/evm/EVMSignTransaction.ts +++ b/packages/core/src/api/evm/EVMSignTransaction.ts @@ -122,7 +122,7 @@ export default class EVMSignTransaction extends BaseMethod { if (formattedTx == null) throw ERRORS.TypedError('Runtime', 'formattedTx is not set'); - if (TransportManager.getMessageVersion() === 'v1') { + if (TransportManager.getProtocolV1MessageSchema() === 'v1LegacySchema') { return signTransactionLegacyV1({ typedCall: this.device.commands.typedCall.bind(this.device.commands), addressN, diff --git a/packages/core/src/api/evm/EVMSignTypedData.ts b/packages/core/src/api/evm/EVMSignTypedData.ts index 8ec9dd27a..00470ff8b 100644 --- a/packages/core/src/api/evm/EVMSignTypedData.ts +++ b/packages/core/src/api/evm/EVMSignTypedData.ts @@ -9,7 +9,6 @@ import { validatePath } from '../helpers/pathUtils'; import { BaseMethod } from '../BaseMethod'; import { validateParams } from '../helpers/paramsValidator'; import { formatAnyHex, parseChainId, stripHexStartZeroes } from '../helpers/hexUtils'; -import { getDeviceFirmwareVersion, getDeviceType } from '../../utils'; import { existCapability } from '../../utils/capabilitieUtils'; import { DeviceModelToTypes, @@ -26,11 +25,20 @@ import { encodeData, getFieldType, parseArrayType } from '../helpers/typeNameUti import type { EthereumTypedDataSignature, EthereumTypedDataStructAck, + EthereumTypedDataStructAckOneKey, MessageKey, MessageResponse, TypedCall, } from '@onekeyfe/hd-transport'; +/** + * EthereumTypedDataStructAckOneKey 与 EthereumTypedDataStructAck 的字段结构与 + * 枚举数值完全一致(生成产物的 OneKey/Trezor 双份消息),仅枚举名义类型不同; + * 这里做无运行时成本的名义转换,避免在调用点散落 any。 + */ +const toOneKeyStructAck = (ack: EthereumTypedDataStructAck): EthereumTypedDataStructAckOneKey => + ack as unknown as EthereumTypedDataStructAckOneKey; + export type EVMSignTypedDataParams = { addressN: number[]; metamaskV4Compat: boolean; @@ -130,7 +138,6 @@ export default class EVMSignTypedData extends BaseMethod if (supportTrezor) { response = await typedCall( 'EthereumTypedDataStructAck', - // @ts-ignore [ 'EthereumTypedDataStructRequest', 'EthereumTypedDataValueRequest', @@ -141,13 +148,12 @@ export default class EVMSignTypedData extends BaseMethod } else { response = await typedCall( 'EthereumTypedDataStructAckOneKey', - // @ts-ignore [ 'EthereumTypedDataStructRequestOneKey', 'EthereumTypedDataValueRequestOneKey', 'EthereumTypedDataSignatureOneKey', ], - dataStruckAck + toOneKeyStructAck(dataStruckAck) ); } } @@ -183,7 +189,7 @@ export default class EVMSignTypedData extends BaseMethod } else if (typeof memberData === 'object' && memberData !== null) { const memberTypeDefinition = types[memberTypeName][index]; memberTypeName = memberTypeDefinition.type; - memberData = memberData[memberTypeDefinition.name]; + memberData = (memberData as Record)[memberTypeDefinition.name]; } else { // TODO } @@ -220,6 +226,15 @@ export default class EVMSignTypedData extends BaseMethod if (response.type === 'EthereumGnosisSafeTxRequest') { const { data } = this.params; + const verifyingContract = data.domain?.verifyingContract; + // EthereumGnosisSafeTxAck.verifyingContract 在 proto 中是 required 字段, + // Gnosis Safe 签名缺少 verifyingContract 没有意义,这里给出明确的参数错误。 + if (!verifyingContract) { + throw ERRORS.TypedError( + HardwareErrorCode.CallMethodInvalidParameter, + 'EIP712Domain.verifyingContract is required for Gnosis Safe transaction' + ); + } const param = { to: data.message.to, value: formatAnyHex(new BigNumber(data.message.value).toString(16)), @@ -232,11 +247,10 @@ export default class EVMSignTypedData extends BaseMethod refundReceiver: data.message.refundReceiver, nonce: formatAnyHex(new BigNumber(data.message.nonce).toString(16)), chain_id: parseChainId(data.domain.chainId), - verifyingContract: data.domain.verifyingContract, + verifyingContract, }; response = await typedCall( 'EthereumGnosisSafeTxAck', - // @ts-ignore ['EthereumTypedDataSignature', 'EthereumTypedDataSignatureOneKey'], param ); @@ -262,8 +276,8 @@ export default class EVMSignTypedData extends BaseMethod let supportTrezor = false; let response: MessageResponse; - switch (TransportManager.getMessageVersion()) { - case 'v1': + switch (TransportManager.getProtocolV1MessageSchema()) { + case 'v1LegacySchema': supportTrezor = true; response = await signTypedDataLegacyV1({ typedCall: this.device.commands.typedCall.bind(this.device.commands), @@ -274,7 +288,7 @@ export default class EVMSignTypedData extends BaseMethod }); break; - case 'latest': + case 'v1CurrentSchema': default: supportTrezor = false; response = await signTypedData({ @@ -310,8 +324,8 @@ export default class EVMSignTypedData extends BaseMethod }) { if (!domainHash) throw ERRORS.TypedError('Runtime', 'domainHash is required'); - switch (TransportManager.getMessageVersion()) { - case 'v1': + switch (TransportManager.getProtocolV1MessageSchema()) { + case 'v1LegacySchema': return signTypedHashLegacyV1({ typedCall, addressN, @@ -321,7 +335,7 @@ export default class EVMSignTypedData extends BaseMethod device: this.device, }); - case 'latest': + case 'v1CurrentSchema': default: return signTypedHash({ typedCall, @@ -348,8 +362,8 @@ export default class EVMSignTypedData extends BaseMethod let biggerLimit = 1024; // 1k - const currentVersion = getDeviceFirmwareVersion(this.device.features).join('.'); - const currentDeviceType = getDeviceType(this.device.features); + const currentVersion = this.device.getCurrentFirmwareVersionString() ?? '0.0.0'; + const currentDeviceType = this.device.getCurrentDeviceType(); const supportBiggerDataVersion = '4.4.0'; const supportBiggerData = @@ -532,9 +546,9 @@ export default class EVMSignTypedData extends BaseMethod } supportSignTyped() { - const deviceType = getDeviceType(this.device.features); + const deviceType = this.device.getCurrentDeviceType(); if (DeviceModelToTypes.model_mini.includes(deviceType)) { - const currentVersion = getDeviceFirmwareVersion(this.device.features).join('.'); + const currentVersion = this.device.getCurrentFirmwareVersionString() ?? '0.0.0'; const supportSignTypedVersion = '2.2.0'; if (semver.lt(currentVersion, supportSignTypedVersion)) { @@ -564,7 +578,7 @@ export default class EVMSignTypedData extends BaseMethod // For Classic / Mini: // - If parsed typed-data capability is missing, keep using blind-sign. // - For Mini with parsed capability, add extra format checks before parsed signing. - const deviceType = getDeviceType(this.device.features); + const deviceType = this.device.getCurrentDeviceType(); if ( DeviceModelToTypes.model_mini.includes(deviceType) && (!supportEip712OnClassic || this.hasClassicFamilyTypedDataFormatViolations(this.params.data)) diff --git a/packages/core/src/api/evm/EVMVerifyMessage.ts b/packages/core/src/api/evm/EVMVerifyMessage.ts index 544185a62..3ce4aa8c7 100644 --- a/packages/core/src/api/evm/EVMVerifyMessage.ts +++ b/packages/core/src/api/evm/EVMVerifyMessage.ts @@ -31,7 +31,7 @@ export default class EVMSignMessage extends BaseMethod> => { - const deviceType = getDeviceType(device.features); + const deviceType = device.getCurrentDeviceType(); if (deviceType === EDeviceType.Touch || deviceType === EDeviceType.Pro) { // Touch Pro Sign NestedArrays - const currentVersion = getDeviceFirmwareVersion(device.features).join('.'); + const currentVersion = device.getCurrentFirmwareVersionString() ?? '0.0.0'; const supportNestedArraysSignVersion = '4.2.0'; // 4.2.0 is the first version that supports nested arrays in signTypedData diff --git a/packages/core/src/api/evm/legacyV1/getAddress.ts b/packages/core/src/api/evm/legacyV1/getAddress.ts index c2e73bd66..7d02bc47e 100644 --- a/packages/core/src/api/evm/legacyV1/getAddress.ts +++ b/packages/core/src/api/evm/legacyV1/getAddress.ts @@ -7,10 +7,12 @@ export default async function ({ typedCall: TypedCall; param: EthereumGetAddressOneKey; }): Promise> { - return typedCall('EthereumGetAddress', 'EthereumAddress', { + // legacy EthereumGetAddress 的生成类型没有 chain_id 字段,但旧固件按 OneKey 扩展 + // 接受该字段;通过预先声明的对象(非 fresh literal)携带额外字段,保持原有运行时行为。 + const message = { address_n: param.address_n, show_display: param.show_display, - // @ts-ignore chain_id: param.chain_id, - }); + }; + return typedCall('EthereumGetAddress', 'EthereumAddress', message); } diff --git a/packages/core/src/api/evm/legacyV1/getPublicKey.ts b/packages/core/src/api/evm/legacyV1/getPublicKey.ts index b30f06acf..9205f7353 100644 --- a/packages/core/src/api/evm/legacyV1/getPublicKey.ts +++ b/packages/core/src/api/evm/legacyV1/getPublicKey.ts @@ -11,10 +11,12 @@ export default async function ({ typedCall: TypedCall; param: EthereumGetPublicKeyOneKey; }): Promise> { - return typedCall('EthereumGetPublicKey', 'EthereumPublicKey', { + // legacy EthereumGetPublicKey 的生成类型没有 chain_id 字段,但旧固件按 OneKey 扩展 + // 接受该字段;通过预先声明的对象(非 fresh literal)携带额外字段,保持原有运行时行为。 + const message = { address_n: param.address_n, show_display: param.show_display, - // @ts-ignore chain_id: param.chain_id, - }); + }; + return typedCall('EthereumGetPublicKey', 'EthereumPublicKey', message); } diff --git a/packages/core/src/api/evm/legacyV1/signMessage.ts b/packages/core/src/api/evm/legacyV1/signMessage.ts index fffd6026b..8e5fd714e 100644 --- a/packages/core/src/api/evm/legacyV1/signMessage.ts +++ b/packages/core/src/api/evm/legacyV1/signMessage.ts @@ -11,12 +11,14 @@ export default async function ({ typedCall: TypedCall; params: EthereumSignMessageOneKey; }): Promise { - const res = await typedCall('EthereumSignMessage', 'EthereumMessageSignature', { + // legacy EthereumSignMessage 的生成类型没有 chain_id 字段,但旧固件按 OneKey 扩展 + // 接受该字段;通过预先声明的对象(非 fresh literal)携带额外字段,保持原有运行时行为。 + const message = { address_n: params.address_n, message: params.message, - // @ts-ignore chain_id: params.chain_id, - }); + }; + const res = await typedCall('EthereumSignMessage', 'EthereumMessageSignature', message); return Promise.resolve(res.message); } diff --git a/packages/core/src/api/evm/legacyV1/signTypedData.ts b/packages/core/src/api/evm/legacyV1/signTypedData.ts index a7b035a4a..fea73fb08 100644 --- a/packages/core/src/api/evm/legacyV1/signTypedData.ts +++ b/packages/core/src/api/evm/legacyV1/signTypedData.ts @@ -16,22 +16,23 @@ export const signTypedData = async ({ }) => { const { primaryType }: EthereumSignTypedDataMessage = data; + // legacy EthereumSignTypedData 的生成类型没有 chain_id 字段,但旧固件按 OneKey 扩展 + // 接受该字段;通过预先声明的对象(非 fresh literal)携带额外字段,保持原有运行时行为。 + const message = { + address_n: addressN, + primary_type: primaryType as string, + metamask_v4_compat: metamaskV4Compat, + chain_id: chainId, + }; const response = await typedCall( 'EthereumSignTypedData', - // @ts-ignore [ 'EthereumTypedDataStructRequest', 'EthereumTypedDataValueRequest', 'EthereumTypedDataSignature', 'EthereumGnosisSafeTxRequest', ], - { - address_n: addressN, - primary_type: primaryType as string, - metamask_v4_compat: metamaskV4Compat, - // @ts-ignore - chain_id: chainId, - } + message ); return response; }; diff --git a/packages/core/src/api/evm/legacyV1/signTypedHash.ts b/packages/core/src/api/evm/legacyV1/signTypedHash.ts index 6f7aff447..8ab526aed 100644 --- a/packages/core/src/api/evm/legacyV1/signTypedHash.ts +++ b/packages/core/src/api/evm/legacyV1/signTypedHash.ts @@ -1,8 +1,6 @@ import semver from 'semver'; import { EDeviceType, ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared'; -import { getDeviceFirmwareVersion, getDeviceType } from '../../../utils'; - import type { Device } from '../../../device/Device'; import type { MessageResponse, TypedCall } from '@onekeyfe/hd-transport'; @@ -24,10 +22,10 @@ export const signTypedHash = async ({ | MessageResponse<'EthereumTypedDataSignature'> | MessageResponse<'EthereumTypedDataSignatureOneKey'> > => { - const deviceType = getDeviceType(device.features); + const deviceType = device.getCurrentDeviceType(); if (deviceType === EDeviceType.Touch || deviceType === EDeviceType.Pro) { // Touch Pro Sign NestedArrays - const currentVersion = getDeviceFirmwareVersion(device.features).join('.'); + const currentVersion = device.getCurrentFirmwareVersionString() ?? '0.0.0'; const supportNestedArraysSignVersion = '4.2.0'; // 4.2.0 is the first version that supports nested arrays in signTypedData @@ -40,11 +38,13 @@ export const signTypedHash = async ({ } } - return typedCall('EthereumSignTypedHash', 'EthereumTypedDataSignature', { + // legacy EthereumSignTypedHash 的生成类型没有 chain_id 字段,但旧固件按 OneKey 扩展 + // 接受该字段;通过预先声明的对象(非 fresh literal)携带额外字段,保持原有运行时行为。 + const message = { address_n: addressN, domain_separator_hash: domainHash ?? '', message_hash: messageHash, - // @ts-ignore chain_id: chainId, - }); + }; + return typedCall('EthereumSignTypedHash', 'EthereumTypedDataSignature', message); }; diff --git a/packages/core/src/api/evm/legacyV1/verifyMessage.ts b/packages/core/src/api/evm/legacyV1/verifyMessage.ts index c2e33a169..3cd26168d 100644 --- a/packages/core/src/api/evm/legacyV1/verifyMessage.ts +++ b/packages/core/src/api/evm/legacyV1/verifyMessage.ts @@ -7,13 +7,15 @@ export default async function ({ typedCall: TypedCall; params: EthereumVerifyMessageOneKey; }): Promise { - const res = await typedCall('EthereumVerifyMessage', 'Success', { + // legacy EthereumVerifyMessage 的生成类型没有 chain_id 字段,但旧固件按 OneKey 扩展 + // 接受该字段;通过预先声明的对象(非 fresh literal)携带额外字段,保持原有运行时行为。 + const message = { signature: params.signature, message: params.message, address: params.address, - // @ts-ignore chain_id: params.chain_id, - }); + }; + const res = await typedCall('EthereumVerifyMessage', 'Success', message); return Promise.resolve(res.message); } diff --git a/packages/core/src/api/firmware/FirmwareUpdateBaseMethod.ts b/packages/core/src/api/firmware/FirmwareUpdateBaseMethod.ts index fba51109b..7b9c7877e 100644 --- a/packages/core/src/api/firmware/FirmwareUpdateBaseMethod.ts +++ b/packages/core/src/api/firmware/FirmwareUpdateBaseMethod.ts @@ -8,11 +8,12 @@ import { import { FirmwareUpdateTipMessage, UI_REQUEST, createUiMessage } from '../../events/ui-request'; import { DevicePool } from '../../device/DevicePool'; -import { LoggerNames, getDeviceType, getDeviceUUID, getLogger, wait } from '../../utils'; +import { LoggerNames, getDeviceUUID, getLogger, wait } from '../../utils'; import { DeviceModelToTypes } from '../../types'; import { DataManager } from '../../data-manager'; import { BaseMethod } from '../BaseMethod'; import { DEVICE } from '../../events'; +import { createFirmwareProgressThrottle } from './progressThrottle'; import type { IFirmwareUpdateProgressType, @@ -40,6 +41,8 @@ const isDeviceDisconnectedError = (error: unknown) => { export class FirmwareUpdateBaseMethod extends BaseMethod { checkPromise: Deferred | null = null; + private shouldPostFirmwareProgress = createFirmwareProgressThrottle(); + init(): void {} run(): Promise { @@ -83,6 +86,10 @@ export class FirmwareUpdateBaseMethod extends BaseMethod { * @param progress Post the percentage of the progress */ postProgressMessage = (progress: number, progressType: IFirmwareUpdateProgressType) => { + if (!this.shouldPostFirmwareProgress(progress, progressType)) { + return; + } + this.postMessage( createUiMessage(UI_REQUEST.FIRMWARE_PROGRESS, { device: this.device.toMessageObject() as KnownDevice, @@ -138,12 +145,14 @@ export class FirmwareUpdateBaseMethod extends BaseMethod { // check device goto bootloader mode let isFirstCheck = true; let checkCount = 0; + let hasPromptedWebDevice = false; + let isPromptingWebDevice = false; // eslint-disable-next-line prefer-const let timeoutTimer: ReturnType | undefined; const isTouchOrProDevice = - getDeviceType(this?.device?.features) === EDeviceType.Touch || - getDeviceType(this?.device?.features) === EDeviceType.Pro; + this?.device?.getCurrentDeviceType() === EDeviceType.Touch || + this?.device?.getCurrentDeviceType() === EDeviceType.Pro; const intervalTimer: ReturnType | undefined = setInterval( async () => { @@ -158,23 +167,28 @@ export class FirmwareUpdateBaseMethod extends BaseMethod { if ( checkCount > 4 && DataManager.isBrowserWebUsb(DataManager.getSettings('env')) && - !this.payload.skipWebDevicePrompt + !this.payload.skipWebDevicePrompt && + !hasPromptedWebDevice && + !isPromptingWebDevice ) { - clearInterval(intervalTimer); - clearTimeout(timeoutTimer); - + isPromptingWebDevice = true; try { this.postTipMessage(FirmwareUpdateTipMessage.SelectDeviceInBootloaderForWebDevice); const confirmed = await this._promptDeviceInBootloaderForWebDevice(); + hasPromptedWebDevice = true; if (confirmed) { await this._checkDeviceInBootloaderMode(connectId, intervalTimer, timeoutTimer); } } catch (e) { + clearInterval(intervalTimer); + clearTimeout(timeoutTimer); Log.log( 'FirmwareUpdateBaseMethod [checkDeviceToBootloader] _promptDeviceInBootloaderForWebDevice failed: ', e ); this.checkPromise?.reject(e); + } finally { + isPromptingWebDevice = false; } return; } @@ -187,7 +201,7 @@ export class FirmwareUpdateBaseMethod extends BaseMethod { true ); await this.device.initialize(); - if (this.device.features?.bootloader_mode) { + if (this.device.isBootloader()) { clearInterval(intervalTimer); this.checkPromise?.resolve(true); } @@ -220,7 +234,7 @@ export class FirmwareUpdateBaseMethod extends BaseMethod { const devicesDescriptor = deviceDiff?.descriptors ?? []; const { deviceList } = await DevicePool.getDevices(devicesDescriptor, connectId); - if (deviceList.length === 1 && deviceList[0]?.features?.bootloader_mode) { + if (deviceList.length === 1 && deviceList[0]?.isBootloader()) { // should update current device from cache // because device was reboot and had some new requests this.device.updateFromCache(deviceList[0]); @@ -236,9 +250,9 @@ export class FirmwareUpdateBaseMethod extends BaseMethod { async enterBootloaderMode() { const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands()); - if (this.device.features && !this.device.features.bootloader_mode) { + if (this.device.features && !this.device.isBootloader()) { const uuid = getDeviceUUID(this.device.features); - const deviceType = getDeviceType(this.device.features); + const deviceType = this.device.getCurrentDeviceType(); // auto go to bootloader mode try { this.postTipMessage(FirmwareUpdateTipMessage.AutoRebootToBootloader); @@ -436,7 +450,7 @@ export class FirmwareUpdateBaseMethod extends BaseMethod { const deviceDiff = await this.device.deviceConnector?.enumerate(); const devicesDescriptor = deviceDiff?.descriptors ?? []; const { deviceList } = await DevicePool.getDevices(devicesDescriptor, undefined); - if (deviceList.length === 1 && deviceList[0]?.features?.bootloader_mode) { + if (deviceList.length === 1 && deviceList[0]?.isBootloader()) { this.device.updateFromCache(deviceList[0]); await this.device.acquire(); this.device.getCommands().mainId = this.device.mainId ?? ''; diff --git a/packages/core/src/api/firmware/bootloaderHelper.ts b/packages/core/src/api/firmware/bootloaderHelper.ts index adf2b2349..ff9128d08 100644 --- a/packages/core/src/api/firmware/bootloaderHelper.ts +++ b/packages/core/src/api/firmware/bootloaderHelper.ts @@ -1,7 +1,7 @@ import semver from 'semver'; import { EDeviceType } from '@onekeyfe/hd-shared'; -import { getDeviceType } from '../../utils'; +import { getDeviceBootloaderVersion, getDeviceType } from '../../utils'; import type { Features, IVersionArray } from '../../types'; @@ -38,9 +38,10 @@ export function shouldUpdateBootloaderForClassicAndMini({ export function isEnteredManuallyBoot(features: Features, updateType: string) { const deviceType = getDeviceType(features); const isMini = deviceType === EDeviceType.Mini; + const bootloaderVersion = getDeviceBootloaderVersion(features).join('.'); const isBoot183ClassicUpBle = updateType === 'firmware' && deviceType === EDeviceType.Classic && - features.bootloader_version === '1.8.3'; + bootloaderVersion === '1.8.3'; return isMini || isBoot183ClassicUpBle; } diff --git a/packages/core/src/api/firmware/getBinary.ts b/packages/core/src/api/firmware/getBinary.ts index 69547e80a..4d4538223 100644 --- a/packages/core/src/api/firmware/getBinary.ts +++ b/packages/core/src/api/firmware/getBinary.ts @@ -41,7 +41,7 @@ export const getBinary = async ({ } if (version && !semver.eq(releaseInfo.version.join('.'), version.join('.'))) { - const touchWithoutVersion = getDeviceType(features) === 'touch' && !features.onekey_version; + const touchWithoutVersion = getDeviceType(features) === 'touch' && !features.firmwareVersion; if (!touchWithoutVersion) { throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'firmware version mismatch'); } diff --git a/packages/core/src/api/firmware/progressThrottle.ts b/packages/core/src/api/firmware/progressThrottle.ts new file mode 100644 index 000000000..dfa3b65de --- /dev/null +++ b/packages/core/src/api/firmware/progressThrottle.ts @@ -0,0 +1,44 @@ +import type { IFirmwareUpdateProgressType } from '../../events/ui-request'; + +const DEFAULT_FIRMWARE_PROGRESS_INTERVAL_MS = 250; + +type FirmwareProgressThrottleState = { + lastEmittedAt: number; + lastProgress?: number; +}; + +export const createFirmwareProgressThrottle = ( + intervalMs = DEFAULT_FIRMWARE_PROGRESS_INTERVAL_MS +) => { + const stateByType = new Map(); + + return (progress: number, progressType: IFirmwareUpdateProgressType) => { + if (progressType !== 'transferData') { + return true; + } + + const normalizedProgress = Math.max(0, Math.min(100, Math.floor(progress))); + if (normalizedProgress === 0 || normalizedProgress === 100) { + stateByType.set(progressType, { + lastEmittedAt: Date.now(), + lastProgress: normalizedProgress, + }); + return true; + } + + const now = Date.now(); + const state = stateByType.get(progressType); + if ( + state && + (state.lastProgress === normalizedProgress || now - state.lastEmittedAt < intervalMs) + ) { + return false; + } + + stateByType.set(progressType, { + lastEmittedAt: now, + lastProgress: normalizedProgress, + }); + return true; + }; +}; diff --git a/packages/core/src/api/firmware/releaseHelper.ts b/packages/core/src/api/firmware/releaseHelper.ts index 3c84dd2c7..333a1e73d 100644 --- a/packages/core/src/api/firmware/releaseHelper.ts +++ b/packages/core/src/api/firmware/releaseHelper.ts @@ -12,7 +12,7 @@ export const getFirmwareReleaseInfo = (features: Features, firmwareType: EFirmwa const firmwareStatus = DataManager.getFirmwareStatus(features, firmwareType); const changelog = DataManager.getFirmwareChangelog(features, firmwareType); const release = DataManager.getFirmwareLatestRelease(features, firmwareType); - const bootloaderMode = !!features.bootloader_mode; + const bootloaderMode = !!features.bootloaderMode; return { status: firmwareStatus, changelog, @@ -25,7 +25,7 @@ export const getBleFirmwareReleaseInfo = (features: Features) => { const firmwareStatus = DataManager.getBLEFirmwareStatus(features); const changelog = DataManager.getBleFirmwareChangelog(features); const release = DataManager.getBleFirmwareLatestRelease(features); - const bootloaderMode = !!features.bootloader_mode; + const bootloaderMode = !!features.bootloaderMode; return { status: firmwareStatus, changelog, @@ -52,7 +52,7 @@ export const getBootloaderReleaseInfo = ({ Object.prototype.hasOwnProperty.call(item, 'en-US') ); - const bootloaderMode = !!features.bootloader_mode; + const bootloaderMode = !!features.bootloaderMode; let shouldUpdate = false; diff --git a/packages/core/src/api/firmware/uploadFirmware.ts b/packages/core/src/api/firmware/uploadFirmware.ts index 6dadfb55c..36b83b7db 100644 --- a/packages/core/src/api/firmware/uploadFirmware.ts +++ b/packages/core/src/api/firmware/uploadFirmware.ts @@ -3,19 +3,13 @@ import { blake2s } from '@noble/hashes/blake2s'; import JSZip from 'jszip'; import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared'; -import { getDeviceFirmwareVersion } from '../../utils/deviceVersionUtils'; -import { - LoggerNames, - getDeviceBootloaderVersion, - getDeviceType, - getLogger, - wait, -} from '../../utils'; +import { LoggerNames, getDeviceBootloaderVersion, getLogger, wait } from '../../utils'; import { DEVICE, UI_REQUEST, createUiMessage } from '../../events'; import { DeviceModelToTypes } from '../../types'; import { bytesToHex } from '../helpers/hexUtils'; import { DataManager } from '../../data-manager'; import { DevicePool } from '../../device/DevicePool'; +import { buildProtocolV1FeaturesPayload } from '../../deviceProfile'; import type { KnownDevice } from '../../types'; import type { TypedCall, TypedResponseMessage } from '../../device/DeviceCommands'; @@ -41,7 +35,7 @@ const isDeviceDisconnectedError = (error: unknown) => { const postConfirmationMessage = (device: Device) => { // only if firmware is already installed. fresh device does not require button confirmation - if (device.features?.firmware_present) { + if (device.features?.firmwarePresent) { device.emit(DEVICE.BUTTON, device, { code: 'ButtonRequest_FirmwareUpdate' }); } }; @@ -107,7 +101,7 @@ export const uploadFirmware = async ( }, isUpdateBootloader?: boolean ) => { - const deviceType = getDeviceType(device.features); + const deviceType = device.getCurrentDeviceType(); if (DeviceModelToTypes.model_mini.includes(deviceType)) { postConfirmationMessage(device); postProgressTip(device, 'ConfirmOnDevice', postMessage); @@ -116,7 +110,9 @@ export const uploadFirmware = async ( if (isFirmware && !isUpdateBootloader) { const newFeatures = await typedCall('GetFeatures', 'Features', {}); - const deviceBootloaderVersion = getDeviceBootloaderVersion(newFeatures.message).join('.'); + const deviceBootloaderVersion = getDeviceBootloaderVersion( + buildProtocolV1FeaturesPayload(newFeatures.message, device.features) + ).join('.'); const supportUpgradeFileHeader = semver.gte(deviceBootloaderVersion, '2.1.0'); Log.debug('supportUpgradeFileHeader:', supportUpgradeFileHeader); @@ -429,7 +425,7 @@ const emmcFileWriteWithRetry = async ( const deviceDiff = await device.deviceConnector?.enumerate(); const devicesDescriptor = deviceDiff?.descriptors ?? []; const { deviceList } = await DevicePool.getDevices(devicesDescriptor, undefined); - if (deviceList.length === 1 && deviceList[0]?.features?.bootloader_mode) { + if (deviceList.length === 1 && deviceList[0]?.isBootloader()) { device.updateFromCache(deviceList[0]); await device.acquire(); device.getCommands().mainId = device.mainId ?? ''; diff --git a/packages/core/src/api/helpers/batchGetPublickeys.ts b/packages/core/src/api/helpers/batchGetPublickeys.ts index 81482a1a1..72ab87b24 100644 --- a/packages/core/src/api/helpers/batchGetPublickeys.ts +++ b/packages/core/src/api/helpers/batchGetPublickeys.ts @@ -1,18 +1,63 @@ +import semver from 'semver'; import { + EDeviceType, HardwareErrorCode, TypedError, createDeviceNotSupportMethodError, } from '@onekeyfe/hd-shared'; -import { supportBatchPublicKey } from '../../utils/deviceFeaturesUtils'; import { isEqualBip44CoinType } from './pathUtils'; import { splitArray } from '../../utils/arrayUtils'; -import { getDeviceType, getFirmwareType } from '../../utils'; import { DeviceModelToTypes } from '../../types'; import type { EcdsaPublicKeys, Path } from '@onekeyfe/hd-transport'; import type { Device } from '../../device/Device'; +/** + * Protocol-agnostic version of `supportBatchPublicKey` (utils/deviceFeaturesUtils): + * derives device type and firmware version through Device accessors so that + * Protocol V2 devices (features === undefined, profile set) resolve correctly. + */ +export function supportBatchPublicKeyByDevice( + device: Device, + options?: { + includeNode?: boolean; + } +): boolean { + const currentVersion = device.getCurrentFirmwareVersionString() ?? '0.0.0'; + const deviceType = device.getCurrentDeviceType(); + + // Pro2 (Protocol V2) 版本线独立于 Pro 系列,固件从首个版本即支持 batch / include_node, + // 不能套用下面 Pro 系列的 4.x 门槛。 + if (device.isProtocolV2() || deviceType === EDeviceType.Pro2) { + return true; + } + + // btc batch get public key + if (!!options?.includeNode && deviceType === EDeviceType.Pro) { + return semver.gte(currentVersion, '4.14.0'); + } + if (!!options?.includeNode && deviceType === EDeviceType.Touch) { + return semver.gte(currentVersion, '4.11.0'); + } + if (!!options?.includeNode && DeviceModelToTypes.model_classic1s.includes(deviceType)) { + return semver.gte(currentVersion, '3.12.0'); + } + if (!!options?.includeNode && DeviceModelToTypes.model_mini.includes(deviceType)) { + return semver.gte(currentVersion, '3.10.0'); + } + if (options?.includeNode) { + return false; + } + + // support batch get public key + if (deviceType === EDeviceType.Touch || deviceType === EDeviceType.Pro) { + return semver.gte(currentVersion, '3.1.0'); + } + + return semver.gte(currentVersion, '2.6.0'); +} + export async function batchGetPublickeys( device: Device, paths: Path[], @@ -28,9 +73,9 @@ export async function batchGetPublickeys( throw TypedError(HardwareErrorCode.ForbiddenKeyPath, 'Path length must be greater than 3'); } - const supportsBatchPublicKey = supportBatchPublicKey(device.features, options); + const supportsBatchPublicKey = supportBatchPublicKeyByDevice(device, options); if (!supportsBatchPublicKey) { - throw createDeviceNotSupportMethodError('BatchGetPublickeys', getFirmwareType(device.features)); + throw createDeviceNotSupportMethodError('BatchGetPublickeys', device.getCurrentFirmwareType()); } const existsPathNotEqualCoinType = paths.find(p => !isEqualBip44CoinType(p.address_n, coinType)); @@ -39,7 +84,7 @@ export async function batchGetPublickeys( } let batchSize = 10; - const deviceType = getDeviceType(device.features); + const deviceType = device.getCurrentDeviceType(); if (DeviceModelToTypes.model_mini.includes(deviceType)) { batchSize = 10; } else if (DeviceModelToTypes.model_touch.includes(deviceType)) { @@ -60,7 +105,7 @@ export async function batchGetPublickeys( if (res.type !== 'EcdsaPublicKeys') { throw createDeviceNotSupportMethodError( 'BatchGetPublickeys', - getFirmwareType(device.features) + device.getCurrentFirmwareType() ); } else { result.root_fingerprint = res.message.root_fingerprint; diff --git a/packages/core/src/api/helpers/filesystemValidation.ts b/packages/core/src/api/helpers/filesystemValidation.ts new file mode 100644 index 000000000..59b01585b --- /dev/null +++ b/packages/core/src/api/helpers/filesystemValidation.ts @@ -0,0 +1,51 @@ +import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared'; + +export const invalidParameter = (message: string) => + ERRORS.TypedError(HardwareErrorCode.CallMethodInvalidParameter, message); + +export function validateNonEmptyString(value: unknown, name: string): string { + if (typeof value !== 'string' || value.trim().length === 0) { + throw invalidParameter(`Parameter [${name}] is required and must be a non-empty string.`); + } + return value; +} + +export function validateNonNegativeInteger( + value: unknown, + name: string, + defaultValue?: number +): number { + if (value === undefined || value === null) { + if (defaultValue !== undefined) return defaultValue; + throw invalidParameter(`Missing required parameter: ${name}`); + } + + const numeric = typeof value === 'string' && value.trim() !== '' ? Number(value) : value; + if (typeof numeric !== 'number' || !Number.isSafeInteger(numeric) || numeric < 0) { + throw invalidParameter(`Parameter [${name}] must be a non-negative integer.`); + } + return numeric; +} + +export function validateOptionalNonNegativeInteger( + value: unknown, + name: string +): number | undefined { + if (value === undefined || value === null) return undefined; + return validateNonNegativeInteger(value, name); +} + +export function validateOptionalPercentage(value: unknown, name: string): number | undefined { + const numeric = validateOptionalNonNegativeInteger(value, name); + if (numeric === undefined) return undefined; + if (numeric > 100) { + throw invalidParameter(`Parameter [${name}] must be between 0 and 100.`); + } + return numeric; +} + +export function validateRequiredData(value: unknown, name: string): void { + if (value === undefined || value === null) { + throw invalidParameter(`Missing required parameter: ${name}`); + } +} diff --git a/packages/core/src/api/helpers/protocolV2FileWrite.ts b/packages/core/src/api/helpers/protocolV2FileWrite.ts new file mode 100644 index 000000000..d8cb4baaa --- /dev/null +++ b/packages/core/src/api/helpers/protocolV2FileWrite.ts @@ -0,0 +1,164 @@ +import { + PROTOCOL_V2_BLE_FILE_CHUNK_SIZE, + PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE, +} from '@onekeyfe/hd-transport'; +import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared'; + +import { DataManager } from '../../data-manager'; + +import type { DeviceCommands } from '../../device/DeviceCommands'; + +export type ProtocolV2FileWriteData = ArrayBuffer | Uint8Array | Blob | string; + +export type ProtocolV2FileWriteProgress = { + progress: number; + transferredBytes: number; + totalBytes: number; + rateBytesPerSecond?: number; + elapsedMs: number; +}; + +export type ProtocolV2FileWriteOptions = { + commands: Pick; + path: string; + data: ProtocolV2FileWriteData; + offset?: number; + totalSize?: number; + chunkSize?: number; + chunkLen?: number; + overwrite?: boolean; + append?: boolean; + uiPercentage?: number; + timeoutMs?: number; + throwIfAborted?: () => void; + onProgress?: (progress: ProtocolV2FileWriteProgress) => void; +}; + +const MIN_FILE_CHUNK_SIZE = 64; + +function getProtocolV2FileChunkLimit() { + const env = DataManager.getSettings('env'); + return env && DataManager.isBleConnect(env) + ? PROTOCOL_V2_BLE_FILE_CHUNK_SIZE + : PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE; +} + +async function dataToUint8Array(data: ProtocolV2FileWriteData): Promise { + if (typeof data === 'string') return new TextEncoder().encode(data); + if (data instanceof ArrayBuffer) return new Uint8Array(data); + if (ArrayBuffer.isView(data)) { + return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); + } + if (typeof Blob !== 'undefined' && data instanceof Blob) { + return new Uint8Array(await data.arrayBuffer()); + } + throw ERRORS.TypedError( + HardwareErrorCode.CallMethodInvalidParameter, + 'Unsupported FilesystemFileWrite data' + ); +} + +function normalizeChunkSize(value: unknown, maxChunkSize: number): number { + const numeric = Number(value); + if (!Number.isFinite(numeric) || numeric <= 0) return maxChunkSize; + return Math.min(Math.max(Math.floor(numeric), MIN_FILE_CHUNK_SIZE), maxChunkSize); +} + +function getDeviceTransferProgress(before: number, after: number, total: number) { + if (!Number.isFinite(total) || total <= 0) return 100; + if (before <= 0 && after < total) return 0; + if (after >= total) return 100; + return Math.min(Math.max(Math.ceil((after / total) * 100), 1), 99); +} + +function getConfirmedProgress(processed: number, total: number, written: number, length: number) { + if (Number.isFinite(processed) && Number.isFinite(total) && total > 0) { + if (processed >= total) return 100; + return Math.min(Math.max(Math.floor((processed / total) * 100), 0), 99); + } + if (length > 0) return written >= length ? 100 : Math.floor((written / length) * 100); + return 100; +} + +export async function writeProtocolV2File(options: ProtocolV2FileWriteOptions) { + options.throwIfAborted?.(); + const data = await dataToUint8Array(options.data); + const dataLength = data.byteLength; + const startOffset = + Number.isFinite(options.offset) && Number(options.offset) > 0 ? Number(options.offset) : 0; + const totalSize = + Number.isFinite(options.totalSize) && Number(options.totalSize) > 0 + ? Number(options.totalSize) + : startOffset + dataLength; + + if (totalSize < startOffset + dataLength) { + throw ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + `FilesystemFileWrite totalSize ${totalSize} is smaller than offset + data length ${ + startOffset + dataLength + }` + ); + } + + const chunkSize = normalizeChunkSize( + options.chunkSize ?? options.chunkLen, + getProtocolV2FileChunkLimit() + ); + let written = 0; + let chunks = 0; + let lastMessage: Record | undefined; + const startTime = Date.now(); + + while (written < dataLength) { + options.throwIfAborted?.(); + const chunk = data.slice(written, Math.min(written + chunkSize, dataLength)); + const offset = startOffset + written; + const progress = + options.uiPercentage ?? + getDeviceTransferProgress(offset, offset + chunk.byteLength, totalSize); + const response = await options.commands.typedCall( + 'FilesystemFileWrite', + 'FilesystemFile', + { + file: { path: options.path, offset, total_size: totalSize, data: chunk }, + overwrite: chunks === 0 ? options.overwrite ?? false : false, + append: options.append ?? false, + ui_percentage: progress, + }, + { timeoutMs: options.timeoutMs } + ); + options.throwIfAborted?.(); + lastMessage = response.message; + const processedByte = Number(response.message?.processed_byte); + written = + Number.isFinite(processedByte) && processedByte > offset + ? processedByte - startOffset + : written + chunk.byteLength; + if (written > dataLength) { + throw ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + `FilesystemFileWrite invalid processed_byte ${processedByte}` + ); + } + chunks += 1; + const elapsedMs = Date.now() - startTime; + const transferredBytes = Math.min(written, dataLength); + options.onProgress?.({ + progress: getConfirmedProgress(startOffset + written, totalSize, written, dataLength), + transferredBytes, + totalBytes: dataLength, + rateBytesPerSecond: + elapsedMs > 0 ? Math.round((transferredBytes / elapsedMs) * 1000) : undefined, + elapsedMs, + }); + } + + return { + ...lastMessage, + path: options.path, + offset: startOffset, + total_size: totalSize, + processed_byte: startOffset + written, + chunks, + }; +} diff --git a/packages/core/src/api/index.ts b/packages/core/src/api/index.ts index 339e125be..5cbcd0d4e 100644 --- a/packages/core/src/api/index.ts +++ b/packages/core/src/api/index.ts @@ -3,9 +3,11 @@ export { default as preInitialize } from './device/PreInitialize'; export { default as searchDevices } from './SearchDevices'; export { default as getFeatures } from './GetFeatures'; +export { default as getDeviceInfo } from './GetDeviceInfo'; export { default as getOnekeyFeatures } from './GetOnekeyFeatures'; export { default as getPassphraseState } from './GetPassphraseState'; export { default as getLogs } from './GetLogs'; +export { default as clearSessionCache } from './ClearSessionCache'; export { default as checkFirmwareRelease } from './CheckFirmwareRelease'; export { default as checkBLEFirmwareRelease } from './CheckBLEFirmwareRelease'; export { default as checkBridgeStatus } from './CheckBridgeStatus'; @@ -38,8 +40,43 @@ export { default as getNextU2FCounter } from './u2f/GetNextU2FCounter'; export { default as firmwareUpdate } from './FirmwareUpdate'; export { default as firmwareUpdateV2 } from './FirmwareUpdateV2'; export { default as firmwareUpdateV3 } from './FirmwareUpdateV3'; +export { default as firmwareUpdateV4 } from './FirmwareUpdateV4'; export { default as promptWebDeviceAccess } from './PromptWebDeviceAccess'; +// File system & device control API (Protocol V2 only) +export { default as protocolInfoRequest } from './protocol-v2/ProtocolInfoRequest'; +export { default as ping } from './protocol-v2/Ping'; +export { default as deviceReboot } from './protocol-v2/DeviceReboot'; +export { default as deviceInfoGet } from './protocol-v2/DeviceInfoGet'; +export { default as deviceStatusGet } from './protocol-v2/DeviceStatusGet'; +export { default as deviceGetOnboardingStatus } from './protocol-v2/DeviceGetOnboardingStatus'; +export { default as deviceSessionGet } from './protocol-v2/DeviceSessionGet'; +export { default as deviceFirmwareUpdate } from './protocol-v2/DeviceFirmwareUpdate'; +export { default as deviceGetFirmwareUpdateStatus } from './protocol-v2/DeviceGetFirmwareUpdateStatus'; +export { default as deviceFactoryInfoSet } from './protocol-v2/DeviceFactoryInfoSet'; +export { default as deviceFactoryInfoGet } from './protocol-v2/DeviceFactoryInfoGet'; +export { default as deviceSettingsGet } from './protocol-v2/DeviceSettingsGet'; +export { default as deviceSettingsSet } from './protocol-v2/DeviceSettingsSet'; +export { default as deviceSettingsPageShow } from './protocol-v2/DeviceSettingsPageShow'; +export { default as deviceUploadWallpaper } from './protocol-v2/DeviceUploadWallpaper'; +export { default as filesystemPermissionFix } from './protocol-v2/FilesystemPermissionFix'; +export { default as filesystemFormat } from './protocol-v2/FilesystemFormat'; +export { default as fileRead } from './FileRead'; +export { default as fileWrite } from './FileWrite'; +export { default as fileDelete } from './FileDelete'; +export { default as dirList } from './DirList'; +export { default as dirMake } from './DirMake'; +export { default as dirRemove } from './DirRemove'; +export { default as pathInfo } from './PathInfo'; +export { default as filesystemFileRead } from './FileRead'; +export { default as filesystemFileWrite } from './FileWrite'; +export { default as uploadPortfolio } from './UploadPortfolio'; +export { default as filesystemFileDelete } from './FileDelete'; +export { default as filesystemDirList } from './DirList'; +export { default as filesystemDirMake } from './DirMake'; +export { default as filesystemDirRemove } from './DirRemove'; +export { default as filesystemPathInfoQuery } from './PathInfo'; + export { default as cipherKeyValue } from './CipherKeyValue'; export { default as allNetworkGetAddress } from './allnetwork/AllNetworkGetAddress'; diff --git a/packages/core/src/api/kaspa/KaspaSignTransaction.ts b/packages/core/src/api/kaspa/KaspaSignTransaction.ts index 76c713bdc..b194159ec 100644 --- a/packages/core/src/api/kaspa/KaspaSignTransaction.ts +++ b/packages/core/src/api/kaspa/KaspaSignTransaction.ts @@ -134,15 +134,14 @@ export default class KaspaSignTransaction extends BaseMethod { model_touch: { min: '4.8.0', }, + pro2: { + min: '0.0.0', + unsupported: true, + }, }; } diff --git a/packages/core/src/api/neo/NeoGetAddress.ts b/packages/core/src/api/neo/NeoGetAddress.ts index 41c7dfc2e..490264147 100644 --- a/packages/core/src/api/neo/NeoGetAddress.ts +++ b/packages/core/src/api/neo/NeoGetAddress.ts @@ -5,6 +5,7 @@ import { UI_REQUEST } from '../../constants/ui-request'; import type { NeoGetAddress as HardwareNeoGetAddress } from '@onekeyfe/hd-transport'; import type { NeoAddress, NeoGetAddressParams } from '../../types/api/neoGetAddress'; +import type { DeviceFirmwareRange } from '../../types'; export default class NeoGetAddress extends BaseMethod { hasBundle = false; @@ -37,8 +38,12 @@ export default class NeoGetAddress extends BaseMethod { }); } - getVersionRange() { + getVersionRange(): DeviceFirmwareRange { return { + pro2: { + min: '0.0.0', + unsupported: true, + }, pro: { min: '4.12.0', }, diff --git a/packages/core/src/api/neo/NeoSignTransaction.ts b/packages/core/src/api/neo/NeoSignTransaction.ts index 7b0a66c78..6cddc736c 100644 --- a/packages/core/src/api/neo/NeoSignTransaction.ts +++ b/packages/core/src/api/neo/NeoSignTransaction.ts @@ -5,6 +5,7 @@ import { validateParams } from '../helpers/paramsValidator'; import { formatAnyHex } from '../helpers/hexUtils'; import type { NeoSignTx } from '@onekeyfe/hd-transport'; +import type { DeviceFirmwareRange } from '../../types'; export default class NeoSignTransaction extends BaseMethod { init() { @@ -29,8 +30,12 @@ export default class NeoSignTransaction extends BaseMethod { }; } - getVersionRange() { + getVersionRange(): DeviceFirmwareRange { return { + pro2: { + min: '0.0.0', + unsupported: true, + }, pro: { min: '4.12.0', }, diff --git a/packages/core/src/api/nervos/NervosGetAddress.ts b/packages/core/src/api/nervos/NervosGetAddress.ts index 05ce9f590..71170e258 100644 --- a/packages/core/src/api/nervos/NervosGetAddress.ts +++ b/packages/core/src/api/nervos/NervosGetAddress.ts @@ -4,7 +4,7 @@ import { BaseMethod } from '../BaseMethod'; import { validateParams, validateResult } from '../helpers/paramsValidator'; import type { NervosGetAddress as HardwareNervosGetAddress } from '@onekeyfe/hd-transport'; -import type { NervosAddress, NervosGetAddressParams } from '../../types'; +import type { DeviceFirmwareRange, NervosAddress, NervosGetAddressParams } from '../../types'; export default class NervosGetAddress extends BaseMethod { hasBundle = false; @@ -40,8 +40,12 @@ export default class NervosGetAddress extends BaseMethod & { @@ -41,8 +45,12 @@ export default class NervosSignTransaction extends BaseMethod { }; } - getVersionRange() { + getVersionRange(): DeviceFirmwareRange { return { + pro2: { + min: '0.0.0', + unsupported: true, + }, model_mini: { min: '3.7.0', }, @@ -59,7 +67,9 @@ export default class NervosSignTransaction extends BaseMethod { res: TypedResponseMessage<'NervosSignedTx'> | TypedResponseMessage<'NervosTxRequest'>, data: Buffer, offset = 0 - ): Promise => { + // 设备可能在最后一个 NervosTxRequest(无 data_length)里返回签名, + // 该消息没有 address 字段,返回类型如实声明为联合类型 + ): Promise => { if (res.type === 'NervosSignedTx') { if (!res?.message?.signature) { throw new Error('No signature returned'); diff --git a/packages/core/src/api/nexa/NexaGetAddress.ts b/packages/core/src/api/nexa/NexaGetAddress.ts index 6d81c14ae..096cfc87f 100644 --- a/packages/core/src/api/nexa/NexaGetAddress.ts +++ b/packages/core/src/api/nexa/NexaGetAddress.ts @@ -4,7 +4,7 @@ import { BaseMethod } from '../BaseMethod'; import { validateParams, validateResult } from '../helpers/paramsValidator'; import type { NexaGetAddress as HardwareNexaGetAddress } from '@onekeyfe/hd-transport'; -import type { NexaGetAddressParams } from '../../types'; +import type { DeviceFirmwareRange, NexaGetAddressParams } from '../../types'; export default class NexaGetAddress extends BaseMethod { hasBundle = false; @@ -43,8 +43,12 @@ export default class NexaGetAddress extends BaseMethod }); } - getVersionRange() { + getVersionRange(): DeviceFirmwareRange { return { + pro2: { + min: '0.0.0', + unsupported: true, + }, model_mini: { min: '3.2.0', }, diff --git a/packages/core/src/api/nexa/NexaSignTransaction.ts b/packages/core/src/api/nexa/NexaSignTransaction.ts index e5d496c0f..f4ffe18ba 100644 --- a/packages/core/src/api/nexa/NexaSignTransaction.ts +++ b/packages/core/src/api/nexa/NexaSignTransaction.ts @@ -6,7 +6,7 @@ import { validateParams } from '../helpers/paramsValidator'; import type { TypedResponseMessage } from '../../device/DeviceCommands'; import type { TypedCall } from '@onekeyfe/hd-transport'; -import type { NexaSignTransactionParams, NexaSignature } from '../../types'; +import type { DeviceFirmwareRange, NexaSignTransactionParams, NexaSignature } from '../../types'; export default class NexaSignTransaction extends BaseMethod { hasBundle = false; @@ -27,8 +27,12 @@ export default class NexaSignTransaction extends BaseMethod = { @@ -33,6 +36,9 @@ const specialVersionRange: Record = { model_touch: { min: '4.7.0', }, + pro2: { + min: '0.0.0', + }, }, [Networks.Manta]: { model_mini: { @@ -41,6 +47,9 @@ const specialVersionRange: Record = { model_touch: { min: '4.9.0', }, + pro2: { + min: '0.0.0', + }, }, }; diff --git a/packages/core/src/api/protocol-v2/DeviceFactoryInfoGet.ts b/packages/core/src/api/protocol-v2/DeviceFactoryInfoGet.ts new file mode 100644 index 000000000..ee9197cb6 --- /dev/null +++ b/packages/core/src/api/protocol-v2/DeviceFactoryInfoGet.ts @@ -0,0 +1,20 @@ +import { BaseMethod } from '../BaseMethod'; + +export default class DeviceFactoryInfoGet extends BaseMethod { + init() { + // Protocol V2 (Pro2) 专属方法,core 调度层统一做非 V2 设备守卫 + this.requireProtocolV2 = true; + this.skipForceUpdateCheck = true; + this.useDevicePassphraseState = false; + this.params = undefined; + } + + async run() { + const res = await this.device.commands.typedCall( + 'DeviceFactoryInfoGet', + 'DeviceFactoryInfo', + {} + ); + return Promise.resolve(res.message); + } +} diff --git a/packages/core/src/api/protocol-v2/DeviceFactoryInfoSet.ts b/packages/core/src/api/protocol-v2/DeviceFactoryInfoSet.ts new file mode 100644 index 000000000..b1a449878 --- /dev/null +++ b/packages/core/src/api/protocol-v2/DeviceFactoryInfoSet.ts @@ -0,0 +1,32 @@ +import { BaseMethod } from '../BaseMethod'; + +import type { DeviceFactoryInfoSetParams } from './helpers'; + +export default class DeviceFactoryInfoSet extends BaseMethod { + init() { + // Protocol V2 (Pro2) 专属方法,core 调度层统一做非 V2 设备守卫 + this.requireProtocolV2 = true; + this.skipForceUpdateCheck = true; + this.useDevicePassphraseState = false; + this.params = { + version: this.payload.version, + serial_number: this.payload.serial_number, + burn_in_completed: this.payload.burn_in_completed, + factory_test_completed: this.payload.factory_test_completed, + manufacture_time: this.payload.manufacture_time, + }; + } + + async run() { + const res = await this.device.commands.typedCall('DeviceFactoryInfoSet', 'Success', { + info: { + version: this.params.version, + serial_number: this.params.serial_number, + burn_in_completed: this.params.burn_in_completed, + factory_test_completed: this.params.factory_test_completed, + manufacture_time: this.params.manufacture_time, + }, + }); + return Promise.resolve(res.message); + } +} diff --git a/packages/core/src/api/protocol-v2/DeviceFirmwareUpdate.ts b/packages/core/src/api/protocol-v2/DeviceFirmwareUpdate.ts new file mode 100644 index 000000000..2df044bae --- /dev/null +++ b/packages/core/src/api/protocol-v2/DeviceFirmwareUpdate.ts @@ -0,0 +1,61 @@ +import { BaseMethod } from '../BaseMethod'; +import { + PROTOCOL_V2_FIRMWARE_UPDATE_OPTIONS, + PROTOCOL_V2_FIRMWARE_UPDATE_RESPONSE_TYPES, + isProtocolV2DeviceDisconnectedError, + normalizeFirmwareTargets, +} from './helpers'; +import { UI_REQUEST, createUiMessage } from '../../events/ui-request'; + +import type { KnownDevice } from '../../types'; +import type { MessageFromOneKey } from '@onekeyfe/hd-transport'; +import type { DeviceFirmwareUpdateParams } from './helpers'; + +export default class DeviceFirmwareUpdate extends BaseMethod { + init() { + // Protocol V2 (Pro2) 专属方法,core 调度层统一做非 V2 设备守卫 + this.requireProtocolV2 = true; + this.skipForceUpdateCheck = true; + this.useDevicePassphraseState = false; + this.params = { + targets: this.payload.targets, + targetId: this.payload.targetId, + target_id: this.payload.target_id, + path: this.payload.path, + }; + } + + async run() { + const targets = normalizeFirmwareTargets(this.params); + try { + const res = await this.device.commands.typedCall( + 'DeviceFirmwareUpdateRequest', + PROTOCOL_V2_FIRMWARE_UPDATE_RESPONSE_TYPES, + { + targets, + }, + { + ...PROTOCOL_V2_FIRMWARE_UPDATE_OPTIONS, + onIntermediateResponse: (response: MessageFromOneKey) => { + if (response.type !== 'DeviceFirmwareUpdateStatus') return; + this.postMessage( + createUiMessage(UI_REQUEST.FIRMWARE_PROGRESS, { + device: this.device.toMessageObject() as KnownDevice, + progress: 99, + progressType: 'installingFirmware', + }) + ); + }, + } + ); + return res.message; + } catch (error) { + if (isProtocolV2DeviceDisconnectedError(error)) { + return { + message: 'Device firmware update started', + }; + } + throw error; + } + } +} diff --git a/packages/core/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts b/packages/core/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts new file mode 100644 index 000000000..e8814912e --- /dev/null +++ b/packages/core/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts @@ -0,0 +1,55 @@ +import { BaseMethod } from '../BaseMethod'; +import { invalidParameter } from '../helpers/filesystemValidation'; + +import type { DeviceFirmwareUpdateStatusGetParams } from './helpers'; +import type { DeviceFirmwareUpdateRecordFields } from '@onekeyfe/hd-transport'; + +const DEVICE_FIRMWARE_UPDATE_STATUS_FIELDS = ['status', 'payload_version', 'path'] as const; + +function normalizeStatusFields( + fields: DeviceFirmwareUpdateStatusGetParams['fields'] +): DeviceFirmwareUpdateRecordFields | undefined { + if (fields === undefined || fields === null) return undefined; + if (typeof fields !== 'object' || Array.isArray(fields)) { + throw invalidParameter('Parameter [fields] must be an object.'); + } + + const unknownField = Object.keys(fields).find( + field => !DEVICE_FIRMWARE_UPDATE_STATUS_FIELDS.includes(field as any) + ); + if (unknownField) { + throw invalidParameter(`Unsupported firmware update status field: ${unknownField}`); + } + + const normalized: DeviceFirmwareUpdateRecordFields = {}; + for (const field of DEVICE_FIRMWARE_UPDATE_STATUS_FIELDS) { + const value = fields[field]; + if (value !== undefined) { + if (typeof value !== 'boolean') { + throw invalidParameter(`Parameter [fields.${field}] must be a boolean.`); + } + normalized[field] = value; + } + } + return normalized; +} + +export default class DeviceGetFirmwareUpdateStatus extends BaseMethod { + init() { + // Protocol V2 (Pro2) 专属方法,core 调度层统一做非 V2 设备守卫 + this.requireProtocolV2 = true; + this.skipForceUpdateCheck = true; + this.useDevicePassphraseState = false; + const fields = normalizeStatusFields(this.payload.fields); + this.params = fields ? { fields } : {}; + } + + async run() { + const res = await this.device.commands.typedCall( + 'DeviceFirmwareUpdateStatusGet', + 'DeviceFirmwareUpdateStatus', + this.params + ); + return Promise.resolve(res.message); + } +} diff --git a/packages/core/src/api/protocol-v2/DeviceGetOnboardingStatus.ts b/packages/core/src/api/protocol-v2/DeviceGetOnboardingStatus.ts new file mode 100644 index 000000000..d8c5b9bcf --- /dev/null +++ b/packages/core/src/api/protocol-v2/DeviceGetOnboardingStatus.ts @@ -0,0 +1,18 @@ +import { BaseMethod } from '../BaseMethod'; + +export default class DeviceGetOnboardingStatus extends BaseMethod { + init() { + this.requireProtocolV2 = true; + this.skipForceUpdateCheck = true; + this.useDevicePassphraseState = false; + } + + async run() { + const { message } = await this.device.commands.typedCall( + 'DevGetOnboardingStatus', + 'DevOnboardingStatus', + {} + ); + return message; + } +} diff --git a/packages/core/src/api/protocol-v2/DeviceInfoGet.ts b/packages/core/src/api/protocol-v2/DeviceInfoGet.ts new file mode 100644 index 000000000..115739cd1 --- /dev/null +++ b/packages/core/src/api/protocol-v2/DeviceInfoGet.ts @@ -0,0 +1,148 @@ +import { createDeviceNotSupportMethodError } from '@onekeyfe/hd-shared'; + +import { UI_REQUEST } from '../../constants/ui-request'; +import { PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS } from '../../protocols/protocol-v2'; +import { BaseMethod } from '../BaseMethod'; +import { invalidParameter } from '../helpers/filesystemValidation'; + +export type DeviceInfoGetTargets = { + hw?: boolean; + fw?: boolean; + coprocessor?: boolean; + se1?: boolean; + se2?: boolean; + se3?: boolean; + se4?: boolean; + status?: boolean; +}; + +export type DeviceInfoGetTypes = { + version?: boolean; + build_id?: boolean; + hash?: boolean; + specific?: boolean; +}; + +export type DeviceInfoGetParams = { + targets?: DeviceInfoGetTargets; + types?: DeviceInfoGetTypes; +}; + +const TARGET_KEYS: (keyof DeviceInfoGetTargets)[] = [ + 'hw', + 'fw', + 'coprocessor', + 'se1', + 'se2', + 'se3', + 'se4', + 'status', +]; + +const TYPE_KEYS: (keyof DeviceInfoGetTypes)[] = ['version', 'build_id', 'hash', 'specific']; + +const DEFAULT_TARGETS: DeviceInfoGetTargets = { + hw: true, + fw: true, + coprocessor: true, + status: true, +}; + +const DEFAULT_TYPES: DeviceInfoGetTypes = { + version: true, + specific: true, +}; + +function pickBooleanKeys>( + value: unknown, + keys: (keyof T)[] +): T | undefined { + if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined; + const source = value as Record; + const result = {} as T; + let hasKey = false; + for (const key of keys) { + if (source[key as string]) { + result[key] = true as T[keyof T]; + hasKey = true; + } + } + return hasKey ? result : undefined; +} + +function assertKnownKeys(value: unknown, keys: string[], name: string) { + if (value == null) return; + if (typeof value !== 'object' || Array.isArray(value)) { + throw invalidParameter(`Parameter [${name}] must be an object.`); + } + const allowed = new Set(keys); + const unknownKeys = Object.keys(value).filter(key => !allowed.has(key)); + if (unknownKeys.length > 0) { + throw invalidParameter( + `Parameter [${name}] contains unsupported key(s): ${unknownKeys.join(', ')}.` + ); + } +} + +function normalizeTargets(value: unknown): DeviceInfoGetTargets | undefined { + assertKnownKeys( + value, + TARGET_KEYS.map(key => String(key)), + 'targets' + ); + return pickBooleanKeys(value, TARGET_KEYS); +} + +function normalizeTypes(value: unknown): DeviceInfoGetTypes | undefined { + assertKnownKeys( + value, + TYPE_KEYS.map(key => String(key)), + 'types' + ); + return pickBooleanKeys(value, TYPE_KEYS); +} + +/** + * 原生 DeviceInfoGet(Protocol V2 only)。 + * + * 与 getDeviceInfo 不同:不构建 DeviceProfile、不更新设备缓存, + * 按调用方给定的 targets/types 原样请求并返回未加工的 DeviceInfo 消息, + * 用于调试固件字段上报。 + */ +export default class DeviceInfoGet extends BaseMethod<{ + targets: DeviceInfoGetTargets; + types: DeviceInfoGetTypes; +}> { + init() { + // Protocol V2 (Pro2) 专属方法,core 调度层统一做非 V2 设备守卫 + this.requireProtocolV2 = true; + this.allowDeviceMode = [ + ...this.allowDeviceMode, + UI_REQUEST.NOT_INITIALIZE, + UI_REQUEST.BOOTLOADER, + ]; + this.useDevicePassphraseState = false; + this.skipForceUpdateCheck = true; + this.params = { + targets: normalizeTargets(this.payload.targets) ?? DEFAULT_TARGETS, + types: normalizeTypes(this.payload.types) ?? DEFAULT_TYPES, + }; + } + + async run() { + if (!this.device.isProtocolV2()) { + throw createDeviceNotSupportMethodError(this.name, this.device.getCurrentFirmwareType()); + } + + const res = await this.device.commands.typedCall( + 'DeviceInfoGet', + 'DeviceInfo', + { + targets: this.params.targets, + types: this.params.types, + }, + { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS } + ); + return Promise.resolve(res.message); + } +} diff --git a/packages/core/src/api/protocol-v2/DeviceReboot.ts b/packages/core/src/api/protocol-v2/DeviceReboot.ts new file mode 100644 index 000000000..f38c6b2df --- /dev/null +++ b/packages/core/src/api/protocol-v2/DeviceReboot.ts @@ -0,0 +1,24 @@ +import { BaseMethod } from '../BaseMethod'; +import { normalizeRebootType } from './helpers'; + +import type { DeviceRebootParams } from './helpers'; + +export default class DeviceReboot extends BaseMethod { + init() { + // Protocol V2 (Pro2) 专属方法,core 调度层统一做非 V2 设备守卫 + this.requireProtocolV2 = true; + this.skipForceUpdateCheck = true; + this.useDevicePassphraseState = false; + this.params = { + rebootType: this.payload.rebootType, + reboot_type: this.payload.reboot_type, + }; + } + + async run() { + const res = await this.device.commands.typedCall('DeviceReboot', 'Success', { + reboot_type: normalizeRebootType(this.params.reboot_type ?? this.params.rebootType), + }); + return Promise.resolve(res.message); + } +} diff --git a/packages/core/src/api/protocol-v2/DeviceSessionGet.ts b/packages/core/src/api/protocol-v2/DeviceSessionGet.ts new file mode 100644 index 000000000..050dd67e8 --- /dev/null +++ b/packages/core/src/api/protocol-v2/DeviceSessionGet.ts @@ -0,0 +1,19 @@ +import { BaseMethod } from '../BaseMethod'; + +export default class DeviceSessionGet extends BaseMethod { + init() { + this.requireProtocolV2 = true; + this.useDevicePassphraseState = false; + this.skipForceUpdateCheck = true; + this.params = undefined; + } + + async run() { + const { message } = await this.device.commands.typedCall( + 'DeviceSessionGet', + 'DeviceSession', + {} + ); + return message; + } +} diff --git a/packages/core/src/api/protocol-v2/DeviceSettingsGet.ts b/packages/core/src/api/protocol-v2/DeviceSettingsGet.ts new file mode 100644 index 000000000..f9039a331 --- /dev/null +++ b/packages/core/src/api/protocol-v2/DeviceSettingsGet.ts @@ -0,0 +1,16 @@ +import { BaseMethod } from '../BaseMethod'; + +export default class DeviceSettingsGet extends BaseMethod { + init() { + this.requireProtocolV2 = true; + this.unlockPolicy = 'retry-on-locked'; + this.skipForceUpdateCheck = true; + this.useDevicePassphraseState = false; + this.params = undefined; + } + + async run() { + const res = await this.device.commands.typedCall('DeviceSettingsGet', 'DeviceSettings', {}); + return Promise.resolve(res.message); + } +} diff --git a/packages/core/src/api/protocol-v2/DeviceSettingsPageShow.ts b/packages/core/src/api/protocol-v2/DeviceSettingsPageShow.ts new file mode 100644 index 000000000..dfbbe080e --- /dev/null +++ b/packages/core/src/api/protocol-v2/DeviceSettingsPageShow.ts @@ -0,0 +1,59 @@ +import { DeviceSettingsPage } from '@onekeyfe/hd-transport'; + +import { BaseMethod } from '../BaseMethod'; +import { invalidParameter } from '../helpers/filesystemValidation'; + +export type SupportedDeviceSettingsPage = DeviceSettingsPage; + +export type DeviceSettingsPageShowParams = { + page?: + | SupportedDeviceSettingsPage + | 'DeviceReset' + | 'DevicePinChange' + | 'DevicePassphrase' + | 'DeviceAirgap'; + fieldName?: string; + field_name?: string; +}; + +const SUPPORTED_PAGES = new Set([ + DeviceSettingsPage.DeviceReset, + DeviceSettingsPage.DevicePinChange, + DeviceSettingsPage.DevicePassphrase, + DeviceSettingsPage.DeviceAirgap, +]); + +function normalizePage(value: DeviceSettingsPageShowParams['page']): SupportedDeviceSettingsPage { + const page = typeof value === 'string' ? DeviceSettingsPage[value] : value; + if (page !== undefined && SUPPORTED_PAGES.has(page)) { + return page; + } + throw invalidParameter( + 'Parameter [page] must be DeviceReset, DevicePinChange, DevicePassphrase, or DeviceAirgap.' + ); +} + +export default class DeviceSettingsPageShow extends BaseMethod<{ + page: SupportedDeviceSettingsPage; + field_name?: string; +}> { + init() { + this.requireProtocolV2 = true; + this.unlockPolicy = 'retry-on-locked'; + this.skipForceUpdateCheck = true; + this.useDevicePassphraseState = false; + this.params = { + page: normalizePage(this.payload.page), + field_name: this.payload.field_name ?? this.payload.fieldName, + }; + } + + async run() { + const res = await this.device.commands.typedCall( + 'DeviceSettingsPageShow', + 'Success', + this.params + ); + return Promise.resolve(res.message); + } +} diff --git a/packages/core/src/api/protocol-v2/DeviceSettingsSet.ts b/packages/core/src/api/protocol-v2/DeviceSettingsSet.ts new file mode 100644 index 000000000..626c6afe7 --- /dev/null +++ b/packages/core/src/api/protocol-v2/DeviceSettingsSet.ts @@ -0,0 +1,39 @@ +import { BaseMethod } from '../BaseMethod'; +import { invalidParameter } from '../helpers/filesystemValidation'; + +import type { DeviceSettings } from '@onekeyfe/hd-transport'; + +export type DeviceSettingsSetParams = { + settings?: Omit; +}; + +export default class DeviceSettingsSet extends BaseMethod<{ + settings: Omit; +}> { + init() { + const { settings } = this.payload; + if (!settings || typeof settings !== 'object' || Array.isArray(settings)) { + throw invalidParameter('Parameter [settings] must be an object.'); + } + + const { + passphrase_enable: _passphraseEnable, + airgap_mode: _airgapMode, + ...supported + } = settings as DeviceSettings; + if (Object.keys(supported).length === 0) { + throw invalidParameter('Parameter [settings] must contain at least one supported setting.'); + } + + this.requireProtocolV2 = true; + this.unlockPolicy = 'retry-on-locked'; + this.skipForceUpdateCheck = true; + this.useDevicePassphraseState = false; + this.params = { settings: supported }; + } + + async run() { + const res = await this.device.commands.typedCall('DeviceSettingsSet', 'Success', this.params); + return Promise.resolve(res.message); + } +} diff --git a/packages/core/src/api/protocol-v2/DeviceStatusGet.ts b/packages/core/src/api/protocol-v2/DeviceStatusGet.ts new file mode 100644 index 000000000..3ce84b5e8 --- /dev/null +++ b/packages/core/src/api/protocol-v2/DeviceStatusGet.ts @@ -0,0 +1,14 @@ +import { BaseMethod } from '../BaseMethod'; + +export default class DeviceStatusGet extends BaseMethod { + init() { + this.requireProtocolV2 = true; + this.useDevicePassphraseState = false; + this.skipForceUpdateCheck = true; + } + + async run() { + const { message } = await this.device.commands.typedCall('DeviceStatusGet', 'DeviceStatus', {}); + return message; + } +} diff --git a/packages/core/src/api/protocol-v2/DeviceUploadWallpaper.ts b/packages/core/src/api/protocol-v2/DeviceUploadWallpaper.ts new file mode 100644 index 000000000..dac6315a1 --- /dev/null +++ b/packages/core/src/api/protocol-v2/DeviceUploadWallpaper.ts @@ -0,0 +1,124 @@ +import { blake2s } from '@noble/hashes/blake2s'; +import { bytesToHex } from '@noble/hashes/utils'; + +import { BaseMethod } from '../BaseMethod'; +import { invalidParameter } from '../helpers/filesystemValidation'; +import { writeProtocolV2File } from '../helpers/protocolV2FileWrite'; +import { + encodePro2Wallpaper, + PRO2_WALLPAPER_HEIGHT, + PRO2_WALLPAPER_WIDTH, + type Pro2WallpaperColorFormat, +} from '../../utils/pro2Wallpaper'; + +export type DeviceUploadWallpaperParams = { + width: number; + height: number; + rgba: Uint8Array | ArrayBuffer; + fileName?: string; + chunkSize?: number; +}; + +export type DeviceUploadWallpaperResponse = { + path: string; + size: number; + colorFormat: Pro2WallpaperColorFormat; + message?: string; +}; + +const WALLPAPER_DIRECTORY = 'vol0:/wallpapers/user'; +const SAFE_FILE_NAME = /^[A-Za-z0-9_-]+(?:\.bin)?$/; + +function normalizeFileName(fileName: string | undefined, data: Uint8Array): string { + if (fileName !== undefined && (!fileName || !SAFE_FILE_NAME.test(fileName))) { + throw invalidParameter( + 'Parameter [fileName] may only contain letters, numbers, underscores, hyphens and an optional .bin suffix.' + ); + } + const baseName = fileName ?? `wallpaper-${bytesToHex(blake2s(data)).slice(0, 12)}`; + return baseName.endsWith('.bin') ? baseName : `${baseName}.bin`; +} + +export default class DeviceUploadWallpaper extends BaseMethod { + private encoded?: { data: Uint8Array; colorFormat: Pro2WallpaperColorFormat }; + + private directoryReady = false; + + private uploaded = false; + + private path = ''; + + init() { + const { width, height, rgba, fileName, chunkSize } = this.payload; + if (width !== PRO2_WALLPAPER_WIDTH || height !== PRO2_WALLPAPER_HEIGHT) { + throw invalidParameter( + `Pro2 wallpaper dimensions must be ${PRO2_WALLPAPER_WIDTH}x${PRO2_WALLPAPER_HEIGHT}.` + ); + } + if (!(rgba instanceof ArrayBuffer) && !ArrayBuffer.isView(rgba)) { + throw invalidParameter('Parameter [rgba] must be an ArrayBuffer or Uint8Array.'); + } + if (chunkSize !== undefined && (!Number.isInteger(chunkSize) || chunkSize <= 0)) { + throw invalidParameter('Parameter [chunkSize] must be a positive integer.'); + } + + const rgbaBytes = + rgba instanceof ArrayBuffer + ? rgba + : new Uint8Array(rgba.buffer, rgba.byteOffset, rgba.byteLength); + this.encoded = encodePro2Wallpaper({ width, height, rgba: rgbaBytes }); + this.path = `${WALLPAPER_DIRECTORY}/${normalizeFileName(fileName, this.encoded.data)}`; + this.params = { width, height, rgba: rgbaBytes, fileName, chunkSize }; + this.requireProtocolV2 = true; + this.unlockPolicy = 'retry-on-locked'; + this.skipForceUpdateCheck = true; + this.useDevicePassphraseState = false; + } + + private async ensureDirectory() { + if (this.directoryReady) return; + try { + await this.device.commands.typedCall('FilesystemDirMake', 'Success', { + path: WALLPAPER_DIRECTORY, + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (!/exist/i.test(message)) throw error; + } + this.directoryReady = true; + } + + private async upload() { + if (this.uploaded) return; + const { encoded } = this; + if (!encoded) throw invalidParameter('Wallpaper data has not been initialized.'); + + await writeProtocolV2File({ + commands: this.device.commands, + path: this.path, + data: encoded.data, + totalSize: encoded.data.byteLength, + chunkSize: this.params.chunkSize, + overwrite: true, + append: false, + throwIfAborted: () => this.throwIfAborted(), + }); + this.uploaded = true; + } + + async run(): Promise { + const { encoded } = this; + if (!encoded) throw invalidParameter('Wallpaper data has not been initialized.'); + await this.ensureDirectory(); + await this.upload(); + const response = await this.device.commands.typedCall('DeviceSettingsSet', 'Success', { + settings: { wallpaper_path: this.path }, + }); + return { + path: this.path, + size: encoded.data.byteLength, + colorFormat: encoded.colorFormat, + message: response.message?.message, + }; + } +} diff --git a/packages/core/src/api/protocol-v2/FilesystemFormat.ts b/packages/core/src/api/protocol-v2/FilesystemFormat.ts new file mode 100644 index 000000000..ad46154c8 --- /dev/null +++ b/packages/core/src/api/protocol-v2/FilesystemFormat.ts @@ -0,0 +1,19 @@ +import { BaseMethod } from '../BaseMethod'; + +export default class FilesystemFormat extends BaseMethod { + init() { + // Protocol V2 (Pro2) 专属方法,core 调度层统一做非 V2 设备守卫 + this.requireProtocolV2 = true; + this.skipForceUpdateCheck = true; + this.useDevicePassphraseState = false; + this.params = undefined; + } + + async run() { + const res = await this.device.commands.typedCall('FilesystemFormat', 'Success', { + data: true, + user: true, + }); + return Promise.resolve(res.message); + } +} diff --git a/packages/core/src/api/protocol-v2/FilesystemPermissionFix.ts b/packages/core/src/api/protocol-v2/FilesystemPermissionFix.ts new file mode 100644 index 000000000..6ec368898 --- /dev/null +++ b/packages/core/src/api/protocol-v2/FilesystemPermissionFix.ts @@ -0,0 +1,16 @@ +import { BaseMethod } from '../BaseMethod'; + +export default class FilesystemPermissionFix extends BaseMethod { + init() { + // Protocol V2 (Pro2) 专属方法,core 调度层统一做非 V2 设备守卫 + this.requireProtocolV2 = true; + this.skipForceUpdateCheck = true; + this.useDevicePassphraseState = false; + this.params = undefined; + } + + async run() { + const res = await this.device.commands.typedCall('FilesystemPermissionFix', 'Success', {}); + return Promise.resolve(res.message); + } +} diff --git a/packages/core/src/api/protocol-v2/Ping.ts b/packages/core/src/api/protocol-v2/Ping.ts new file mode 100644 index 000000000..b897ebc81 --- /dev/null +++ b/packages/core/src/api/protocol-v2/Ping.ts @@ -0,0 +1,18 @@ +import { BaseMethod } from '../BaseMethod'; + +export default class Ping extends BaseMethod<{ message?: string }> { + init() { + // Protocol V2 (Pro2) 专属方法,core 调度层统一做非 V2 设备守卫 + this.requireProtocolV2 = true; + this.skipForceUpdateCheck = true; + this.useDevicePassphraseState = false; + this.params = { message: this.payload.message }; + } + + async run() { + const res = await this.device.commands.typedCall('Ping', 'Success', { + message: this.params.message ?? '', + }); + return Promise.resolve(res.message); + } +} diff --git a/packages/core/src/api/protocol-v2/ProtocolInfoRequest.ts b/packages/core/src/api/protocol-v2/ProtocolInfoRequest.ts new file mode 100644 index 000000000..e0e0603eb --- /dev/null +++ b/packages/core/src/api/protocol-v2/ProtocolInfoRequest.ts @@ -0,0 +1,16 @@ +import { BaseMethod } from '../BaseMethod'; + +export default class ProtocolInfoRequest extends BaseMethod { + init() { + // Protocol V2 (Pro2) 专属方法,core 调度层统一做非 V2 设备守卫 + this.requireProtocolV2 = true; + this.skipForceUpdateCheck = true; + this.useDevicePassphraseState = false; + this.params = undefined; + } + + async run() { + const res = await this.device.commands.typedCall('ProtocolInfoRequest', 'ProtocolInfo', {}); + return Promise.resolve(res.message); + } +} diff --git a/packages/core/src/api/protocol-v2/helpers.ts b/packages/core/src/api/protocol-v2/helpers.ts new file mode 100644 index 000000000..eea63926e --- /dev/null +++ b/packages/core/src/api/protocol-v2/helpers.ts @@ -0,0 +1,206 @@ +import { DeviceRebootType } from '@onekeyfe/hd-transport'; + +import { invalidParameter, validateNonEmptyString } from '../helpers/filesystemValidation'; +import { ProtocolV2FirmwareTargetType } from '../../protocols/protocol-v2/firmware'; + +import type { + DeviceFirmwareTarget, + DeviceFirmwareTargetType, + DeviceFirmwareUpdateRecordFields, + TransportCallOptions, +} from '@onekeyfe/hd-transport'; + +export type RebootTypeInput = DeviceRebootType | keyof typeof DeviceRebootType | string | number; + +export type DeviceRebootParams = { + rebootType?: RebootTypeInput; + reboot_type?: RebootTypeInput; +}; + +export type DeviceFirmwareTargetInput = + | DeviceFirmwareTarget + | { + targetId?: DeviceFirmwareTargetType | string | number; + target_id?: DeviceFirmwareTargetType | string | number; + path: string; + }; + +export type DeviceFirmwareUpdateParams = { + targets?: DeviceFirmwareTargetInput[]; + targetId?: DeviceFirmwareTargetType | string | number; + target_id?: DeviceFirmwareTargetType | string | number; + path?: string; +}; + +export type DeviceFirmwareUpdateStatusGetParams = { + fields?: DeviceFirmwareUpdateRecordFields; +}; + +export type DeviceFactoryInfoSetParams = { + version?: number; + serial_number?: string; + burn_in_completed?: boolean; + factory_test_completed?: boolean; + manufacture_time?: { + year: number; + month: number; + day: number; + hour: number; + minute: number; + second: number; + }; +}; + +const DEVICE_REBOOT_TYPES: Record = { + Normal: DeviceRebootType.Normal, + normal: DeviceRebootType.Normal, + Romloader: DeviceRebootType.Romloader, + romloader: DeviceRebootType.Romloader, + Boardloader: DeviceRebootType.Romloader, + boardloader: DeviceRebootType.Romloader, + Bootloader: DeviceRebootType.Bootloader, + bootloader: DeviceRebootType.Bootloader, +}; + +export const PROTOCOL_V2_FIRMWARE_UPDATE_OPTIONS: TransportCallOptions = { + intermediateTypes: ['DeviceFirmwareUpdateStatus'], +}; + +export const PROTOCOL_V2_FIRMWARE_UPDATE_RESPONSE_TYPES: ( + | 'Success' + | 'DeviceFirmwareUpdateStatus' +)[] = ['Success', 'DeviceFirmwareUpdateStatus']; + +export const getProtocolV2UnknownErrorText = (error: unknown) => { + if (!error) { + return ''; + } + if (typeof error === 'string') { + return error; + } + + const parts: string[] = []; + if (error instanceof Error) { + parts.push(error.name, error.message); + } + + if (typeof error === 'object') { + const record = error as Record; + for (const field of ['name', 'message', 'reason', 'code', 'errorCode', 'nativeErrorCode']) { + const value = record[field]; + if (value !== undefined && value !== null) { + parts.push(String(value)); + } + } + } + + const stringified = String(error); + if (stringified && stringified !== '[object Object]') { + parts.push(stringified); + } + + return parts.filter(Boolean).join(' '); +}; + +export const isProtocolV2DeviceDisconnectedError = (error: unknown) => { + const message = getProtocolV2UnknownErrorText(error).toLowerCase(); + const compactMessage = message.replace(/\s+/g, ''); + return ( + message.includes('notfounderror') || + (message.includes("failed to execute 'open'") && message.includes('usbdevice')) || + message.includes('device was disconnected') || + message.includes('device disconnected') || + message.includes('device disconnect') || + message.includes('was disconnected') || + message.includes('bledevicedisconnected') || + message.includes('bleconnectederror') || + message.includes('connected error is always runtime error') || + message.includes('connection has timed out unexpectedly') || + message.includes('connection error has occured') || + message.includes('connection error has occurred') || + message.includes('transferIn') || + message.includes('transferin') || + message.includes('usbdevice') || + message.includes('multiplatformbleadapter') || + message.includes('multipalformebleadapter') || + compactMessage.includes('rxerrorerror6') || + message.includes('rxerror error 6') + ); +}; + +export function normalizeRebootType(value: RebootTypeInput | undefined): DeviceRebootType { + if (typeof value === 'number') return value; + if (typeof value === 'string') { + const numeric = Number(value); + if (Number.isFinite(numeric)) return numeric; + if (value in DEVICE_REBOOT_TYPES) return DEVICE_REBOOT_TYPES[value]; + } + return DeviceRebootType.Normal; +} + +// 与 firmware-pro2 的 proto_target_to_firmware_target 安装白名单保持一致。 +const INSTALLABLE_FIRMWARE_TARGET_IDS = new Set([ + ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_CRATE, + ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER, + ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1, + ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2, + ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_COPROCESSOR, + ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE01, + ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE02, + ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE03, + ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE04, +]); +const FIRMWARE_TARGET_ID_BY_NAME = new Map( + Object.entries(ProtocolV2FirmwareTargetType).flatMap(([key, value]) => + INSTALLABLE_FIRMWARE_TARGET_IDS.has(value) ? [[key, value as DeviceFirmwareTargetType]] : [] + ) +); + +function normalizeTargetId( + value: DeviceFirmwareTargetType | string | number | undefined, + name: string +): DeviceFirmwareTargetType { + if (value === undefined || value === null) { + throw invalidParameter(`Missing required parameter: ${name}`); + } + const named = typeof value === 'string' ? FIRMWARE_TARGET_ID_BY_NAME.get(value) : undefined; + const numeric = named ?? (typeof value === 'number' ? value : Number(value)); + if (Number.isSafeInteger(numeric) && INSTALLABLE_FIRMWARE_TARGET_IDS.has(numeric)) { + return numeric as DeviceFirmwareTargetType; + } + throw invalidParameter( + `Parameter [${name}] must be an installable firmware target id (one of ${[ + ...INSTALLABLE_FIRMWARE_TARGET_IDS, + ].join(', ')}).` + ); +} + +export function normalizeFirmwareTargets( + params: DeviceFirmwareUpdateParams +): DeviceFirmwareTarget[] { + const targets = + params.targets ?? + (params.path + ? [ + { + target_id: params.target_id ?? params.targetId ?? 0, + path: params.path, + }, + ] + : []); + + if (!Array.isArray(targets) || targets.length === 0) { + throw invalidParameter('Parameter [targets] must contain at least one firmware target.'); + } + + return targets.map((target, index) => { + if (!target || typeof target !== 'object') { + throw invalidParameter(`Parameter [targets.${index}] must be an object.`); + } + const targetId = target.target_id ?? target.targetId; + return { + target_id: normalizeTargetId(targetId, `targets.${index}.target_id`), + path: validateNonEmptyString(target.path, `targets.${index}.path`), + }; + }); +} diff --git a/packages/core/src/api/scdo/ScdoGetAddress.ts b/packages/core/src/api/scdo/ScdoGetAddress.ts index c8231139b..ac925d411 100644 --- a/packages/core/src/api/scdo/ScdoGetAddress.ts +++ b/packages/core/src/api/scdo/ScdoGetAddress.ts @@ -4,7 +4,7 @@ import { BaseMethod } from '../BaseMethod'; import { validateParams, validateResult } from '../helpers/paramsValidator'; import type { ScdoGetAddress as HardwareScdoGetAddress } from '@onekeyfe/hd-transport'; -import type { ScdoAddress, ScdoGetAddressParams } from '../../types'; +import type { DeviceFirmwareRange, ScdoAddress, ScdoGetAddressParams } from '../../types'; export default class ScdoGetAddress extends BaseMethod { hasBundle = false; @@ -38,8 +38,12 @@ export default class ScdoGetAddress extends BaseMethod }); } - getVersionRange() { + getVersionRange(): DeviceFirmwareRange { return { + pro2: { + min: '0.0.0', + unsupported: true, + }, model_touch: { min: '4.10.0', }, diff --git a/packages/core/src/api/scdo/ScdoSignMessage.ts b/packages/core/src/api/scdo/ScdoSignMessage.ts index 782fb9d72..70a3aa561 100644 --- a/packages/core/src/api/scdo/ScdoSignMessage.ts +++ b/packages/core/src/api/scdo/ScdoSignMessage.ts @@ -5,6 +5,7 @@ import { validateParams } from '../helpers/paramsValidator'; import { stripHexPrefix } from '../helpers/hexUtils'; import type { ScdoSignMessage as HardwareScdoSignMessage } from '@onekeyfe/hd-transport'; +import type { DeviceFirmwareRange } from '../../types'; export default class ScdoSignMessage extends BaseMethod { init() { @@ -28,8 +29,12 @@ export default class ScdoSignMessage extends BaseMethod }; } - getVersionRange() { + getVersionRange(): DeviceFirmwareRange { return { + pro2: { + min: '0.0.0', + unsupported: true, + }, model_touch: { min: '4.10.0', }, diff --git a/packages/core/src/api/scdo/ScdoSignTransaction.ts b/packages/core/src/api/scdo/ScdoSignTransaction.ts index 21b9135ea..09b7ab221 100644 --- a/packages/core/src/api/scdo/ScdoSignTransaction.ts +++ b/packages/core/src/api/scdo/ScdoSignTransaction.ts @@ -6,7 +6,7 @@ import { BaseMethod } from '../BaseMethod'; import { validateParams } from '../helpers/paramsValidator'; import { formatAnyHex, stripHexStartZeroes } from '../helpers/hexUtils'; -import type { ScdoSignTransactionParams } from '../../types'; +import type { DeviceFirmwareRange, ScdoSignTransactionParams } from '../../types'; import type { ScdoSignTx as HardwareScdoSignTx, ScdoSignedTx, @@ -51,8 +51,12 @@ export default class ScdoSignTransaction extends BaseMethod }; } - getVersionRange() { + getVersionRange(): DeviceFirmwareRange { return { + pro2: { + min: '0.0.0', + unsupported: true, + }, model_touch: { min: '4.10.0', }, diff --git a/packages/core/src/api/solana/SolGetAddress.ts b/packages/core/src/api/solana/SolGetAddress.ts index 9153e4d45..4f2d2ed42 100644 --- a/packages/core/src/api/solana/SolGetAddress.ts +++ b/packages/core/src/api/solana/SolGetAddress.ts @@ -38,6 +38,10 @@ export default class SolGetAddress extends BaseMethod { }); } + getVersionRange() { + return {}; + } + async run() { const responses: SolanaAddress[] = []; diff --git a/packages/core/src/api/solana/SolSignMessage.ts b/packages/core/src/api/solana/SolSignMessage.ts index 62f1f0041..efa8f94a0 100644 --- a/packages/core/src/api/solana/SolSignMessage.ts +++ b/packages/core/src/api/solana/SolSignMessage.ts @@ -30,6 +30,9 @@ export default class SolSignMessage extends BaseMethod { hasBundle = false; @@ -38,6 +38,15 @@ export default class StellarGetAddress extends BaseMethod { switch (op.type) { case 'createAccount': diff --git a/packages/core/src/api/sui/SuiGetAddress.ts b/packages/core/src/api/sui/SuiGetAddress.ts index 502b7d95a..a213be753 100644 --- a/packages/core/src/api/sui/SuiGetAddress.ts +++ b/packages/core/src/api/sui/SuiGetAddress.ts @@ -2,9 +2,8 @@ import { UI_REQUEST } from '../../constants/ui-request'; import { serializedPath, validatePath } from '../helpers/pathUtils'; import { BaseMethod } from '../BaseMethod'; import { validateParams, validateResult } from '../helpers/paramsValidator'; -import { supportBatchPublicKey } from '../../utils/deviceFeaturesUtils'; import { publicKeyToAddress } from './normalize'; -import { batchGetPublickeys } from '../helpers/batchGetPublickeys'; +import { batchGetPublickeys, supportBatchPublicKeyByDevice } from '../helpers/batchGetPublickeys'; import type { SuiAddress, SuiGetAddressParams } from '../../types'; import type { SuiGetAddress as HardwareSuiGetAddress } from '@onekeyfe/hd-transport'; @@ -49,6 +48,9 @@ export default class SuiGetAddress extends BaseMethod { getVersionRange() { return { + pro2: { + min: '0.0.0', + }, model_mini: { min: '3.0.0', }, @@ -59,7 +61,7 @@ export default class SuiGetAddress extends BaseMethod { } async run() { - const supportsBatchPublicKey = supportBatchPublicKey(this.device?.features); + const supportsBatchPublicKey = supportBatchPublicKeyByDevice(this.device); let responses: SuiAddress[] = []; if (supportsBatchPublicKey) { const publicKeyRes = await batchGetPublickeys(this.device, this.params, 'ed25519', 784); diff --git a/packages/core/src/api/sui/SuiGetPublicKey.ts b/packages/core/src/api/sui/SuiGetPublicKey.ts index eba6690e8..f7a7e2e6a 100644 --- a/packages/core/src/api/sui/SuiGetPublicKey.ts +++ b/packages/core/src/api/sui/SuiGetPublicKey.ts @@ -40,6 +40,9 @@ export default class SuiGetPublicKey extends BaseMethod { getVersionRange() { return { + pro2: { + min: '0.0.0', + }, model_mini: { min: '3.0.0', }, diff --git a/packages/core/src/api/sui/SuiSignMessage.ts b/packages/core/src/api/sui/SuiSignMessage.ts index e7550faa9..602acad76 100644 --- a/packages/core/src/api/sui/SuiSignMessage.ts +++ b/packages/core/src/api/sui/SuiSignMessage.ts @@ -30,6 +30,9 @@ export default class SuiSignMessage extends BaseMethod { getVersionRange() { return { + pro2: { + min: '0.0.0', + }, model_mini: { min: '3.4.0', }, diff --git a/packages/core/src/api/sui/SuiSignTransaction.ts b/packages/core/src/api/sui/SuiSignTransaction.ts index bd097feed..b9a9b9d69 100644 --- a/packages/core/src/api/sui/SuiSignTransaction.ts +++ b/packages/core/src/api/sui/SuiSignTransaction.ts @@ -1,20 +1,16 @@ import semver from 'semver'; import { bytesToHex } from '@noble/hashes/utils'; +import { EDeviceType } from '@onekeyfe/hd-shared'; import { UI_REQUEST } from '../../constants/ui-request'; import { validatePath } from '../helpers/pathUtils'; import { BaseMethod } from '../BaseMethod'; import { validateParams } from '../helpers/paramsValidator'; import { formatAnyHex } from '../helpers/hexUtils'; -import { getDeviceFirmwareVersion, getDeviceType } from '../../utils'; import { DeviceModelToTypes } from '../../types'; -import type { - SuiSignTx as HardwareSuiSignTx, - SuiSignedTx, - TypedCall, -} from '@onekeyfe/hd-transport'; -import type { TypedResponseMessage } from '../../device/DeviceCommands'; +import type { SuiSignTx as HardwareSuiSignTx, SuiSignedTx } from '@onekeyfe/hd-transport'; +import type { TypedCall, TypedResponseMessage } from '../../device/DeviceCommands'; type SuiSignTx = Omit & HardwareSuiSignTx; @@ -43,6 +39,9 @@ export default class SuiSignTransaction extends BaseMethod { getVersionRange() { return { + pro2: { + min: '0.0.0', + }, model_mini: { min: '3.0.0', }, @@ -53,8 +52,12 @@ export default class SuiSignTransaction extends BaseMethod { } supportChunkTransfer() { - const deviceType = getDeviceType(this.device.features); - const deviceFirmwareVersion = getDeviceFirmwareVersion(this.device.features).join('.'); + if (this.device.isProtocolV2() || this.device.getCurrentDeviceType() === EDeviceType.Pro2) { + return true; + } + + const deviceType = this.device.getCurrentDeviceType(); + const deviceFirmwareVersion = this.device.getCurrentFirmwareVersionString() ?? '0.0.0'; if (DeviceModelToTypes.model_mini.includes(deviceType)) { if (semver.valid(deviceFirmwareVersion)) { @@ -85,7 +88,7 @@ export default class SuiSignTransaction extends BaseMethod { if (!data_length) { // sign Done - return res.message; + return res.message as SuiSignedTx; } const payload = data.subarray(offset, offset + data_length); @@ -104,7 +107,7 @@ export default class SuiSignTransaction extends BaseMethod { async run() { const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands()); let offset = 0; - let data: Buffer; + let data = Buffer.alloc(0); if (this.supportChunkTransfer()) { offset = this.chunkByteSize; @@ -121,7 +124,6 @@ export default class SuiSignTransaction extends BaseMethod { ...this.params, }); - // @ts-expect-error return this.processTxRequest(typedCall, res, data, offset); } } diff --git a/packages/core/src/api/ton/TonGetAddress.ts b/packages/core/src/api/ton/TonGetAddress.ts index b50ce7bbf..5cc8ebbd7 100644 --- a/packages/core/src/api/ton/TonGetAddress.ts +++ b/packages/core/src/api/ton/TonGetAddress.ts @@ -57,6 +57,9 @@ export default class TonGetAddress extends BaseMethod { getVersionRange() { return { + pro2: { + min: '0.0.0', + }, model_touch: { min: '4.10.0', }, diff --git a/packages/core/src/api/ton/TonSignData.ts b/packages/core/src/api/ton/TonSignData.ts index 54a9e0056..5a44737b6 100644 --- a/packages/core/src/api/ton/TonSignData.ts +++ b/packages/core/src/api/ton/TonSignData.ts @@ -9,9 +9,8 @@ import type { TonSignDataParams } from '../../types/api/tonSignData'; export default class TonSignData extends BaseMethod { init() { - // Keep strict-check off until the firmware release that ships TonSignData - // is decided — we don't yet know the min versions for touch/classic1s. - // Flip back to true and add getVersionRange() once those numbers land. + // Keep strict-check off for touch/classic1s until their firmware release + // versions are decided. Pro2 is explicitly allowed by getVersionRange(). this.strictCheckDeviceSupport = false; this.checkDeviceId = true; this.allowDeviceMode = [...this.allowDeviceMode, UI_REQUEST.NOT_INITIALIZE]; @@ -51,6 +50,10 @@ export default class TonSignData extends BaseMethod { }; } + getVersionRange() { + return {}; + } + async run() { const res = await this.device.commands.typedCall('TonSignData', 'TonSignedData', { ...this.params, diff --git a/packages/core/src/api/ton/TonSignMessage.ts b/packages/core/src/api/ton/TonSignMessage.ts index 0596ee78f..f353ff00f 100644 --- a/packages/core/src/api/ton/TonSignMessage.ts +++ b/packages/core/src/api/ton/TonSignMessage.ts @@ -7,7 +7,6 @@ import { validatePath } from '../helpers/pathUtils'; import { BaseMethod } from '../BaseMethod'; import { validateParams } from '../helpers/paramsValidator'; import { DeviceModelToTypes } from '../../types'; -import { getDeviceFirmwareVersion, getDeviceType, getMethodVersionRange } from '../../utils'; import { formatAnyHex, stripHexStartZeroes } from '../helpers/hexUtils'; import { cutString } from '../helpers/stringUtils'; @@ -86,6 +85,9 @@ export default class TonSignMessage extends BaseMethod { getVersionRange() { return { + pro2: { + min: '0.0.0', + }, model_touch: { min: '4.10.0', }, @@ -107,9 +109,8 @@ export default class TonSignMessage extends BaseMethod { } checkSupportJettonAmountBytes() { - const firmwareVersion = getDeviceFirmwareVersion(this.device.features)?.join('.'); - const versionRange = getMethodVersionRange( - this.device.features, + const firmwareVersion = this.device.getCurrentFirmwareVersionString() ?? '0.0.0'; + const versionRange = this.device.getCurrentMethodVersionRange( type => this.getSupportJettonAmountBytesVersionRange()[type] ); @@ -167,7 +168,7 @@ export default class TonSignMessage extends BaseMethod { data: string ): Promise => { if (!request.init_data_length) { - const deviceType = getDeviceType(this.device.features); + const deviceType = this.device.getCurrentDeviceType(); const hasClassic = DeviceModelToTypes.model_classic1s.includes(deviceType); // use signing_message_repr sign, not exists signning_message, skip validate const hasSigningMessageRepr = request.signning_message == null; diff --git a/packages/core/src/api/ton/TonSignProof.ts b/packages/core/src/api/ton/TonSignProof.ts index c58ee2597..787453045 100644 --- a/packages/core/src/api/ton/TonSignProof.ts +++ b/packages/core/src/api/ton/TonSignProof.ts @@ -44,6 +44,9 @@ export default class TonSignProof extends BaseMethod { getVersionRange() { return { + pro2: { + min: '0.0.0', + }, model_touch: { min: '4.10.0', }, diff --git a/packages/core/src/api/tron/TronSignMessage.ts b/packages/core/src/api/tron/TronSignMessage.ts index fae280d34..760c2f0ad 100644 --- a/packages/core/src/api/tron/TronSignMessage.ts +++ b/packages/core/src/api/tron/TronSignMessage.ts @@ -29,7 +29,7 @@ export default class TronSignMessage extends BaseMethod if (this.payload.messageType === 'V1' || this.payload.messageType == null) { throw createDeviceNotSupportMethodError( 'TronSignMessage', - getFirmwareType(this.device.features) + getFirmwareType(this.device?.features) ); } @@ -45,6 +45,9 @@ export default class TronSignMessage extends BaseMethod getVersionRange() { return { + pro2: { + min: '0.0.0', + }, model_mini: { min: '2.5.0', }, diff --git a/packages/core/src/api/tron/TronSignTransaction.ts b/packages/core/src/api/tron/TronSignTransaction.ts index bcec10b3f..2c0efd15b 100644 --- a/packages/core/src/api/tron/TronSignTransaction.ts +++ b/packages/core/src/api/tron/TronSignTransaction.ts @@ -148,6 +148,9 @@ export default class TronSignTransaction extends BaseMethod { getVersionRange() { return { + pro2: { + min: '0.0.0', + }, model_mini: { min: '2.5.0', }, diff --git a/packages/core/src/api/xrp/XrpSignTransaction.ts b/packages/core/src/api/xrp/XrpSignTransaction.ts index dac073157..4cc458393 100644 --- a/packages/core/src/api/xrp/XrpSignTransaction.ts +++ b/packages/core/src/api/xrp/XrpSignTransaction.ts @@ -30,7 +30,7 @@ export default class XrpGetAddress extends BaseMethod { name: 'payment', type: 'object' }, ]); validateParams(transaction.payment, [ - { name: 'amount', type: 'number', required: true }, + { name: 'amount', type: 'uint', required: true }, { name: 'destination', type: 'string', required: true }, { name: 'destinationTag', type: 'number' }, ]); diff --git a/packages/core/src/constants/index.ts b/packages/core/src/constants/index.ts index 909e66ed4..3f54244d2 100644 --- a/packages/core/src/constants/index.ts +++ b/packages/core/src/constants/index.ts @@ -1,2 +1,8 @@ export { safeThrowError } from './errors'; -export { Messages as PROTO, TonSignDataType } from '@onekeyfe/hd-transport'; +export { + Messages as PROTO, + ResourceType, + TonSignDataType, + TonWalletVersion, +} from '@onekeyfe/hd-transport'; +export type { HDNodeType, Success as DeviceSuccess } from '@onekeyfe/hd-transport'; diff --git a/packages/core/src/core/RequestQueue.ts b/packages/core/src/core/RequestQueue.ts index 40431eaa1..3925a0722 100644 --- a/packages/core/src/core/RequestQueue.ts +++ b/packages/core/src/core/RequestQueue.ts @@ -14,6 +14,8 @@ export type RequestTask = { export default class RequestQueue { private requestQueue = new Map(); + private operationRequestIds = new Map(); + private pendingCallbackTasks = new Map>(); // 生成唯一请求ID @@ -30,11 +32,21 @@ export default class RequestQueue { method.responseID = requestId; } const abortController = new AbortController(); + method.abortSignal = abortController.signal; const task = { id: requestId, method, abortController }; this.requestQueue.set(requestId, task); + const operationId = method.payload?.operationId; + if (typeof operationId === 'string' && operationId) { + this.operationRequestIds.set(operationId, requestId); + } return task; } + public abortOperation(operationId: string) { + const requestId = this.operationRequestIds.get(operationId); + return requestId === undefined ? false : this.abortRequest(requestId); + } + public getTask(requestId: number): RequestTask | undefined { return this.requestQueue.get(requestId); } @@ -105,6 +117,13 @@ export default class RequestQueue { // 删除请求 public releaseTask(requestId: number) { + const operationId = this.requestQueue.get(requestId)?.method.payload?.operationId; + if ( + typeof operationId === 'string' && + this.operationRequestIds.get(operationId) === requestId + ) { + this.operationRequestIds.delete(operationId); + } this.requestQueue.delete(requestId); } diff --git a/packages/core/src/core/index.ts b/packages/core/src/core/index.ts index eddbf4bf7..e369279bb 100644 --- a/packages/core/src/core/index.ts +++ b/packages/core/src/core/index.ts @@ -17,11 +17,8 @@ import { import { LoggerNames, enableLog, - getDeviceBLEFirmwareVersion, - getDeviceFirmwareVersion, - getFirmwareType, getLogger, - getMethodVersionRange, + isMethodVersionRangeUnsupported, setLoggerPostMessage, wait, } from '../utils'; @@ -29,7 +26,6 @@ import { findDefectiveBatchDevice, getDefectiveDeviceInfo, } from '../utils/findDefectiveBatchDevice'; -import { supportNewPassphrase } from '../utils/deviceFeaturesUtils'; import { cleanupSdkInstance, completeRequestContext, @@ -60,17 +56,14 @@ import TransportManager from '../data-manager/TransportManager'; import DeviceConnector from '../device/DeviceConnector'; import RequestQueue from './RequestQueue'; import { getSynchronize } from '../utils/getSynchronize'; +import { runMethodWithUnlockRetry } from '../protocols/protocol-v2/unlockRetry'; -import type { ConnectSettings, KnownDevice } from '../types'; +import type { ConnectSettings, Features, KnownDevice } from '../types'; import type { CoreMessage, IFrameCallMessage, UiPromise, UiPromiseResponse } from '../events'; import type { DeviceEvents, InitOptions, RunOptions } from '../device/Device'; import type { SdkTracingContext } from '../utils/tracing'; import type { Deferred } from '@onekeyfe/hd-shared'; -import type { - Features, - LowlevelTransportSharedPlugin, - OneKeyDeviceInfo, -} from '@onekeyfe/hd-transport'; +import type { LowlevelTransportSharedPlugin, OneKeyDeviceInfo } from '@onekeyfe/hd-transport'; import type { BaseMethod } from '../api/BaseMethod'; const Log = getLogger(LoggerNames.Core); @@ -102,6 +95,8 @@ const parseInitOptions = (method?: BaseMethod): InitOptions => ({ passphraseState: method?.payload.passphraseState, deviceId: method?.payload.deviceId, deriveCardano: method && hasDeriveCardano(method), + connectProtocol: method?.payload.connectProtocol, + protocolV2DeviceInfoTimeoutMs: method?.payload.protocolV2DeviceInfoTimeoutMs, }); let _core: Core; @@ -229,9 +224,15 @@ const handlePreWarmSignal = async ( message: CoreMessage, method: BaseMethod ): Promise => { + const createAckResponse = () => { + completeMethodRequestContext(method); + method.dispose(); + return createResponseMessage(method.responseID, true, true); + }; + // no connectId: can't target a device safely, skip pre-warm (ack only) if (!method.connectId) { - return createResponseMessage(method.responseID, true, true); + return createAckResponse(); } const key = method.getPreWarmKey(); @@ -244,12 +245,12 @@ const handlePreWarmSignal = async ( } catch { // pre-warm is best-effort; ignore its failure for the coalesced caller } - return createResponseMessage(method.responseID, true, true); + return createAckResponse(); } const doneAt = preWarmDoneAt.get(key); if (typeof doneAt === 'number' && Date.now() - doneAt <= method.preWarmTtl) { - return createResponseMessage(method.responseID, true, true); + return createAckResponse(); } const run = onCallDevice(context, message, method); @@ -420,13 +421,24 @@ const onCallDevice = async ( await waitForPendingPromise(getPrePendingCallPromise, setPrePendingCallPromise); const inner = async (): Promise => { + // Protocol V2 专属方法统一守卫:协议在 acquire/initialize 后已确定, + // 非 V2 设备直接抛出明确的 NotSupport 错误(见 BaseMethod.requireProtocolV2)。 + if (method.requireProtocolV2 && !device.isProtocolV2()) { + throw createDeviceNotSupportMethodError(method.name, device.getCurrentFirmwareType()); + } + // check firmware version - const versionRange = getMethodVersionRange( - device.features, + const versionRange = device.getCurrentMethodVersionRange( type => method.getVersionRange()[type] ); - - if (device.features) { + const currentFirmwareVersion = device.getCurrentFirmwareVersionString() ?? '0.0.0'; + const currentBleVersion = device.getCurrentBLEFirmwareVersionString() ?? '0.0.0'; + const deviceFirmwareType = device.getCurrentFirmwareType(); + let newVersionStatus: ReturnType | undefined; + + // 故障批次检测与远端强制升级门禁基于 V1 features/remote config, + // Pro2(V2) 暂不参与:profile 版的 firmwareStatus 需要 DataManager 支持后再接入。 + if (device.features && !device.isProtocolV2()) { await DataManager.checkAndReloadData(); // 检测故障固件设备 @@ -443,12 +455,9 @@ const onCallDevice = async ( } } - const deviceFirmwareType = getFirmwareType(device.features); - const newVersionStatus = DataManager.getFirmwareStatus(device.features, deviceFirmwareType); + newVersionStatus = DataManager.getFirmwareStatus(device.features, deviceFirmwareType); const bleVersionStatus = DataManager.getBLEFirmwareStatus(device.features); - const currentFirmwareVersion = getDeviceFirmwareVersion(device.features).join('.'); - const currentBleVersion = getDeviceBLEFirmwareVersion(device.features).join('.'); if ( (newVersionStatus === 'required' || bleVersionStatus === 'required') && method.skipForceUpdateCheck === false @@ -474,42 +483,43 @@ const onCallDevice = async ( currentVersions ); } + } - if (versionRange) { - if ( - semver.valid(versionRange.min) && - semver.lt(currentFirmwareVersion, versionRange.min) - ) { - if (newVersionStatus === 'none' || newVersionStatus === 'valid') { - throw createNewFirmwareUnReleaseHardwareError({ - currentVersion: currentFirmwareVersion, - requireVersion: versionRange.min, - methodName: method.name, - firmwareType: getFirmwareType(device.features), - }); - } + if (isMethodVersionRangeUnsupported(versionRange)) { + throw createDeviceNotSupportMethodError(method.name, deviceFirmwareType); + } - return Promise.reject( - createNeedUpgradeFirmwareHardwareError({ - currentVersion: currentFirmwareVersion, - requireVersion: versionRange.min, - methodName: method.name, - firmwareType: getFirmwareType(device.features), - }) - ); - } - if ( - versionRange.max && - semver.valid(versionRange.max) && - semver.gte(currentFirmwareVersion, versionRange.max) - ) { - return Promise.reject( - createDeprecatedHardwareError(currentFirmwareVersion, versionRange.max, method.name) - ); + if (versionRange) { + if (semver.valid(versionRange.min) && semver.lt(currentFirmwareVersion, versionRange.min)) { + if (newVersionStatus === 'none' || newVersionStatus === 'valid') { + throw createNewFirmwareUnReleaseHardwareError({ + currentVersion: currentFirmwareVersion, + requireVersion: versionRange.min, + methodName: method.name, + firmwareType: deviceFirmwareType, + }); } - } else if (method.strictCheckDeviceSupport) { - throw createDeviceNotSupportMethodError(method.name, getFirmwareType(device.features)); + + return Promise.reject( + createNeedUpgradeFirmwareHardwareError({ + currentVersion: currentFirmwareVersion, + requireVersion: versionRange.min, + methodName: method.name, + firmwareType: deviceFirmwareType, + }) + ); } + if ( + versionRange.max && + semver.valid(versionRange.max) && + semver.gte(currentFirmwareVersion, versionRange.max) + ) { + return Promise.reject( + createDeprecatedHardwareError(currentFirmwareVersion, versionRange.max, method.name) + ); + } + } else if (method.strictCheckDeviceSupport) { + throw createDeviceNotSupportMethodError(method.name, deviceFirmwareType); } // check call method mode @@ -547,16 +557,16 @@ const onCallDevice = async ( method.checkDeviceSupportFeature(); // reconfigure messages - if (_deviceList) { + if (_deviceList && device.features && !device.isProtocolV2()) { await TransportManager.reconfigure(device.features); } // Check to see if it is safe to use Passphrase checkPassphraseEnableState(method, device.features); - if (device.hasUsePassphrase() && method.useDevicePassphraseState) { + if (shouldCheckPassphraseState(method, device)) { // check version - const support = supportNewPassphrase(device.features); + const support = device.supportNewPassphrase(); if (!support.support) { return Promise.reject( ERRORS.TypedError( @@ -607,7 +617,7 @@ const onCallDevice = async ( method.device?.commands?.checkDisposed(); try { - const response: object = await method.run(); + const response: object = await runMethodWithUnlockRetry(method, device); messageResponse = createResponseMessage(method.responseID, true, response); requestQueue.resolveRequest(method.responseID, messageResponse); completeMethodRequestContext(method); @@ -723,17 +733,27 @@ function initDevice(method: BaseMethod) { let device: Device | typeof undefined; const allDevices = _deviceList.allDevices(); - if (method.payload?.detectBootloaderDevice && allDevices.some(d => d.features?.bootloader_mode)) { + if (method.payload?.detectBootloaderDevice && allDevices.some(d => d.isBootloader())) { throw ERRORS.TypedError(HardwareErrorCode.DeviceDetectInBootloaderMode); } if (method.connectId) { device = _deviceList.getDevice(method.connectId); + if (!device && method.name === 'firmwareUpdateV4' && allDevices.length === 1) { + const [singleDevice] = allDevices; + if (singleDevice.isBootloader()) { + Log.debug( + 'firmwareUpdateV4 uses the only bootloader device when connectId changed after reboot' + ); + device = singleDevice; + } + } } else if (allDevices.length === 1) { [device] = allDevices; } else if (allDevices.length > 1) { throw ERRORS.TypedError( [ + 'firmwareUpdateV4', 'firmwareUpdateV3', 'firmwareUpdateV2', 'checkFirmwareRelease', @@ -796,8 +816,8 @@ function canSkipInitialize(method: BaseMethod, device: Device): boolean { if (!method.connectId) reasons.push('connectId.missing'); // passphrase state must match the pre-initialize if (!device.isPreInitializeMetaMatch(method.payload)) reasons.push('meta.mismatch'); - // device must have been initialized before (has features) - if (!device.features) reasons.push('features.missing'); + // device must have been initialized before. + if (device.isUnacquired()) reasons.push('deviceInfo.missing'); // within pre-initialize TTL if (!device.isPreInitializedValid(PRE_INITIALIZE_TTL_MS)) reasons.push('ttl.expired'); @@ -813,13 +833,23 @@ function canSkipInitialize(method: BaseMethod, device: Device): boolean { return true; } +function isRetryableBleProtocolV2ProbeError(method: BaseMethod, error: unknown) { + const message = error instanceof Error ? error.message : String(error ?? ''); + return ( + method.payload.connectProtocol === 'V2' && + message.includes('Device protocol mismatch') && + message.includes('expected V2') && + message.includes('did not respond to expected protocol') + ); +} + /** * If the Bluetooth connection times out, retry up to 6 times * @param retryCount - Current retry count (default 0) */ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCount = 0) { try { - await device.acquire(); + await device.acquire(method.payload.connectProtocol); if (method.payload?.onlyConnectBleDevice) { return; } @@ -832,7 +862,12 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun }); } } catch (err) { - if (err.errorCode === HardwareErrorCode.BleTimeoutError && retryCount < 6) { + if ( + (err.errorCode === HardwareErrorCode.BleTimeoutError || + err.errorCode === HardwareErrorCode.BleConnectedError || + isRetryableBleProtocolV2ProbeError(method, err)) && + retryCount < 6 + ) { const nextRetry = retryCount + 1; Log.debug(`Bluetooth connect timeout and will retry, retry count: ${nextRetry}`); await wait(3000); @@ -1091,7 +1126,11 @@ export const cancel = (context: CoreContext, connectId?: string) => { const checkPassphraseEnableState = (method: BaseMethod, features?: Features) => { if (!method.useDevicePassphraseState) return; - if (features?.passphrase_protection === true) { + const passphraseProtection = method.device + ? method.device.getCurrentPassphraseProtection() + : features?.passphraseProtection; + + if (passphraseProtection === true) { const hasNoPassphraseState = method.payload.passphraseState == null || method.payload.passphraseState === ''; const shouldRequirePassphrase = @@ -1103,12 +1142,18 @@ const checkPassphraseEnableState = (method: BaseMethod, features?: Features) => } } - if (features?.passphrase_protection === false && method.payload.passphraseState) { + if (passphraseProtection === false && method.payload.passphraseState) { DevicePool.clearDeviceCache(method.payload.connectId); throw ERRORS.TypedError(HardwareErrorCode.DeviceNotOpenedPassphrase); } }; +const shouldCheckPassphraseState = (method: BaseMethod, device: Device) => { + if (!method.useDevicePassphraseState) return false; + + return device.hasUsePassphrase(); +}; + const cleanup = () => { _uiPromises = []; Log.debug('Cleanup...'); @@ -1297,6 +1342,10 @@ export default class Core extends EventEmitter { Log.debug(`[Core] Created SDK instance: ${this.sdkInstanceId}`); } + cancelOperation(operationId: string) { + this.requestQueue.abortOperation(operationId); + } + private getCoreContext() { return { sdkInstanceId: this.sdkInstanceId, @@ -1365,6 +1414,10 @@ export default class Core extends EventEmitter { cancel(this.getCoreContext(), message.payload.connectId); break; } + case IFRAME.CANCEL_OPERATION: { + this.cancelOperation(message.payload.operationId); + break; + } case IFRAME.CALLBACK: { Log.log('callback message: ', message); postMessage(message); diff --git a/packages/core/src/data-manager/DataManager.ts b/packages/core/src/data-manager/DataManager.ts index 534cf1c2c..f6ef1f1e4 100644 --- a/packages/core/src/data-manager/DataManager.ts +++ b/packages/core/src/data-manager/DataManager.ts @@ -4,6 +4,7 @@ import { EDeviceType, EFirmwareType } from '@onekeyfe/hd-shared'; import MessagesJSON from '../data/messages/messages.json'; import MessagesLegacyV1JSON from '../data/messages/messages_legacy_v1.json'; +import MessagesProtocolV2JSON from '../data/messages/messages-protocol-v2.json'; import { LoggerNames, getDeviceBLEFirmwareVersion, @@ -33,6 +34,7 @@ const Log = getLogger(LoggerNames.Core); export const FIRMWARE_FIELDS = [ 'firmware', + 'firmware-v1', 'firmware-v2', 'firmware-v8', 'firmware-btc-v8', @@ -40,10 +42,12 @@ export const FIRMWARE_FIELDS = [ export type IFirmwareField = (typeof FIRMWARE_FIELDS)[number]; -export type MessageVersion = 'latest' | 'v1'; +export type ProtocolV1MessageSchema = 'v1CurrentSchema' | 'v1LegacySchema'; +export type ProtobufMessageSchema = ProtocolV1MessageSchema | 'v2Schema'; const FIRMWARE_FIELD_TYPE_MAP: Readonly> = { firmware: EFirmwareType.Universal, + 'firmware-v1': EFirmwareType.Universal, 'firmware-v2': EFirmwareType.Universal, 'firmware-v8': EFirmwareType.Universal, 'firmware-btc-v8': EFirmwareType.BitcoinOnly, @@ -62,7 +66,9 @@ function getFirmwareTypeFromField(firmwareField: IFirmwareField): EFirmwareType } export default class DataManager { - static deviceMap: DeviceTypeMap = { + static deviceMap: DeviceTypeMap & { + [k: string]: DeviceTypeMap[keyof DeviceTypeMap] | undefined; + } = { [EDeviceType.Classic]: { firmware: [], ble: [], @@ -83,6 +89,10 @@ export default class DataManager { firmware: [], ble: [], }, + [EDeviceType.Pro2]: { + firmware: [], + ble: [], + }, [EDeviceType.ClassicPure]: { firmware: [], ble: [], @@ -93,9 +103,10 @@ export default class DataManager { static settings: ConnectSettings; - static messages: { [version in MessageVersion]: JSON } = { - latest: MessagesJSON as unknown as JSON, - v1: MessagesLegacyV1JSON as unknown as JSON, + static messages: { [schema in ProtobufMessageSchema]: JSON } = { + v1CurrentSchema: MessagesJSON as unknown as JSON, + v1LegacySchema: MessagesLegacyV1JSON as unknown as JSON, + v2Schema: MessagesProtocolV2JSON as unknown as JSON, }; static lastCheckTimestamp = 0; @@ -109,11 +120,11 @@ export default class DataManager { const deviceFirmwareType = getFirmwareType(features); const deviceFirmwareVersion = getDeviceFirmwareVersion(features); - if (features.firmware_present === false) { + if (features.firmwarePresent === false) { return 'none'; } - if (DeviceModelToTypes.model_mini.includes(deviceType) && features.bootloader_mode) { + if (DeviceModelToTypes.model_mini.includes(deviceType) && features.bootloaderMode) { return 'unknown'; } @@ -259,8 +270,8 @@ export default class DataManager { const targetDeviceConfigList = this.deviceMap[deviceType]?.[firmwareUpdateField] ?? []; if ( - features.firmware_present === false || - (DeviceModelToTypes.model_classic.includes(deviceType) && features.bootloader_mode) + features.firmwarePresent === false || + (DeviceModelToTypes.model_classic.includes(deviceType) && features.bootloaderMode) ) { // Always return least changelog return getReleaseChangelog(targetDeviceConfigList, '0.0.0'); @@ -442,6 +453,7 @@ export default class DataManager { [EDeviceType.Mini]: this.enrichFirmwareReleaseInfo(data.mini), [EDeviceType.Touch]: this.enrichFirmwareReleaseInfo(data.touch), [EDeviceType.Pro]: this.enrichFirmwareReleaseInfo(data.pro), + [EDeviceType.Pro2]: this.enrichFirmwareReleaseInfo(data.pro2), }; this.assets = { bridge: data.bridge, @@ -472,8 +484,8 @@ export default class DataManager { } } - static getProtobufMessages(messageVersion: MessageVersion = 'latest'): JSON { - return this.messages[messageVersion]; + static getProtobufMessages(schema: ProtobufMessageSchema = 'v1CurrentSchema'): JSON { + return this.messages[schema]; } static getSettings(key?: undefined): ConnectSettings; diff --git a/packages/core/src/data-manager/MessagesConfig.ts b/packages/core/src/data-manager/MessagesConfig.ts index daefeaa68..b56937009 100644 --- a/packages/core/src/data-manager/MessagesConfig.ts +++ b/packages/core/src/data-manager/MessagesConfig.ts @@ -1,28 +1,28 @@ import type { IDeviceModel, IDeviceType } from '../types'; -import type { MessageVersion } from './DataManager'; +import type { ProtocolV1MessageSchema } from './DataManager'; type DeviceVersionConfig = { [deviceType in IDeviceType | IDeviceModel]?: { minVersion: string; - messageVersion: MessageVersion; + protocolV1MessageSchema: ProtocolV1MessageSchema; }[]; }; export const PROTOBUF_MESSAGE_CONFIG: DeviceVersionConfig = { model_mini: [ - // Classic1s starts from 3.5.0, so use latest by default - // Only use v1 for specific old versions (< 3.3.0) - { minVersion: '3.3.0', messageVersion: 'latest' }, - { minVersion: '0.0.1', messageVersion: 'v1' }, - // Fallback to latest for unknown versions (0.0.0) to prevent device type detection issues - { minVersion: '0.0.0', messageVersion: 'latest' }, + // Classic1s starts from 3.5.0, so use the current Protocol V1 schema by default. + // Only use the legacy Protocol V1 schema for specific old versions (< 3.3.0). + { minVersion: '3.3.0', protocolV1MessageSchema: 'v1CurrentSchema' }, + { minVersion: '0.0.1', protocolV1MessageSchema: 'v1LegacySchema' }, + // Fallback to current Protocol V1 schema for unknown versions (0.0.0). + { minVersion: '0.0.0', protocolV1MessageSchema: 'v1CurrentSchema' }, ], model_touch: [ - // Use latest by default for Touch/Pro - // Only use v1 for specific old versions (< 4.5.0) - { minVersion: '4.5.0', messageVersion: 'latest' }, - { minVersion: '0.0.1', messageVersion: 'v1' }, - // Fallback to latest for unknown versions (0.0.0) - { minVersion: '0.0.0', messageVersion: 'latest' }, + // Use the current Protocol V1 schema by default for Touch/Pro. + // Only use the legacy Protocol V1 schema for specific old versions (< 4.5.0). + { minVersion: '4.5.0', protocolV1MessageSchema: 'v1CurrentSchema' }, + { minVersion: '0.0.1', protocolV1MessageSchema: 'v1LegacySchema' }, + // Fallback to current Protocol V1 schema for unknown versions (0.0.0). + { minVersion: '0.0.0', protocolV1MessageSchema: 'v1CurrentSchema' }, ], }; diff --git a/packages/core/src/data-manager/TransportManager.ts b/packages/core/src/data-manager/TransportManager.ts index 7514fa66a..8489b57b1 100644 --- a/packages/core/src/data-manager/TransportManager.ts +++ b/packages/core/src/data-manager/TransportManager.ts @@ -4,9 +4,9 @@ import DataManager from './DataManager'; import { LoggerNames, getLogger } from '../utils'; // eslint-disable-next-line import/no-cycle import { DevicePool } from '../device/DevicePool'; -import { getSupportMessageVersion } from '../utils/deviceFeaturesUtils'; +import { getSupportProtocolV1MessageSchema } from '../utils/deviceFeaturesUtils'; -import type { MessageVersion } from './DataManager'; +import type { ProtocolV1MessageSchema } from './DataManager'; import type { LowlevelTransportSharedPlugin, Transport } from '@onekeyfe/hd-transport'; import type { Features } from '../types'; @@ -17,6 +17,7 @@ const LowLevelLogger = getLogger(LoggerNames.HdTransportLowLevel); const NodeUsbLogger = getLogger(LoggerNames.HdTransportNodeUsb); const WebBleLogger = getLogger(LoggerNames.HdWebBleTransport); const WebUsbLogger = getLogger(LoggerNames.HdTransportWebUsb); +const REACT_NATIVE_BLE_SCAN_TIMEOUT_MS = 8000; /** * transport 在同一个环境中只会存在一个 @@ -32,7 +33,7 @@ export default class TransportManager { static reactNativeInit = false; - static messageVersion: MessageVersion = 'latest'; + static protocolV1MessageSchema: ProtocolV1MessageSchema = 'v1CurrentSchema'; static plugin: LowlevelTransportSharedPlugin | null = null; @@ -40,7 +41,7 @@ export default class TransportManager { Log.debug('transport manager load'); this.defaultMessages = DataManager.getProtobufMessages(); this.currentMessages = this.defaultMessages; - this.messageVersion = 'latest'; + this.protocolV1MessageSchema = 'v1CurrentSchema'; } static async configure() { @@ -74,6 +75,9 @@ export default class TransportManager { } Log.debug('Configuring transports'); await this.transport.configure(JSON.stringify(this.defaultMessages)); + this.currentMessages = this.defaultMessages; + this.protocolV1MessageSchema = 'v1CurrentSchema'; + await this.configureProtocolV2Messages(); Log.debug('Configuring transports done'); } catch (error) { Log.debug('Initializing transports error: ', error); @@ -83,20 +87,31 @@ export default class TransportManager { } } - static async reconfigure(features?: Features | undefined) { - Log.debug(`Begin reconfiguring transports`); - const { messageVersion, messages } = getSupportMessageVersion(features); + /** + * Re-load the transport's main protobuf schema based on a device's reported features. + * + * This handles protobuf schema compatibility within Protocol V1 (e.g. Touch's legacy + * vs current Protocol V1 schema). It is NOT used to switch between Protocol V1 and Protocol V2 — + * the transport already holds both schemas after initial configure(), and routes per + * device by `getProtocolType()`. + */ + static async reconfigure(features?: Features) { + if (!features) { + return; + } + + const { protocolV1MessageSchema, messages } = getSupportProtocolV1MessageSchema(features); if (this.currentMessages === messages || !messages) { return; } - Log.debug(`Reconfiguring transports version:${messageVersion}`); + Log.debug(`Reconfiguring transports Protocol V1 schema:${protocolV1MessageSchema}`); try { await this.transport.configure(JSON.stringify(messages)); this.currentMessages = messages; - this.messageVersion = messageVersion; + this.protocolV1MessageSchema = protocolV1MessageSchema; } catch (error) { throw ERRORS.TypedError( HardwareErrorCode.TransportInvalidProtobuf, @@ -109,7 +124,9 @@ export default class TransportManager { const env = DataManager.getSettings('env'); if (env === 'react-native') { /** Actually initializes the ReactNativeTransport */ - this.transport = new TransportConstructor({ scanTimeout: 3000 }) as unknown as Transport; + this.transport = new TransportConstructor({ + scanTimeout: REACT_NATIVE_BLE_SCAN_TIMEOUT_MS, + }) as unknown as Transport; } else { /** Actually initializes the HttpTransport */ this.transport = new TransportConstructor() as unknown as Transport; @@ -130,6 +147,15 @@ export default class TransportManager { return this.transport; } + private static async configureProtocolV2Messages() { + const protocolV2Messages = DataManager.getProtobufMessages('v2Schema'); + const { configureProtocolV2 } = this.transport; + if (protocolV2Messages && typeof configureProtocolV2 === 'function') { + await configureProtocolV2.call(this.transport, JSON.stringify(protocolV2Messages)); + Log.debug('Protocol V2 messages configured'); + } + } + static getDefaultMessages() { return this.defaultMessages; } @@ -138,7 +164,7 @@ export default class TransportManager { return this.currentMessages; } - static getMessageVersion() { - return this.messageVersion; + static getProtocolV1MessageSchema() { + return this.protocolV1MessageSchema; } } diff --git a/packages/core/src/data-manager/connectSettings.ts b/packages/core/src/data-manager/connectSettings.ts index c8be0d0ac..adb6f3616 100644 --- a/packages/core/src/data-manager/connectSettings.ts +++ b/packages/core/src/data-manager/connectSettings.ts @@ -115,6 +115,10 @@ export const parseConnectSettings = (input: Partial = {}) => { settings.fetchConfig = input.fetchConfig; } + if (input.configFetcher) { + settings.configFetcher = input.configFetcher; + } + return settings; }; diff --git a/packages/core/src/data/messages/messages-protocol-v2.json b/packages/core/src/data/messages/messages-protocol-v2.json new file mode 100644 index 000000000..6b417ad6e --- /dev/null +++ b/packages/core/src/data/messages/messages-protocol-v2.json @@ -0,0 +1,13431 @@ +{ + "nested": { + "wire_in": { + "type": "bool", + "id": 50002, + "extend": "google.protobuf.EnumValueOptions" + }, + "wire_out": { + "type": "bool", + "id": 50003, + "extend": "google.protobuf.EnumValueOptions" + }, + "wire_debug_in": { + "type": "bool", + "id": 50004, + "extend": "google.protobuf.EnumValueOptions" + }, + "wire_debug_out": { + "type": "bool", + "id": 50005, + "extend": "google.protobuf.EnumValueOptions" + }, + "wire_tiny": { + "type": "bool", + "id": 50006, + "extend": "google.protobuf.EnumValueOptions" + }, + "wire_bootloader": { + "type": "bool", + "id": 50007, + "extend": "google.protobuf.EnumValueOptions" + }, + "wire_no_fsm": { + "type": "bool", + "id": 50008, + "extend": "google.protobuf.EnumValueOptions" + }, + "bitcoin_only": { + "type": "bool", + "id": 60000, + "extend": "google.protobuf.EnumValueOptions" + }, + "has_bitcoin_only_values": { + "type": "bool", + "id": 51001, + "extend": "google.protobuf.EnumOptions" + }, + "experimental_message": { + "type": "bool", + "id": 52001, + "extend": "google.protobuf.MessageOptions" + }, + "wire_type": { + "type": "uint32", + "id": 52002, + "extend": "google.protobuf.MessageOptions" + }, + "experimental_field": { + "type": "bool", + "id": 53001, + "extend": "google.protobuf.FieldOptions" + }, + "include_in_bitcoin_only": { + "type": "bool", + "id": 60000, + "extend": "google.protobuf.FileOptions" + }, + "MessageType": { + "options": { + "(has_bitcoin_only_values)": true + }, + "values": { + "MessageType_Initialize": 0, + "MessageType_ChangePin": 4, + "MessageType_WipeDevice": 5, + "MessageType_GetEntropy": 9, + "MessageType_Entropy": 10, + "MessageType_LoadDevice": 13, + "MessageType_ResetDevice": 14, + "MessageType_SetBusy": 16, + "MessageType_Features": 17, + "MessageType_PinMatrixRequest": 18, + "MessageType_PinMatrixAck": 19, + "MessageType_Cancel": 20, + "MessageType_LockDevice": 24, + "MessageType_ApplySettings": 25, + "MessageType_ButtonRequest": 26, + "MessageType_ButtonAck": 27, + "MessageType_ApplyFlags": 28, + "MessageType_GetNonce": 31, + "MessageType_Nonce": 33, + "MessageType_BackupDevice": 34, + "MessageType_EntropyRequest": 35, + "MessageType_EntropyAck": 36, + "MessageType_PassphraseRequest": 41, + "MessageType_PassphraseAck": 42, + "MessageType_RecoveryDevice": 45, + "MessageType_WordRequest": 46, + "MessageType_WordAck": 47, + "MessageType_GetFeatures": 55, + "MessageType_SdProtect": 79, + "MessageType_ChangeWipeCode": 82, + "MessageType_DoPreauthorized": 84, + "MessageType_PreauthorizedRequest": 85, + "MessageType_CancelAuthorization": 86, + "MessageType_GetFirmwareHash": 88, + "MessageType_FirmwareHash": 89, + "MessageType_UnlockPath": 93, + "MessageType_UnlockedPathRequest": 94, + "MessageType_SetU2FCounter": 63, + "MessageType_GetNextU2FCounter": 80, + "MessageType_NextU2FCounter": 81, + "MessageType_Deprecated_PassphraseStateRequest": 77, + "MessageType_Deprecated_PassphraseStateAck": 78, + "MessageType_GetPublicKey": 11, + "MessageType_PublicKey": 12, + "MessageType_SignTx": 15, + "MessageType_TxRequest": 21, + "MessageType_TxAck": 22, + "MessageType_GetAddress": 29, + "MessageType_Address": 30, + "MessageType_TxAckPaymentRequest": 37, + "MessageType_SignMessage": 38, + "MessageType_VerifyMessage": 39, + "MessageType_MessageSignature": 40, + "MessageType_GetOwnershipId": 43, + "MessageType_OwnershipId": 44, + "MessageType_GetOwnershipProof": 49, + "MessageType_OwnershipProof": 50, + "MessageType_AuthorizeCoinJoin": 51, + "MessageType_SignPsbt": 10052, + "MessageType_SignedPsbt": 10053, + "MessageType_CipherKeyValue": 23, + "MessageType_CipheredKeyValue": 48, + "MessageType_SignIdentity": 53, + "MessageType_SignedIdentity": 54, + "MessageType_GetECDHSessionKey": 61, + "MessageType_ECDHSessionKey": 62, + "MessageType_CosiCommit": 71, + "MessageType_CosiCommitment": 72, + "MessageType_CosiSign": 73, + "MessageType_CosiSignature": 74, + "MessageType_BatchGetPublickeys": 10016, + "MessageType_EcdsaPublicKeys": 10017, + "MessageType_DebugLinkDecision": 100, + "MessageType_DebugLinkGetState": 101, + "MessageType_DebugLinkState": 102, + "MessageType_DebugLinkStop": 103, + "MessageType_DebugLinkLog": 104, + "MessageType_DebugLinkMemoryRead": 110, + "MessageType_DebugLinkMemory": 111, + "MessageType_DebugLinkMemoryWrite": 112, + "MessageType_DebugLinkFlashErase": 113, + "MessageType_DebugLinkLayout": 9001, + "MessageType_DebugLinkReseedRandom": 9002, + "MessageType_DebugLinkRecordScreen": 9003, + "MessageType_DebugLinkEraseSdCard": 9005, + "MessageType_DebugLinkWatchLayout": 9006, + "MessageType_EthereumGetPublicKey": 450, + "MessageType_EthereumPublicKey": 451, + "MessageType_EthereumGetAddress": 56, + "MessageType_EthereumAddress": 57, + "MessageType_EthereumSignTx": 58, + "MessageType_EthereumSignTxEIP1559": 452, + "MessageType_EthereumTxRequest": 59, + "MessageType_EthereumTxAck": 60, + "MessageType_EthereumSignMessage": 64, + "MessageType_EthereumVerifyMessage": 65, + "MessageType_EthereumMessageSignature": 66, + "MessageType_EthereumSignTypedData": 464, + "MessageType_EthereumTypedDataStructRequest": 465, + "MessageType_EthereumTypedDataStructAck": 466, + "MessageType_EthereumTypedDataValueRequest": 467, + "MessageType_EthereumTypedDataValueAck": 468, + "MessageType_EthereumTypedDataSignature": 469, + "MessageType_EthereumSignTypedHash": 470, + "MessageType_EthereumGetPublicKeyOneKey": 20100, + "MessageType_EthereumPublicKeyOneKey": 20101, + "MessageType_EthereumGetAddressOneKey": 20102, + "MessageType_EthereumAddressOneKey": 20103, + "MessageType_EthereumSignTxOneKey": 20104, + "MessageType_EthereumSignTxEIP1559OneKey": 20105, + "MessageType_EthereumTxRequestOneKey": 20106, + "MessageType_EthereumTxAckOneKey": 20107, + "MessageType_EthereumSignMessageOneKey": 20108, + "MessageType_EthereumVerifyMessageOneKey": 20109, + "MessageType_EthereumMessageSignatureOneKey": 20110, + "MessageType_EthereumSignTypedDataOneKey": 20111, + "MessageType_EthereumTypedDataStructRequestOneKey": 20112, + "MessageType_EthereumTypedDataStructAckOneKey": 20113, + "MessageType_EthereumTypedDataValueRequestOneKey": 20114, + "MessageType_EthereumTypedDataValueAckOneKey": 20115, + "MessageType_EthereumTypedDataSignatureOneKey": 20116, + "MessageType_EthereumSignTypedHashOneKey": 20117, + "MessageType_EthereumGnosisSafeTxAck": 20118, + "MessageType_EthereumGnosisSafeTxRequest": 20119, + "MessageType_EthereumSignTxEIP7702OneKey": 20120, + "MessageType_EthereumSignTypedDataQR": 20121, + "MessageType_NEMGetAddress": 67, + "MessageType_NEMAddress": 68, + "MessageType_NEMSignTx": 69, + "MessageType_NEMSignedTx": 70, + "MessageType_NEMDecryptMessage": 75, + "MessageType_NEMDecryptedMessage": 76, + "MessageType_TezosGetAddress": 150, + "MessageType_TezosAddress": 151, + "MessageType_TezosSignTx": 152, + "MessageType_TezosSignedTx": 153, + "MessageType_TezosGetPublicKey": 154, + "MessageType_TezosPublicKey": 155, + "MessageType_StellarSignTx": 202, + "MessageType_StellarTxOpRequest": 203, + "MessageType_StellarGetAddress": 207, + "MessageType_StellarAddress": 208, + "MessageType_StellarCreateAccountOp": 210, + "MessageType_StellarPaymentOp": 211, + "MessageType_StellarPathPaymentStrictReceiveOp": 212, + "MessageType_StellarManageSellOfferOp": 213, + "MessageType_StellarCreatePassiveSellOfferOp": 214, + "MessageType_StellarSetOptionsOp": 215, + "MessageType_StellarChangeTrustOp": 216, + "MessageType_StellarAllowTrustOp": 217, + "MessageType_StellarAccountMergeOp": 218, + "MessageType_StellarManageDataOp": 220, + "MessageType_StellarBumpSequenceOp": 221, + "MessageType_StellarManageBuyOfferOp": 222, + "MessageType_StellarPathPaymentStrictSendOp": 223, + "MessageType_StellarSignedTx": 230, + "MessageType_CardanoGetPublicKey": 305, + "MessageType_CardanoPublicKey": 306, + "MessageType_CardanoGetAddress": 307, + "MessageType_CardanoAddress": 308, + "MessageType_CardanoTxItemAck": 313, + "MessageType_CardanoTxAuxiliaryDataSupplement": 314, + "MessageType_CardanoTxWitnessRequest": 315, + "MessageType_CardanoTxWitnessResponse": 316, + "MessageType_CardanoTxHostAck": 317, + "MessageType_CardanoTxBodyHash": 318, + "MessageType_CardanoSignTxFinished": 319, + "MessageType_CardanoSignTxInit": 320, + "MessageType_CardanoTxInput": 321, + "MessageType_CardanoTxOutput": 322, + "MessageType_CardanoAssetGroup": 323, + "MessageType_CardanoToken": 324, + "MessageType_CardanoTxCertificate": 325, + "MessageType_CardanoTxWithdrawal": 326, + "MessageType_CardanoTxAuxiliaryData": 327, + "MessageType_CardanoPoolOwner": 328, + "MessageType_CardanoPoolRelayParameters": 329, + "MessageType_CardanoGetNativeScriptHash": 330, + "MessageType_CardanoNativeScriptHash": 331, + "MessageType_CardanoTxMint": 332, + "MessageType_CardanoTxCollateralInput": 333, + "MessageType_CardanoTxRequiredSigner": 334, + "MessageType_CardanoTxInlineDatumChunk": 335, + "MessageType_CardanoTxReferenceScriptChunk": 336, + "MessageType_CardanoTxReferenceInput": 337, + "MessageType_CardanoSignMessage": 350, + "MessageType_CardanoMessageSignature": 351, + "MessageType_RippleGetAddress": 400, + "MessageType_RippleAddress": 401, + "MessageType_RippleSignTx": 402, + "MessageType_RippleSignedTx": 403, + "MessageType_MoneroTransactionInitRequest": 501, + "MessageType_MoneroTransactionInitAck": 502, + "MessageType_MoneroTransactionSetInputRequest": 503, + "MessageType_MoneroTransactionSetInputAck": 504, + "MessageType_MoneroTransactionInputViniRequest": 507, + "MessageType_MoneroTransactionInputViniAck": 508, + "MessageType_MoneroTransactionAllInputsSetRequest": 509, + "MessageType_MoneroTransactionAllInputsSetAck": 510, + "MessageType_MoneroTransactionSetOutputRequest": 511, + "MessageType_MoneroTransactionSetOutputAck": 512, + "MessageType_MoneroTransactionAllOutSetRequest": 513, + "MessageType_MoneroTransactionAllOutSetAck": 514, + "MessageType_MoneroTransactionSignInputRequest": 515, + "MessageType_MoneroTransactionSignInputAck": 516, + "MessageType_MoneroTransactionFinalRequest": 517, + "MessageType_MoneroTransactionFinalAck": 518, + "MessageType_MoneroKeyImageExportInitRequest": 530, + "MessageType_MoneroKeyImageExportInitAck": 531, + "MessageType_MoneroKeyImageSyncStepRequest": 532, + "MessageType_MoneroKeyImageSyncStepAck": 533, + "MessageType_MoneroKeyImageSyncFinalRequest": 534, + "MessageType_MoneroKeyImageSyncFinalAck": 535, + "MessageType_MoneroGetAddress": 540, + "MessageType_MoneroAddress": 541, + "MessageType_MoneroGetWatchKey": 542, + "MessageType_MoneroWatchKey": 543, + "MessageType_DebugMoneroDiagRequest": 546, + "MessageType_DebugMoneroDiagAck": 547, + "MessageType_MoneroGetTxKeyRequest": 550, + "MessageType_MoneroGetTxKeyAck": 551, + "MessageType_MoneroLiveRefreshStartRequest": 552, + "MessageType_MoneroLiveRefreshStartAck": 553, + "MessageType_MoneroLiveRefreshStepRequest": 554, + "MessageType_MoneroLiveRefreshStepAck": 555, + "MessageType_MoneroLiveRefreshFinalRequest": 556, + "MessageType_MoneroLiveRefreshFinalAck": 557, + "MessageType_EosGetPublicKey": 600, + "MessageType_EosPublicKey": 601, + "MessageType_EosSignTx": 602, + "MessageType_EosTxActionRequest": 603, + "MessageType_EosTxActionAck": 604, + "MessageType_EosSignedTx": 605, + "MessageType_BinanceGetAddress": 700, + "MessageType_BinanceAddress": 701, + "MessageType_BinanceGetPublicKey": 702, + "MessageType_BinancePublicKey": 703, + "MessageType_BinanceSignTx": 704, + "MessageType_BinanceTxRequest": 705, + "MessageType_BinanceTransferMsg": 706, + "MessageType_BinanceOrderMsg": 707, + "MessageType_BinanceCancelMsg": 708, + "MessageType_BinanceSignedTx": 709, + "MessageType_StarcoinGetAddress": 10300, + "MessageType_StarcoinAddress": 10301, + "MessageType_StarcoinGetPublicKey": 10302, + "MessageType_StarcoinPublicKey": 10303, + "MessageType_StarcoinSignTx": 10304, + "MessageType_StarcoinSignedTx": 10305, + "MessageType_StarcoinSignMessage": 10306, + "MessageType_StarcoinMessageSignature": 10307, + "MessageType_StarcoinVerifyMessage": 10308, + "MessageType_ConfluxGetAddress": 10112, + "MessageType_ConfluxAddress": 10113, + "MessageType_ConfluxSignTx": 10114, + "MessageType_ConfluxTxRequest": 10115, + "MessageType_ConfluxTxAck": 10116, + "MessageType_ConfluxSignMessage": 10117, + "MessageType_ConfluxSignMessageCIP23": 10118, + "MessageType_ConfluxMessageSignature": 10119, + "MessageType_TronGetAddress": 10501, + "MessageType_TronAddress": 10502, + "MessageType_TronSignTx": 10503, + "MessageType_TronSignedTx": 10504, + "MessageType_TronSignMessage": 10505, + "MessageType_TronMessageSignature": 10506, + "MessageType_NearGetAddress": 10701, + "MessageType_NearAddress": 10702, + "MessageType_NearSignTx": 10703, + "MessageType_NearSignedTx": 10704, + "MessageType_AptosGetAddress": 10600, + "MessageType_AptosAddress": 10601, + "MessageType_AptosSignTx": 10602, + "MessageType_AptosSignedTx": 10603, + "MessageType_AptosSignMessage": 10604, + "MessageType_AptosMessageSignature": 10605, + "MessageType_AptosSignSIWAMessage": 10606, + "MessageType_WebAuthnListResidentCredentials": 800, + "MessageType_WebAuthnCredentials": 801, + "MessageType_WebAuthnAddResidentCredential": 802, + "MessageType_WebAuthnRemoveResidentCredential": 803, + "MessageType_SolanaGetAddress": 10100, + "MessageType_SolanaAddress": 10101, + "MessageType_SolanaSignTx": 10102, + "MessageType_SolanaSignedTx": 10103, + "MessageType_SolanaSignOffChainMessage": 10104, + "MessageType_SolanaMessageSignature": 10105, + "MessageType_SolanaSignUnsafeMessage": 10106, + "MessageType_CosmosGetAddress": 10800, + "MessageType_CosmosAddress": 10801, + "MessageType_CosmosSignTx": 10802, + "MessageType_CosmosSignedTx": 10803, + "MessageType_AlgorandGetAddress": 10900, + "MessageType_AlgorandAddress": 10901, + "MessageType_AlgorandSignTx": 10902, + "MessageType_AlgorandSignedTx": 10903, + "MessageType_PolkadotGetAddress": 11000, + "MessageType_PolkadotAddress": 11001, + "MessageType_PolkadotSignTx": 11002, + "MessageType_PolkadotSignedTx": 11003, + "MessageType_SuiGetAddress": 11100, + "MessageType_SuiAddress": 11101, + "MessageType_SuiSignTx": 11102, + "MessageType_SuiSignedTx": 11103, + "MessageType_SuiSignMessage": 11104, + "MessageType_SuiMessageSignature": 11105, + "MessageType_SuiTxRequest": 11106, + "MessageType_SuiTxAck": 11107, + "MessageType_FilecoinGetAddress": 11200, + "MessageType_FilecoinAddress": 11201, + "MessageType_FilecoinSignTx": 11202, + "MessageType_FilecoinSignedTx": 11203, + "MessageType_KaspaGetAddress": 11300, + "MessageType_KaspaAddress": 11301, + "MessageType_KaspaSignTx": 11302, + "MessageType_KaspaSignedTx": 11303, + "MessageType_KaspaTxInputRequest": 11304, + "MessageType_KaspaTxInputAck": 11305, + "MessageType_NexaGetAddress": 11400, + "MessageType_NexaAddress": 11401, + "MessageType_NexaSignTx": 11402, + "MessageType_NexaSignedTx": 11403, + "MessageType_NexaTxInputRequest": 11404, + "MessageType_NexaTxInputAck": 11405, + "MessageType_NostrGetPublicKey": 11500, + "MessageType_NostrPublicKey": 11501, + "MessageType_NostrSignEvent": 11502, + "MessageType_NostrSignedEvent": 11503, + "MessageType_NostrEncryptMessage": 11504, + "MessageType_NostrEncryptedMessage": 11505, + "MessageType_NostrDecryptMessage": 11506, + "MessageType_NostrDecryptedMessage": 11507, + "MessageType_NostrSignSchnorr": 11508, + "MessageType_NostrSignedSchnorr": 11509, + "MessageType_LnurlAuth": 11600, + "MessageType_LnurlAuthResp": 11601, + "MessageType_NervosGetAddress": 11701, + "MessageType_NervosAddress": 11702, + "MessageType_NervosSignTx": 11703, + "MessageType_NervosSignedTx": 11704, + "MessageType_NervosTxRequest": 11705, + "MessageType_NervosTxAck": 11706, + "MessageType_TonGetAddress": 11901, + "MessageType_TonAddress": 11902, + "MessageType_TonSignMessage": 11903, + "MessageType_TonSignedMessage": 11904, + "MessageType_TonSignProof": 11905, + "MessageType_TonSignedProof": 11906, + "MessageType_TonTxAck": 11907, + "MessageType_ScdoGetAddress": 12001, + "MessageType_ScdoAddress": 12002, + "MessageType_ScdoSignTx": 12003, + "MessageType_ScdoSignedTx": 12004, + "MessageType_ScdoTxAck": 12005, + "MessageType_ScdoSignMessage": 12006, + "MessageType_ScdoSignedMessage": 12007, + "MessageType_AlephiumGetAddress": 12101, + "MessageType_AlephiumAddress": 12102, + "MessageType_AlephiumSignTx": 12103, + "MessageType_AlephiumSignedTx": 12104, + "MessageType_AlephiumTxRequest": 12105, + "MessageType_AlephiumTxAck": 12106, + "MessageType_AlephiumBytecodeRequest": 12107, + "MessageType_AlephiumBytecodeAck": 12108, + "MessageType_AlephiumSignMessage": 12109, + "MessageType_AlephiumMessageSignature": 12110, + "MessageType_BenfenGetAddress": 12201, + "MessageType_BenfenAddress": 12202, + "MessageType_BenfenSignTx": 12203, + "MessageType_BenfenSignedTx": 12204, + "MessageType_BenfenSignMessage": 12205, + "MessageType_BenfenMessageSignature": 12206, + "MessageType_BenfenTxRequest": 12207, + "MessageType_BenfenTxAck": 12208, + "MessageType_NeoGetAddress": 12301, + "MessageType_NeoAddress": 12302, + "MessageType_NeoSignTx": 12303, + "MessageType_NeoSignedTx": 12304, + "MessageType_UiviewShowAddressRequest": 30200, + "MessageType_UiviewShowPublicKeyRequest": 30201, + "MessageType_UiviewConfirmTxRequest": 30202, + "MessageType_UiviewConfirmSignMessageRequest": 30203, + "MessageType_UiviewResponse": 30204, + "MessageType_DeviceFactoryInfoSet": 60000, + "MessageType_DeviceFactoryInfoGet": 60001, + "MessageType_DeviceFactoryInfo": 60002, + "MessageType_DeviceFactoryPermanentLock": 60003, + "MessageType_DeviceFactoryTest": 60004, + "MessageType_ProtocolInfoRequest": 60200, + "MessageType_ProtocolInfo": 60201, + "MessageType_Ping": 60206, + "MessageType_Success": 60207, + "MessageType_Failure": 60208, + "MessageType_DeviceReboot": 60400, + "MessageType_DeviceSettings": 60410, + "MessageType_DeviceSettingsGet": 60411, + "MessageType_DeviceSettingsSet": 60412, + "MessageType_DeviceSettingsPageShow": 60413, + "MessageType_DeviceCertificate": 60420, + "MessageType_DeviceCertificateWrite": 60421, + "MessageType_DeviceCertificateRead": 60422, + "MessageType_DeviceCertificateSignature": 60423, + "MessageType_DeviceCertificateSign": 60424, + "MessageType_SetWallpaper": 60430, + "MessageType_GetWallpaper": 60431, + "MessageType_Wallpaper": 60432, + "MessageType_DeviceInfoGet": 60600, + "MessageType_DeviceInfo": 60601, + "MessageType_DeviceStatusGet": 60602, + "MessageType_DeviceStatus": 60603, + "MessageType_DevGetOnboardingStatus": 60604, + "MessageType_DevOnboardingStatus": 60605, + "MessageType_DeviceSessionGet": 60606, + "MessageType_DeviceSession": 60607, + "MessageType_DeviceSessionAskPin": 60608, + "MessageType_FilesystemPermissionFix": 60800, + "MessageType_FilesystemPathInfo": 60801, + "MessageType_FilesystemPathInfoQuery": 60802, + "MessageType_FilesystemFile": 60803, + "MessageType_FilesystemFileRead": 60804, + "MessageType_FilesystemFileWrite": 60805, + "MessageType_FilesystemFileDelete": 60806, + "MessageType_FilesystemDir": 60807, + "MessageType_FilesystemDirList": 60808, + "MessageType_FilesystemDirMake": 60809, + "MessageType_FilesystemDirRemove": 60810, + "MessageType_FilesystemFormat": 60811, + "MessageType_DeviceFirmwareUpdateRequest": 61000, + "MessageType_DeviceFirmwareUpdateStatusGet": 61001, + "MessageType_DeviceFirmwareUpdateStatus": 61002, + "MessageType_PortfolioUpdate": 61200 + }, + "reserved": [ + [90, 92], + [114, 122], + [300, 304], + [309, 312] + ] + }, + "AlephiumGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + }, + "include_public_key": { + "type": "bool", + "id": 3 + }, + "target_group": { + "type": "uint32", + "id": 4 + } + } + }, + "AlephiumAddress": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + }, + "public_key": { + "type": "bytes", + "id": 2 + }, + "derived_path": { + "rule": "repeated", + "type": "uint32", + "id": 3, + "options": { + "packed": false + } + } + } + }, + "AlephiumSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "data_initial_chunk": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "data_length": { + "type": "uint32", + "id": 3 + } + } + }, + "AlephiumSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "address": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "AlephiumTxRequest": { + "fields": { + "data_length": { + "type": "uint32", + "id": 1 + }, + "public_key": { + "type": "bytes", + "id": 2 + }, + "signature": { + "type": "bytes", + "id": 3 + } + } + }, + "AlephiumTxAck": { + "fields": { + "data_chunk": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "AlephiumBytecodeRequest": { + "fields": { + "data_length": { + "type": "uint32", + "id": 1 + }, + "public_key": { + "type": "bytes", + "id": 2 + }, + "signature": { + "type": "bytes", + "id": 3 + } + } + }, + "AlephiumBytecodeAck": { + "fields": { + "bytecode_data": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "AlephiumSignMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "type": "bytes", + "id": 2 + }, + "message_type": { + "type": "bytes", + "id": 3 + } + } + }, + "AlephiumMessageSignature": { + "fields": { + "signature": { + "type": "bytes", + "id": 1 + }, + "address": { + "type": "string", + "id": 2 + } + } + }, + "AlgorandGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 3 + } + } + }, + "AlgorandAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + } + } + }, + "AlgorandSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "AlgorandSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "AptosTransactionType": { + "values": { + "STANDARD": 0, + "WITH_DATA": 1 + } + }, + "AptosGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "AptosAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + } + } + }, + "AptosSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "tx_type": { + "type": "AptosTransactionType", + "id": 3, + "options": { + "default": "STANDARD" + } + } + } + }, + "AptosSignedTx": { + "fields": { + "public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "AptosSignMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "payload": { + "rule": "required", + "type": "AptosMessagePayload", + "id": 2 + } + }, + "nested": { + "AptosMessagePayload": { + "fields": { + "address": { + "type": "string", + "id": 2 + }, + "chain_id": { + "type": "string", + "id": 3 + }, + "application": { + "type": "string", + "id": 4 + }, + "nonce": { + "rule": "required", + "type": "string", + "id": 5 + }, + "message": { + "rule": "required", + "type": "string", + "id": 6 + } + } + } + } + }, + "AptosSignSIWAMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "siwa_payload": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "AptosMessageSignature": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "address": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "BenfenGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "BenfenAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + } + } + }, + "BenfenSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "data_initial_chunk": { + "type": "bytes", + "id": 3, + "options": { + "default": "" + } + }, + "coin_type": { + "type": "bytes", + "id": 4 + }, + "data_length": { + "type": "uint32", + "id": 5 + } + } + }, + "BenfenSignedTx": { + "fields": { + "public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "BenfenTxRequest": { + "fields": { + "data_length": { + "type": "uint32", + "id": 1 + }, + "public_key": { + "type": "bytes", + "id": 2 + }, + "signature": { + "type": "bytes", + "id": 3 + } + } + }, + "BenfenTxAck": { + "fields": { + "data_chunk": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "BenfenSignMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "BenfenMessageSignature": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "address": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "BinanceGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "BinanceAddress": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "BinanceGetPublicKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "BinancePublicKey": { + "fields": { + "public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "BinanceSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "msg_count": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "account_number": { + "rule": "required", + "type": "sint64", + "id": 3 + }, + "chain_id": { + "type": "string", + "id": 4 + }, + "memo": { + "type": "string", + "id": 5 + }, + "sequence": { + "rule": "required", + "type": "sint64", + "id": 6 + }, + "source": { + "rule": "required", + "type": "sint64", + "id": 7 + } + } + }, + "BinanceTxRequest": { + "fields": {} + }, + "BinanceTransferMsg": { + "fields": { + "inputs": { + "rule": "repeated", + "type": "BinanceInputOutput", + "id": 1 + }, + "outputs": { + "rule": "repeated", + "type": "BinanceInputOutput", + "id": 2 + } + }, + "nested": { + "BinanceInputOutput": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + }, + "coins": { + "rule": "repeated", + "type": "BinanceCoin", + "id": 2 + } + } + }, + "BinanceCoin": { + "fields": { + "amount": { + "rule": "required", + "type": "sint64", + "id": 1 + }, + "denom": { + "rule": "required", + "type": "string", + "id": 2 + } + } + } + } + }, + "BinanceOrderMsg": { + "fields": { + "id": { + "type": "string", + "id": 1 + }, + "ordertype": { + "rule": "required", + "type": "BinanceOrderType", + "id": 2 + }, + "price": { + "rule": "required", + "type": "sint64", + "id": 3 + }, + "quantity": { + "rule": "required", + "type": "sint64", + "id": 4 + }, + "sender": { + "type": "string", + "id": 5 + }, + "side": { + "rule": "required", + "type": "BinanceOrderSide", + "id": 6 + }, + "symbol": { + "type": "string", + "id": 7 + }, + "timeinforce": { + "rule": "required", + "type": "BinanceTimeInForce", + "id": 8 + } + }, + "nested": { + "BinanceOrderType": { + "values": { + "OT_UNKNOWN": 0, + "MARKET": 1, + "LIMIT": 2, + "OT_RESERVED": 3 + } + }, + "BinanceOrderSide": { + "values": { + "SIDE_UNKNOWN": 0, + "BUY": 1, + "SELL": 2 + } + }, + "BinanceTimeInForce": { + "values": { + "TIF_UNKNOWN": 0, + "GTE": 1, + "TIF_RESERVED": 2, + "IOC": 3 + } + } + } + }, + "BinanceCancelMsg": { + "fields": { + "refid": { + "type": "string", + "id": 1 + }, + "sender": { + "type": "string", + "id": 2 + }, + "symbol": { + "type": "string", + "id": 3 + } + } + }, + "BinanceSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "public_key": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "InputScriptType": { + "values": { + "SPENDADDRESS": 0, + "SPENDMULTISIG": 1, + "EXTERNAL": 2, + "SPENDWITNESS": 3, + "SPENDP2SHWITNESS": 4, + "SPENDTAPROOT": 5 + } + }, + "OutputScriptType": { + "values": { + "PAYTOADDRESS": 0, + "PAYTOSCRIPTHASH": 1, + "PAYTOMULTISIG": 2, + "PAYTOOPRETURN": 3, + "PAYTOWITNESS": 4, + "PAYTOP2SHWITNESS": 5, + "PAYTOTAPROOT": 6 + } + }, + "DecredStakingSpendType": { + "values": { + "SSGen": 0, + "SSRTX": 1 + } + }, + "AmountUnit": { + "values": { + "BITCOIN": 0, + "MILLIBITCOIN": 1, + "MICROBITCOIN": 2, + "SATOSHI": 3 + } + }, + "MultisigRedeemScriptType": { + "fields": { + "pubkeys": { + "rule": "repeated", + "type": "HDNodePathType", + "id": 1 + }, + "signatures": { + "rule": "repeated", + "type": "bytes", + "id": 2 + }, + "m": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "nodes": { + "rule": "repeated", + "type": "HDNodeType", + "id": 4 + }, + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 5, + "options": { + "packed": false + } + } + }, + "nested": { + "HDNodePathType": { + "fields": { + "node": { + "rule": "required", + "type": "HDNodeType", + "id": 1 + }, + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 2, + "options": { + "packed": false + } + } + } + } + } + }, + "GetPublicKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "ecdsa_curve_name": { + "type": "string", + "id": 2 + }, + "show_display": { + "type": "bool", + "id": 3 + }, + "coin_name": { + "type": "string", + "id": 4, + "options": { + "default": "Bitcoin" + } + }, + "script_type": { + "type": "InputScriptType", + "id": 5, + "options": { + "default": "SPENDADDRESS" + } + }, + "ignore_xpub_magic": { + "type": "bool", + "id": 6 + } + } + }, + "PublicKey": { + "fields": { + "node": { + "rule": "required", + "type": "HDNodeType", + "id": 1 + }, + "xpub": { + "rule": "required", + "type": "string", + "id": 2 + }, + "root_fingerprint": { + "type": "uint32", + "id": 3 + } + } + }, + "GetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "coin_name": { + "type": "string", + "id": 2, + "options": { + "default": "Bitcoin" + } + }, + "show_display": { + "type": "bool", + "id": 3 + }, + "multisig": { + "type": "MultisigRedeemScriptType", + "id": 4 + }, + "script_type": { + "type": "InputScriptType", + "id": 5, + "options": { + "default": "SPENDADDRESS" + } + }, + "ignore_xpub_magic": { + "type": "bool", + "id": 6 + } + } + }, + "Address": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + }, + "mac": { + "type": "bytes", + "id": 2 + } + } + }, + "GetOwnershipId": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "coin_name": { + "type": "string", + "id": 2, + "options": { + "default": "Bitcoin" + } + }, + "multisig": { + "type": "MultisigRedeemScriptType", + "id": 3 + }, + "script_type": { + "type": "InputScriptType", + "id": 4, + "options": { + "default": "SPENDADDRESS" + } + } + } + }, + "OwnershipId": { + "fields": { + "ownership_id": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "SignMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "coin_name": { + "type": "string", + "id": 3, + "options": { + "default": "Bitcoin" + } + }, + "script_type": { + "type": "InputScriptType", + "id": 4, + "options": { + "default": "SPENDADDRESS" + } + }, + "no_script_type": { + "type": "bool", + "id": 5 + }, + "is_bip322_simple": { + "type": "bool", + "id": 10, + "options": { + "default": false + } + } + } + }, + "MessageSignature": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "VerifyMessage": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "coin_name": { + "type": "string", + "id": 4, + "options": { + "default": "Bitcoin" + } + } + } + }, + "SignTx": { + "fields": { + "outputs_count": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "inputs_count": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "coin_name": { + "type": "string", + "id": 3, + "options": { + "default": "Bitcoin" + } + }, + "version": { + "type": "uint32", + "id": 4, + "options": { + "default": 1 + } + }, + "lock_time": { + "type": "uint32", + "id": 5, + "options": { + "default": 0 + } + }, + "expiry": { + "type": "uint32", + "id": 6 + }, + "overwintered": { + "type": "bool", + "id": 7, + "options": { + "deprecated": true + } + }, + "version_group_id": { + "type": "uint32", + "id": 8 + }, + "timestamp": { + "type": "uint32", + "id": 9 + }, + "branch_id": { + "type": "uint32", + "id": 10 + }, + "amount_unit": { + "type": "AmountUnit", + "id": 11, + "options": { + "default": "BITCOIN" + } + }, + "decred_staking_ticket": { + "type": "bool", + "id": 12, + "options": { + "default": false + } + }, + "serialize": { + "type": "bool", + "id": 13, + "options": { + "default": true + } + }, + "coinjoin_request": { + "type": "CoinJoinRequest", + "id": 14 + } + }, + "nested": { + "CoinJoinRequest": { + "fields": { + "fee_rate": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "no_fee_threshold": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "min_registrable_amount": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "mask_public_key": { + "rule": "required", + "type": "bytes", + "id": 4 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 5 + } + } + } + } + }, + "TxRequest": { + "fields": { + "request_type": { + "type": "RequestType", + "id": 1 + }, + "details": { + "type": "TxRequestDetailsType", + "id": 2 + }, + "serialized": { + "type": "TxRequestSerializedType", + "id": 3 + } + }, + "nested": { + "RequestType": { + "values": { + "TXINPUT": 0, + "TXOUTPUT": 1, + "TXMETA": 2, + "TXFINISHED": 3, + "TXEXTRADATA": 4, + "TXORIGINPUT": 5, + "TXORIGOUTPUT": 6, + "TXPAYMENTREQ": 7 + } + }, + "TxRequestDetailsType": { + "fields": { + "request_index": { + "type": "uint32", + "id": 1 + }, + "tx_hash": { + "type": "bytes", + "id": 2 + }, + "extra_data_len": { + "type": "uint32", + "id": 3 + }, + "extra_data_offset": { + "type": "uint32", + "id": 4 + } + } + }, + "TxRequestSerializedType": { + "fields": { + "signature_index": { + "type": "uint32", + "id": 1 + }, + "signature": { + "type": "bytes", + "id": 2 + }, + "serialized_tx": { + "type": "bytes", + "id": 3 + } + } + } + } + }, + "TxAck": { + "options": { + "deprecated": true + }, + "fields": { + "tx": { + "type": "TransactionType", + "id": 1 + } + }, + "nested": { + "TransactionType": { + "fields": { + "version": { + "type": "uint32", + "id": 1 + }, + "inputs": { + "rule": "repeated", + "type": "TxInputType", + "id": 2 + }, + "bin_outputs": { + "rule": "repeated", + "type": "TxOutputBinType", + "id": 3 + }, + "lock_time": { + "type": "uint32", + "id": 4 + }, + "outputs": { + "rule": "repeated", + "type": "TxOutputType", + "id": 5 + }, + "inputs_cnt": { + "type": "uint32", + "id": 6 + }, + "outputs_cnt": { + "type": "uint32", + "id": 7 + }, + "extra_data": { + "type": "bytes", + "id": 8 + }, + "extra_data_len": { + "type": "uint32", + "id": 9 + }, + "expiry": { + "type": "uint32", + "id": 10 + }, + "overwintered": { + "type": "bool", + "id": 11, + "options": { + "deprecated": true + } + }, + "version_group_id": { + "type": "uint32", + "id": 12 + }, + "timestamp": { + "type": "uint32", + "id": 13 + }, + "branch_id": { + "type": "uint32", + "id": 14 + } + }, + "nested": { + "TxInputType": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "prev_hash": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "prev_index": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "script_sig": { + "type": "bytes", + "id": 4 + }, + "sequence": { + "type": "uint32", + "id": 5, + "options": { + "default": 4294967295 + } + }, + "script_type": { + "type": "InputScriptType", + "id": 6, + "options": { + "default": "SPENDADDRESS" + } + }, + "multisig": { + "type": "MultisigRedeemScriptType", + "id": 7 + }, + "amount": { + "type": "uint64", + "id": 8 + }, + "decred_tree": { + "type": "uint32", + "id": 9 + }, + "witness": { + "type": "bytes", + "id": 13 + }, + "ownership_proof": { + "type": "bytes", + "id": 14 + }, + "commitment_data": { + "type": "bytes", + "id": 15 + }, + "orig_hash": { + "type": "bytes", + "id": 16 + }, + "orig_index": { + "type": "uint32", + "id": 17 + }, + "decred_staking_spend": { + "type": "DecredStakingSpendType", + "id": 18 + }, + "script_pubkey": { + "type": "bytes", + "id": 19 + }, + "coinjoin_flags": { + "type": "uint32", + "id": 20, + "options": { + "default": 0 + } + } + } + }, + "TxOutputBinType": { + "fields": { + "amount": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "script_pubkey": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "decred_script_version": { + "type": "uint32", + "id": 3 + } + } + }, + "TxOutputType": { + "fields": { + "address": { + "type": "string", + "id": 1 + }, + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 2, + "options": { + "packed": false + } + }, + "amount": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "script_type": { + "type": "OutputScriptType", + "id": 4, + "options": { + "default": "PAYTOADDRESS" + } + }, + "multisig": { + "type": "MultisigRedeemScriptType", + "id": 5 + }, + "op_return_data": { + "type": "bytes", + "id": 6 + }, + "orig_hash": { + "type": "bytes", + "id": 10 + }, + "orig_index": { + "type": "uint32", + "id": 11 + }, + "payment_req_index": { + "type": "uint32", + "id": 12, + "options": { + "(experimental_field)": true + } + } + } + } + } + } + } + }, + "TxInput": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "prev_hash": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "prev_index": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "script_sig": { + "type": "bytes", + "id": 4 + }, + "sequence": { + "type": "uint32", + "id": 5, + "options": { + "default": 4294967295 + } + }, + "script_type": { + "type": "InputScriptType", + "id": 6, + "options": { + "default": "SPENDADDRESS" + } + }, + "multisig": { + "type": "MultisigRedeemScriptType", + "id": 7 + }, + "amount": { + "rule": "required", + "type": "uint64", + "id": 8 + }, + "decred_tree": { + "type": "uint32", + "id": 9 + }, + "witness": { + "type": "bytes", + "id": 13 + }, + "ownership_proof": { + "type": "bytes", + "id": 14 + }, + "commitment_data": { + "type": "bytes", + "id": 15 + }, + "orig_hash": { + "type": "bytes", + "id": 16 + }, + "orig_index": { + "type": "uint32", + "id": 17 + }, + "decred_staking_spend": { + "type": "DecredStakingSpendType", + "id": 18 + }, + "script_pubkey": { + "type": "bytes", + "id": 19 + }, + "coinjoin_flags": { + "type": "uint32", + "id": 20, + "options": { + "default": 0 + } + } + } + }, + "TxOutput": { + "fields": { + "address": { + "type": "string", + "id": 1 + }, + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 2, + "options": { + "packed": false + } + }, + "amount": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "script_type": { + "type": "OutputScriptType", + "id": 4, + "options": { + "default": "PAYTOADDRESS" + } + }, + "multisig": { + "type": "MultisigRedeemScriptType", + "id": 5 + }, + "op_return_data": { + "type": "bytes", + "id": 6 + }, + "orig_hash": { + "type": "bytes", + "id": 10 + }, + "orig_index": { + "type": "uint32", + "id": 11 + }, + "payment_req_index": { + "type": "uint32", + "id": 12, + "options": { + "(experimental_field)": true + } + } + } + }, + "PrevTx": { + "fields": { + "version": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "lock_time": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "inputs_count": { + "rule": "required", + "type": "uint32", + "id": 6 + }, + "outputs_count": { + "rule": "required", + "type": "uint32", + "id": 7 + }, + "extra_data_len": { + "type": "uint32", + "id": 9, + "options": { + "default": 0 + } + }, + "expiry": { + "type": "uint32", + "id": 10 + }, + "version_group_id": { + "type": "uint32", + "id": 12 + }, + "timestamp": { + "type": "uint32", + "id": 13 + }, + "branch_id": { + "type": "uint32", + "id": 14 + } + } + }, + "PrevInput": { + "fields": { + "prev_hash": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "prev_index": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "script_sig": { + "rule": "required", + "type": "bytes", + "id": 4 + }, + "sequence": { + "rule": "required", + "type": "uint32", + "id": 5 + }, + "decred_tree": { + "type": "uint32", + "id": 9 + } + } + }, + "PrevOutput": { + "fields": { + "amount": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "script_pubkey": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "decred_script_version": { + "type": "uint32", + "id": 3 + } + } + }, + "TxAckPaymentRequest": { + "options": { + "(experimental_message)": true + }, + "fields": { + "nonce": { + "type": "bytes", + "id": 1 + }, + "recipient_name": { + "rule": "required", + "type": "string", + "id": 2 + }, + "memos": { + "rule": "repeated", + "type": "PaymentRequestMemo", + "id": 3 + }, + "amount": { + "type": "uint64", + "id": 4 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 5 + } + }, + "nested": { + "PaymentRequestMemo": { + "fields": { + "text_memo": { + "type": "TextMemo", + "id": 1 + }, + "refund_memo": { + "type": "RefundMemo", + "id": 2 + }, + "coin_purchase_memo": { + "type": "CoinPurchaseMemo", + "id": 3 + } + } + }, + "TextMemo": { + "fields": { + "text": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "RefundMemo": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + }, + "mac": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "CoinPurchaseMemo": { + "fields": { + "coin_type": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "amount": { + "rule": "required", + "type": "string", + "id": 2 + }, + "address": { + "rule": "required", + "type": "string", + "id": 3 + }, + "mac": { + "rule": "required", + "type": "bytes", + "id": 4 + } + } + } + } + }, + "TxAckInput": { + "options": { + "(wire_type)": 22 + }, + "fields": { + "tx": { + "rule": "required", + "type": "TxAckInputWrapper", + "id": 1 + } + }, + "nested": { + "TxAckInputWrapper": { + "fields": { + "input": { + "rule": "required", + "type": "TxInput", + "id": 2 + } + } + } + } + }, + "TxAckOutput": { + "options": { + "(wire_type)": 22 + }, + "fields": { + "tx": { + "rule": "required", + "type": "TxAckOutputWrapper", + "id": 1 + } + }, + "nested": { + "TxAckOutputWrapper": { + "fields": { + "output": { + "rule": "required", + "type": "TxOutput", + "id": 5 + } + } + } + } + }, + "TxAckPrevMeta": { + "options": { + "(wire_type)": 22 + }, + "fields": { + "tx": { + "rule": "required", + "type": "PrevTx", + "id": 1 + } + } + }, + "TxAckPrevInput": { + "options": { + "(wire_type)": 22 + }, + "fields": { + "tx": { + "rule": "required", + "type": "TxAckPrevInputWrapper", + "id": 1 + } + }, + "nested": { + "TxAckPrevInputWrapper": { + "fields": { + "input": { + "rule": "required", + "type": "PrevInput", + "id": 2 + } + } + } + } + }, + "TxAckPrevOutput": { + "options": { + "(wire_type)": 22 + }, + "fields": { + "tx": { + "rule": "required", + "type": "TxAckPrevOutputWrapper", + "id": 1 + } + }, + "nested": { + "TxAckPrevOutputWrapper": { + "fields": { + "output": { + "rule": "required", + "type": "PrevOutput", + "id": 3 + } + } + } + } + }, + "TxAckPrevExtraData": { + "options": { + "(wire_type)": 22 + }, + "fields": { + "tx": { + "rule": "required", + "type": "TxAckPrevExtraDataWrapper", + "id": 1 + } + }, + "nested": { + "TxAckPrevExtraDataWrapper": { + "fields": { + "extra_data_chunk": { + "rule": "required", + "type": "bytes", + "id": 8 + } + } + } + } + }, + "GetOwnershipProof": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "coin_name": { + "type": "string", + "id": 2, + "options": { + "default": "Bitcoin" + } + }, + "script_type": { + "type": "InputScriptType", + "id": 3, + "options": { + "default": "SPENDWITNESS" + } + }, + "multisig": { + "type": "MultisigRedeemScriptType", + "id": 4 + }, + "user_confirmation": { + "type": "bool", + "id": 5, + "options": { + "default": false + } + }, + "ownership_ids": { + "rule": "repeated", + "type": "bytes", + "id": 6 + }, + "commitment_data": { + "type": "bytes", + "id": 7, + "options": { + "default": "" + } + } + } + }, + "OwnershipProof": { + "fields": { + "ownership_proof": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "AuthorizeCoinJoin": { + "fields": { + "coordinator": { + "rule": "required", + "type": "string", + "id": 1 + }, + "max_rounds": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "max_coordinator_fee_rate": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "max_fee_per_kvbyte": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 5, + "options": { + "packed": false + } + }, + "coin_name": { + "type": "string", + "id": 6, + "options": { + "default": "Bitcoin" + } + }, + "script_type": { + "type": "InputScriptType", + "id": 7, + "options": { + "default": "SPENDADDRESS" + } + }, + "amount_unit": { + "type": "AmountUnit", + "id": 8, + "options": { + "default": "BITCOIN" + } + } + } + }, + "SignPsbt": { + "fields": { + "psbt": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "coin_name": { + "type": "string", + "id": 2, + "options": { + "default": "Bitcoin" + } + } + } + }, + "SignedPsbt": { + "fields": { + "psbt": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "ButtonRequest": { + "fields": { + "code": { + "type": "ButtonRequestType", + "id": 1 + }, + "pages": { + "type": "uint32", + "id": 2 + } + }, + "nested": { + "ButtonRequestType": { + "values": { + "ButtonRequest_Other": 1, + "ButtonRequest_FeeOverThreshold": 2, + "ButtonRequest_ConfirmOutput": 3, + "ButtonRequest_ResetDevice": 4, + "ButtonRequest_ConfirmWord": 5, + "ButtonRequest_WipeDevice": 6, + "ButtonRequest_ProtectCall": 7, + "ButtonRequest_SignTx": 8, + "ButtonRequest_FirmwareCheck": 9, + "ButtonRequest_Address": 10, + "ButtonRequest_PublicKey": 11, + "ButtonRequest_MnemonicWordCount": 12, + "ButtonRequest_MnemonicInput": 13, + "_Deprecated_ButtonRequest_PassphraseType": 14, + "ButtonRequest_UnknownDerivationPath": 15, + "ButtonRequest_RecoveryHomepage": 16, + "ButtonRequest_Success": 17, + "ButtonRequest_Warning": 18, + "ButtonRequest_PassphraseEntry": 19, + "ButtonRequest_PinEntry": 20, + "ButtonRequest_AttachPin": 8000 + } + } + } + }, + "ButtonAck": { + "fields": {} + }, + "CardanoDerivationType": { + "values": { + "LEDGER": 0, + "ICARUS": 1, + "ICARUS_TREZOR": 2 + } + }, + "CardanoAddressType": { + "values": { + "BASE": 0, + "BASE_SCRIPT_KEY": 1, + "BASE_KEY_SCRIPT": 2, + "BASE_SCRIPT_SCRIPT": 3, + "POINTER": 4, + "POINTER_SCRIPT": 5, + "ENTERPRISE": 6, + "ENTERPRISE_SCRIPT": 7, + "BYRON": 8, + "REWARD": 14, + "REWARD_SCRIPT": 15 + } + }, + "CardanoNativeScriptType": { + "values": { + "PUB_KEY": 0, + "ALL": 1, + "ANY": 2, + "N_OF_K": 3, + "INVALID_BEFORE": 4, + "INVALID_HEREAFTER": 5 + } + }, + "CardanoNativeScriptHashDisplayFormat": { + "values": { + "HIDE": 0, + "BECH32": 1, + "POLICY_ID": 2 + } + }, + "CardanoTxOutputSerializationFormat": { + "values": { + "ARRAY_LEGACY": 0, + "MAP_BABBAGE": 1 + } + }, + "CardanoCertificateType": { + "values": { + "STAKE_REGISTRATION": 0, + "STAKE_DEREGISTRATION": 1, + "STAKE_DELEGATION": 2, + "STAKE_POOL_REGISTRATION": 3, + "STAKE_REGISTRATION_CONWAY": 7, + "STAKE_DEREGISTRATION_CONWAY": 8, + "VOTE_DELEGATION": 9 + } + }, + "CardanoDRepType": { + "values": { + "KEY_HASH": 0, + "SCRIPT_HASH": 1, + "ABSTAIN": 2, + "NO_CONFIDENCE": 3 + } + }, + "CardanoPoolRelayType": { + "values": { + "SINGLE_HOST_IP": 0, + "SINGLE_HOST_NAME": 1, + "MULTIPLE_HOST_NAME": 2 + } + }, + "CardanoTxAuxiliaryDataSupplementType": { + "values": { + "NONE": 0, + "CVOTE_REGISTRATION_SIGNATURE": 1 + } + }, + "CardanoCVoteRegistrationFormat": { + "values": { + "CIP15": 0, + "CIP36": 1 + } + }, + "CardanoTxSigningMode": { + "values": { + "ORDINARY_TRANSACTION": 0, + "POOL_REGISTRATION_AS_OWNER": 1, + "MULTISIG_TRANSACTION": 2, + "PLUTUS_TRANSACTION": 3 + } + }, + "CardanoTxWitnessType": { + "values": { + "BYRON_WITNESS": 0, + "SHELLEY_WITNESS": 1 + } + }, + "CardanoBlockchainPointerType": { + "fields": { + "block_index": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "tx_index": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "certificate_index": { + "rule": "required", + "type": "uint32", + "id": 3 + } + } + }, + "CardanoNativeScript": { + "fields": { + "type": { + "rule": "required", + "type": "CardanoNativeScriptType", + "id": 1 + }, + "scripts": { + "rule": "repeated", + "type": "CardanoNativeScript", + "id": 2 + }, + "key_hash": { + "type": "bytes", + "id": 3 + }, + "key_path": { + "rule": "repeated", + "type": "uint32", + "id": 4, + "options": { + "packed": false + } + }, + "required_signatures_count": { + "type": "uint32", + "id": 5 + }, + "invalid_before": { + "type": "uint64", + "id": 6 + }, + "invalid_hereafter": { + "type": "uint64", + "id": 7 + } + } + }, + "CardanoGetNativeScriptHash": { + "fields": { + "script": { + "rule": "required", + "type": "CardanoNativeScript", + "id": 1 + }, + "display_format": { + "rule": "required", + "type": "CardanoNativeScriptHashDisplayFormat", + "id": 2 + }, + "derivation_type": { + "rule": "required", + "type": "CardanoDerivationType", + "id": 3 + } + } + }, + "CardanoNativeScriptHash": { + "fields": { + "script_hash": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "CardanoAddressParametersType": { + "fields": { + "address_type": { + "rule": "required", + "type": "CardanoAddressType", + "id": 1 + }, + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 2, + "options": { + "packed": false + } + }, + "address_n_staking": { + "rule": "repeated", + "type": "uint32", + "id": 3, + "options": { + "packed": false + } + }, + "staking_key_hash": { + "type": "bytes", + "id": 4 + }, + "certificate_pointer": { + "type": "CardanoBlockchainPointerType", + "id": 5 + }, + "script_payment_hash": { + "type": "bytes", + "id": 6 + }, + "script_staking_hash": { + "type": "bytes", + "id": 7 + } + } + }, + "CardanoGetAddress": { + "fields": { + "show_display": { + "type": "bool", + "id": 2, + "options": { + "default": false + } + }, + "protocol_magic": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "network_id": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "address_parameters": { + "rule": "required", + "type": "CardanoAddressParametersType", + "id": 5 + }, + "derivation_type": { + "rule": "required", + "type": "CardanoDerivationType", + "id": 6 + }, + "chunkify": { + "type": "bool", + "id": 7 + } + } + }, + "CardanoAddress": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "CardanoGetPublicKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + }, + "derivation_type": { + "rule": "required", + "type": "CardanoDerivationType", + "id": 3 + } + } + }, + "CardanoPublicKey": { + "fields": { + "xpub": { + "rule": "required", + "type": "string", + "id": 1 + }, + "node": { + "rule": "required", + "type": "HDNodeType", + "id": 2 + } + } + }, + "CardanoSignTxInit": { + "fields": { + "signing_mode": { + "rule": "required", + "type": "CardanoTxSigningMode", + "id": 1 + }, + "protocol_magic": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "network_id": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "inputs_count": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "outputs_count": { + "rule": "required", + "type": "uint32", + "id": 5 + }, + "fee": { + "rule": "required", + "type": "uint64", + "id": 6 + }, + "ttl": { + "type": "uint64", + "id": 7 + }, + "certificates_count": { + "rule": "required", + "type": "uint32", + "id": 8 + }, + "withdrawals_count": { + "rule": "required", + "type": "uint32", + "id": 9 + }, + "has_auxiliary_data": { + "rule": "required", + "type": "bool", + "id": 10 + }, + "validity_interval_start": { + "type": "uint64", + "id": 11 + }, + "witness_requests_count": { + "rule": "required", + "type": "uint32", + "id": 12 + }, + "minting_asset_groups_count": { + "rule": "required", + "type": "uint32", + "id": 13 + }, + "derivation_type": { + "rule": "required", + "type": "CardanoDerivationType", + "id": 14 + }, + "include_network_id": { + "type": "bool", + "id": 15, + "options": { + "default": false + } + }, + "script_data_hash": { + "type": "bytes", + "id": 16 + }, + "collateral_inputs_count": { + "rule": "required", + "type": "uint32", + "id": 17 + }, + "required_signers_count": { + "rule": "required", + "type": "uint32", + "id": 18 + }, + "has_collateral_return": { + "type": "bool", + "id": 19, + "options": { + "default": false + } + }, + "total_collateral": { + "type": "uint64", + "id": 20 + }, + "reference_inputs_count": { + "type": "uint32", + "id": 21, + "options": { + "default": 0 + } + }, + "chunkify": { + "type": "bool", + "id": 22 + }, + "tag_cbor_sets": { + "type": "bool", + "id": 23, + "options": { + "default": false + } + } + } + }, + "CardanoTxInput": { + "fields": { + "prev_hash": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "prev_index": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + }, + "CardanoTxOutput": { + "fields": { + "address": { + "type": "string", + "id": 1 + }, + "address_parameters": { + "type": "CardanoAddressParametersType", + "id": 2 + }, + "amount": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "asset_groups_count": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "datum_hash": { + "type": "bytes", + "id": 5 + }, + "format": { + "type": "CardanoTxOutputSerializationFormat", + "id": 6, + "options": { + "default": "ARRAY_LEGACY" + } + }, + "inline_datum_size": { + "type": "uint32", + "id": 7, + "options": { + "default": 0 + } + }, + "reference_script_size": { + "type": "uint32", + "id": 8, + "options": { + "default": 0 + } + } + } + }, + "CardanoAssetGroup": { + "fields": { + "policy_id": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "tokens_count": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + }, + "CardanoToken": { + "fields": { + "asset_name_bytes": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "amount": { + "type": "uint64", + "id": 2 + }, + "mint_amount": { + "type": "sint64", + "id": 3 + } + } + }, + "CardanoTxInlineDatumChunk": { + "fields": { + "data": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "CardanoTxReferenceScriptChunk": { + "fields": { + "data": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "CardanoPoolOwner": { + "fields": { + "staking_key_path": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "staking_key_hash": { + "type": "bytes", + "id": 2 + } + } + }, + "CardanoPoolRelayParameters": { + "fields": { + "type": { + "rule": "required", + "type": "CardanoPoolRelayType", + "id": 1 + }, + "ipv4_address": { + "type": "bytes", + "id": 2 + }, + "ipv6_address": { + "type": "bytes", + "id": 3 + }, + "host_name": { + "type": "string", + "id": 4 + }, + "port": { + "type": "uint32", + "id": 5 + } + } + }, + "CardanoPoolMetadataType": { + "fields": { + "url": { + "rule": "required", + "type": "string", + "id": 1 + }, + "hash": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "CardanoPoolParametersType": { + "fields": { + "pool_id": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "vrf_key_hash": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "pledge": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "cost": { + "rule": "required", + "type": "uint64", + "id": 4 + }, + "margin_numerator": { + "rule": "required", + "type": "uint64", + "id": 5 + }, + "margin_denominator": { + "rule": "required", + "type": "uint64", + "id": 6 + }, + "reward_account": { + "rule": "required", + "type": "string", + "id": 7 + }, + "metadata": { + "type": "CardanoPoolMetadataType", + "id": 10 + }, + "owners_count": { + "rule": "required", + "type": "uint32", + "id": 11 + }, + "relays_count": { + "rule": "required", + "type": "uint32", + "id": 12 + } + } + }, + "CardanoDRep": { + "fields": { + "type": { + "rule": "required", + "type": "CardanoDRepType", + "id": 1 + }, + "key_hash": { + "type": "bytes", + "id": 2 + }, + "script_hash": { + "type": "bytes", + "id": 3 + } + } + }, + "CardanoTxCertificate": { + "fields": { + "type": { + "rule": "required", + "type": "CardanoCertificateType", + "id": 1 + }, + "path": { + "rule": "repeated", + "type": "uint32", + "id": 2, + "options": { + "packed": false + } + }, + "pool": { + "type": "bytes", + "id": 3 + }, + "pool_parameters": { + "type": "CardanoPoolParametersType", + "id": 4 + }, + "script_hash": { + "type": "bytes", + "id": 5 + }, + "key_hash": { + "type": "bytes", + "id": 6 + }, + "deposit": { + "type": "uint64", + "id": 7 + }, + "drep": { + "type": "CardanoDRep", + "id": 8 + } + } + }, + "CardanoTxWithdrawal": { + "fields": { + "path": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "amount": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "script_hash": { + "type": "bytes", + "id": 3 + }, + "key_hash": { + "type": "bytes", + "id": 4 + } + } + }, + "CardanoCVoteRegistrationDelegation": { + "fields": { + "vote_public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "weight": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + }, + "CardanoCVoteRegistrationParametersType": { + "fields": { + "vote_public_key": { + "type": "bytes", + "id": 1 + }, + "staking_path": { + "rule": "repeated", + "type": "uint32", + "id": 2, + "options": { + "packed": false + } + }, + "payment_address_parameters": { + "type": "CardanoAddressParametersType", + "id": 3 + }, + "nonce": { + "rule": "required", + "type": "uint64", + "id": 4 + }, + "format": { + "type": "CardanoCVoteRegistrationFormat", + "id": 5, + "options": { + "default": "CIP15" + } + }, + "delegations": { + "rule": "repeated", + "type": "CardanoCVoteRegistrationDelegation", + "id": 6 + }, + "voting_purpose": { + "type": "uint64", + "id": 7 + }, + "payment_address": { + "type": "string", + "id": 8 + } + } + }, + "CardanoTxAuxiliaryData": { + "fields": { + "cvote_registration_parameters": { + "type": "CardanoCVoteRegistrationParametersType", + "id": 1 + }, + "hash": { + "type": "bytes", + "id": 2 + } + } + }, + "CardanoTxMint": { + "fields": { + "asset_groups_count": { + "rule": "required", + "type": "uint32", + "id": 1 + } + } + }, + "CardanoTxCollateralInput": { + "fields": { + "prev_hash": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "prev_index": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + }, + "CardanoTxRequiredSigner": { + "fields": { + "key_hash": { + "type": "bytes", + "id": 1 + }, + "key_path": { + "rule": "repeated", + "type": "uint32", + "id": 2, + "options": { + "packed": false + } + } + } + }, + "CardanoTxReferenceInput": { + "fields": { + "prev_hash": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "prev_index": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + }, + "CardanoTxItemAck": { + "fields": {} + }, + "CardanoTxAuxiliaryDataSupplement": { + "fields": { + "type": { + "rule": "required", + "type": "CardanoTxAuxiliaryDataSupplementType", + "id": 1 + }, + "auxiliary_data_hash": { + "type": "bytes", + "id": 2 + }, + "cvote_registration_signature": { + "type": "bytes", + "id": 3 + } + } + }, + "CardanoTxWitnessRequest": { + "fields": { + "path": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + } + } + }, + "CardanoTxWitnessResponse": { + "fields": { + "type": { + "rule": "required", + "type": "CardanoTxWitnessType", + "id": 1 + }, + "pub_key": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "chain_code": { + "type": "bytes", + "id": 4 + } + } + }, + "CardanoTxHostAck": { + "fields": {} + }, + "CardanoTxBodyHash": { + "fields": { + "tx_hash": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "CardanoSignTxFinished": { + "fields": {} + }, + "CardanoSignMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "derivation_type": { + "rule": "required", + "type": "CardanoDerivationType", + "id": 3 + }, + "network_id": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "address_type": { + "type": "CardanoAddressType", + "id": 5 + } + } + }, + "CardanoMessageSignature": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "key": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "ConfluxGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + }, + "chain_id": { + "type": "uint32", + "id": 3 + } + } + }, + "ConfluxAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + } + } + }, + "ConfluxSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "nonce": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "gas_price": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "gas_limit": { + "rule": "required", + "type": "bytes", + "id": 4 + }, + "to": { + "type": "string", + "id": 5, + "options": { + "default": "" + } + }, + "value": { + "type": "bytes", + "id": 6, + "options": { + "default": "" + } + }, + "epoch_height": { + "type": "bytes", + "id": 7, + "options": { + "default": "" + } + }, + "storage_limit": { + "type": "bytes", + "id": 8, + "options": { + "default": "" + } + }, + "data_initial_chunk": { + "type": "bytes", + "id": 9, + "options": { + "default": "" + } + }, + "data_length": { + "type": "uint32", + "id": 10, + "options": { + "default": 0 + } + }, + "chain_id": { + "type": "uint32", + "id": 11, + "options": { + "default": 1029 + } + } + } + }, + "ConfluxTxRequest": { + "fields": { + "data_length": { + "type": "uint32", + "id": 1 + }, + "signature_v": { + "type": "uint32", + "id": 2 + }, + "signature_r": { + "type": "bytes", + "id": 3 + }, + "signature_s": { + "type": "bytes", + "id": 4 + } + } + }, + "ConfluxTxAck": { + "fields": { + "data_chunk": { + "type": "bytes", + "id": 1 + } + } + }, + "ConfluxSignMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "type": "bytes", + "id": 2 + } + } + }, + "ConfluxMessageSignature": { + "fields": { + "signature": { + "type": "bytes", + "id": 2 + }, + "address": { + "type": "string", + "id": 3 + } + } + }, + "ConfluxSignMessageCIP23": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "domain_hash": { + "type": "bytes", + "id": 2 + }, + "message_hash": { + "type": "bytes", + "id": 3 + } + } + }, + "CosmosGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "hrp": { + "type": "string", + "id": 2 + }, + "show_display": { + "type": "bool", + "id": 3 + } + } + }, + "CosmosAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + } + } + }, + "CosmosSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "CosmosSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "HDNodeType": { + "fields": { + "depth": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "fingerprint": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "child_num": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "chain_code": { + "rule": "required", + "type": "bytes", + "id": 4 + }, + "private_key": { + "type": "bytes", + "id": 5 + }, + "public_key": { + "rule": "required", + "type": "bytes", + "id": 6 + } + } + }, + "CipherKeyValue": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "key": { + "rule": "required", + "type": "string", + "id": 2 + }, + "value": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "encrypt": { + "type": "bool", + "id": 4 + }, + "ask_on_encrypt": { + "type": "bool", + "id": 5 + }, + "ask_on_decrypt": { + "type": "bool", + "id": 6 + }, + "iv": { + "type": "bytes", + "id": 7 + } + } + }, + "CipheredKeyValue": { + "fields": { + "value": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "IdentityType": { + "fields": { + "proto": { + "type": "string", + "id": 1 + }, + "user": { + "type": "string", + "id": 2 + }, + "host": { + "type": "string", + "id": 3 + }, + "port": { + "type": "string", + "id": 4 + }, + "path": { + "type": "string", + "id": 5 + }, + "index": { + "type": "uint32", + "id": 6, + "options": { + "default": 0 + } + } + } + }, + "SignIdentity": { + "fields": { + "identity": { + "rule": "required", + "type": "IdentityType", + "id": 1 + }, + "challenge_hidden": { + "type": "bytes", + "id": 2, + "options": { + "default": "" + } + }, + "challenge_visual": { + "type": "string", + "id": 3, + "options": { + "default": "" + } + }, + "ecdsa_curve_name": { + "type": "string", + "id": 4 + } + } + }, + "SignedIdentity": { + "fields": { + "address": { + "type": "string", + "id": 1 + }, + "public_key": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 3 + } + } + }, + "GetECDHSessionKey": { + "fields": { + "identity": { + "rule": "required", + "type": "IdentityType", + "id": 1 + }, + "peer_public_key": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "ecdsa_curve_name": { + "type": "string", + "id": 3 + } + } + }, + "ECDHSessionKey": { + "fields": { + "session_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "public_key": { + "type": "bytes", + "id": 2 + } + } + }, + "CosiCommit": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "data": { + "type": "bytes", + "id": 2, + "options": { + "deprecated": true + } + } + } + }, + "CosiCommitment": { + "fields": { + "commitment": { + "type": "bytes", + "id": 1 + }, + "pubkey": { + "type": "bytes", + "id": 2 + } + } + }, + "CosiSign": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "data": { + "type": "bytes", + "id": 2 + }, + "global_commitment": { + "type": "bytes", + "id": 3 + }, + "global_pubkey": { + "type": "bytes", + "id": 4 + } + } + }, + "CosiSignature": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "BatchGetPublickeys": { + "fields": { + "ecdsa_curve_name": { + "type": "string", + "id": 1, + "options": { + "default": "ed25519" + } + }, + "paths": { + "rule": "repeated", + "type": "Path", + "id": 2 + }, + "include_node": { + "type": "bool", + "id": 3, + "options": { + "default": false + } + } + }, + "nested": { + "Path": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + } + } + } + } + }, + "EcdsaPublicKeys": { + "fields": { + "public_keys": { + "rule": "repeated", + "type": "bytes", + "id": 1 + }, + "hd_nodes": { + "rule": "repeated", + "type": "HDNodeType", + "id": 2 + }, + "root_fingerprint": { + "type": "uint32", + "id": 3 + } + } + }, + "EosGetPublicKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "EosPublicKey": { + "fields": { + "wif_public_key": { + "rule": "required", + "type": "string", + "id": 1 + }, + "raw_public_key": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "EosSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "chain_id": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "header": { + "rule": "required", + "type": "EosTxHeader", + "id": 3 + }, + "num_actions": { + "rule": "required", + "type": "uint32", + "id": 4 + } + }, + "nested": { + "EosTxHeader": { + "fields": { + "expiration": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "ref_block_num": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "ref_block_prefix": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "max_net_usage_words": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "max_cpu_usage_ms": { + "rule": "required", + "type": "uint32", + "id": 5 + }, + "delay_sec": { + "rule": "required", + "type": "uint32", + "id": 6 + } + } + } + } + }, + "EosTxActionRequest": { + "fields": { + "data_size": { + "type": "uint32", + "id": 1 + } + } + }, + "EosTxActionAck": { + "fields": { + "common": { + "rule": "required", + "type": "EosActionCommon", + "id": 1 + }, + "transfer": { + "type": "EosActionTransfer", + "id": 2 + }, + "delegate": { + "type": "EosActionDelegate", + "id": 3 + }, + "undelegate": { + "type": "EosActionUndelegate", + "id": 4 + }, + "refund": { + "type": "EosActionRefund", + "id": 5 + }, + "buy_ram": { + "type": "EosActionBuyRam", + "id": 6 + }, + "buy_ram_bytes": { + "type": "EosActionBuyRamBytes", + "id": 7 + }, + "sell_ram": { + "type": "EosActionSellRam", + "id": 8 + }, + "vote_producer": { + "type": "EosActionVoteProducer", + "id": 9 + }, + "update_auth": { + "type": "EosActionUpdateAuth", + "id": 10 + }, + "delete_auth": { + "type": "EosActionDeleteAuth", + "id": 11 + }, + "link_auth": { + "type": "EosActionLinkAuth", + "id": 12 + }, + "unlink_auth": { + "type": "EosActionUnlinkAuth", + "id": 13 + }, + "new_account": { + "type": "EosActionNewAccount", + "id": 14 + }, + "unknown": { + "type": "EosActionUnknown", + "id": 15 + } + }, + "nested": { + "EosAsset": { + "fields": { + "amount": { + "rule": "required", + "type": "sint64", + "id": 1 + }, + "symbol": { + "rule": "required", + "type": "uint64", + "id": 2 + } + } + }, + "EosPermissionLevel": { + "fields": { + "actor": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "permission": { + "rule": "required", + "type": "uint64", + "id": 2 + } + } + }, + "EosAuthorizationKey": { + "fields": { + "type": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "key": { + "type": "bytes", + "id": 2 + }, + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 3, + "options": { + "packed": false + } + }, + "weight": { + "rule": "required", + "type": "uint32", + "id": 4 + } + } + }, + "EosAuthorizationAccount": { + "fields": { + "account": { + "rule": "required", + "type": "EosPermissionLevel", + "id": 1 + }, + "weight": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + }, + "EosAuthorizationWait": { + "fields": { + "wait_sec": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "weight": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + }, + "EosAuthorization": { + "fields": { + "threshold": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "keys": { + "rule": "repeated", + "type": "EosAuthorizationKey", + "id": 2 + }, + "accounts": { + "rule": "repeated", + "type": "EosAuthorizationAccount", + "id": 3 + }, + "waits": { + "rule": "repeated", + "type": "EosAuthorizationWait", + "id": 4 + } + } + }, + "EosActionCommon": { + "fields": { + "account": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "name": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "authorization": { + "rule": "repeated", + "type": "EosPermissionLevel", + "id": 3 + } + } + }, + "EosActionTransfer": { + "fields": { + "sender": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "receiver": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "quantity": { + "rule": "required", + "type": "EosAsset", + "id": 3 + }, + "memo": { + "rule": "required", + "type": "string", + "id": 4 + } + } + }, + "EosActionDelegate": { + "fields": { + "sender": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "receiver": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "net_quantity": { + "rule": "required", + "type": "EosAsset", + "id": 3 + }, + "cpu_quantity": { + "rule": "required", + "type": "EosAsset", + "id": 4 + }, + "transfer": { + "rule": "required", + "type": "bool", + "id": 5 + } + } + }, + "EosActionUndelegate": { + "fields": { + "sender": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "receiver": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "net_quantity": { + "rule": "required", + "type": "EosAsset", + "id": 3 + }, + "cpu_quantity": { + "rule": "required", + "type": "EosAsset", + "id": 4 + } + } + }, + "EosActionRefund": { + "fields": { + "owner": { + "rule": "required", + "type": "uint64", + "id": 1 + } + } + }, + "EosActionBuyRam": { + "fields": { + "payer": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "receiver": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "quantity": { + "rule": "required", + "type": "EosAsset", + "id": 3 + } + } + }, + "EosActionBuyRamBytes": { + "fields": { + "payer": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "receiver": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "bytes": { + "rule": "required", + "type": "uint32", + "id": 3 + } + } + }, + "EosActionSellRam": { + "fields": { + "account": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "bytes": { + "rule": "required", + "type": "uint64", + "id": 2 + } + } + }, + "EosActionVoteProducer": { + "fields": { + "voter": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "proxy": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "producers": { + "rule": "repeated", + "type": "uint64", + "id": 3, + "options": { + "packed": false + } + } + } + }, + "EosActionUpdateAuth": { + "fields": { + "account": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "permission": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "parent": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "auth": { + "rule": "required", + "type": "EosAuthorization", + "id": 4 + } + } + }, + "EosActionDeleteAuth": { + "fields": { + "account": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "permission": { + "rule": "required", + "type": "uint64", + "id": 2 + } + } + }, + "EosActionLinkAuth": { + "fields": { + "account": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "code": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "type": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "requirement": { + "rule": "required", + "type": "uint64", + "id": 4 + } + } + }, + "EosActionUnlinkAuth": { + "fields": { + "account": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "code": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "type": { + "rule": "required", + "type": "uint64", + "id": 3 + } + } + }, + "EosActionNewAccount": { + "fields": { + "creator": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "name": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "owner": { + "rule": "required", + "type": "EosAuthorization", + "id": 3 + }, + "active": { + "rule": "required", + "type": "EosAuthorization", + "id": 4 + } + } + }, + "EosActionUnknown": { + "fields": { + "data_size": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "data_chunk": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + } + } + }, + "EosSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "EthereumGetPublicKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "EthereumPublicKey": { + "fields": { + "node": { + "rule": "required", + "type": "HDNodeType", + "id": 1 + }, + "xpub": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "EthereumGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + }, + "encoded_network": { + "type": "bytes", + "id": 3 + } + } + }, + "EthereumAddress": { + "fields": { + "_old_address": { + "type": "bytes", + "id": 1, + "options": { + "deprecated": true + } + }, + "address": { + "type": "string", + "id": 2 + } + } + }, + "EthereumSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "nonce": { + "type": "bytes", + "id": 2, + "options": { + "default": "" + } + }, + "gas_price": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "gas_limit": { + "rule": "required", + "type": "bytes", + "id": 4 + }, + "to": { + "type": "string", + "id": 11, + "options": { + "default": "" + } + }, + "value": { + "type": "bytes", + "id": 6, + "options": { + "default": "" + } + }, + "data_initial_chunk": { + "type": "bytes", + "id": 7, + "options": { + "default": "" + } + }, + "data_length": { + "type": "uint32", + "id": 8, + "options": { + "default": 0 + } + }, + "chain_id": { + "rule": "required", + "type": "uint64", + "id": 9 + }, + "tx_type": { + "type": "uint32", + "id": 10 + }, + "definitions": { + "type": "EthereumDefinitions", + "id": 12 + } + } + }, + "EthereumSignTxEIP1559": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "nonce": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "max_gas_fee": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "max_priority_fee": { + "rule": "required", + "type": "bytes", + "id": 4 + }, + "gas_limit": { + "rule": "required", + "type": "bytes", + "id": 5 + }, + "to": { + "type": "string", + "id": 6, + "options": { + "default": "" + } + }, + "value": { + "rule": "required", + "type": "bytes", + "id": 7 + }, + "data_initial_chunk": { + "type": "bytes", + "id": 8, + "options": { + "default": "" + } + }, + "data_length": { + "rule": "required", + "type": "uint32", + "id": 9 + }, + "chain_id": { + "rule": "required", + "type": "uint64", + "id": 10 + }, + "access_list": { + "rule": "repeated", + "type": "EthereumAccessList", + "id": 11 + }, + "definitions": { + "type": "EthereumDefinitions", + "id": 12 + } + }, + "nested": { + "EthereumAccessList": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + }, + "storage_keys": { + "rule": "repeated", + "type": "bytes", + "id": 2 + } + } + } + } + }, + "EthereumTxRequest": { + "fields": { + "data_length": { + "type": "uint32", + "id": 1 + }, + "signature_v": { + "type": "uint32", + "id": 2 + }, + "signature_r": { + "type": "bytes", + "id": 3 + }, + "signature_s": { + "type": "bytes", + "id": 4 + } + } + }, + "EthereumTxAck": { + "fields": { + "data_chunk": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "EthereumSignMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "encoded_network": { + "type": "bytes", + "id": 3 + } + } + }, + "EthereumMessageSignature": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "address": { + "rule": "required", + "type": "string", + "id": 3 + } + } + }, + "EthereumVerifyMessage": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "address": { + "rule": "required", + "type": "string", + "id": 4 + } + } + }, + "EthereumSignTypedHash": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "domain_separator_hash": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "message_hash": { + "type": "bytes", + "id": 3 + }, + "encoded_network": { + "type": "bytes", + "id": 4 + } + } + }, + "EthereumTypedDataSignature": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "address": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "EthereumDefinitionType": { + "values": { + "NETWORK": 0, + "TOKEN": 1 + } + }, + "EthereumNetworkInfo": { + "fields": { + "chain_id": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "symbol": { + "rule": "required", + "type": "string", + "id": 2 + }, + "slip44": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "name": { + "rule": "required", + "type": "string", + "id": 4 + }, + "icon": { + "type": "string", + "id": 101 + }, + "primary_color": { + "type": "uint64", + "id": 102 + } + } + }, + "EthereumTokenInfo": { + "fields": { + "address": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "chain_id": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "symbol": { + "rule": "required", + "type": "string", + "id": 3 + }, + "decimals": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "name": { + "rule": "required", + "type": "string", + "id": 5 + } + } + }, + "EthereumDefinitions": { + "fields": { + "encoded_network": { + "type": "bytes", + "id": 1 + }, + "encoded_token": { + "type": "bytes", + "id": 2 + } + } + }, + "EthereumSignTypedData": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "primary_type": { + "rule": "required", + "type": "string", + "id": 2 + }, + "metamask_v4_compat": { + "type": "bool", + "id": 3, + "options": { + "default": true + } + }, + "definitions": { + "type": "EthereumDefinitions", + "id": 4 + } + } + }, + "EthereumTypedDataStructRequest": { + "fields": { + "name": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "EthereumTypedDataStructAck": { + "fields": { + "members": { + "rule": "repeated", + "type": "EthereumStructMember", + "id": 1 + } + }, + "nested": { + "EthereumStructMember": { + "fields": { + "type": { + "rule": "required", + "type": "EthereumFieldType", + "id": 1 + }, + "name": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "EthereumFieldType": { + "fields": { + "data_type": { + "rule": "required", + "type": "EthereumDataType", + "id": 1 + }, + "size": { + "type": "uint32", + "id": 2 + }, + "entry_type": { + "type": "EthereumFieldType", + "id": 3 + }, + "struct_name": { + "type": "string", + "id": 4 + } + } + }, + "EthereumDataType": { + "values": { + "UINT": 1, + "INT": 2, + "BYTES": 3, + "STRING": 4, + "BOOL": 5, + "ADDRESS": 6, + "ARRAY": 7, + "STRUCT": 8 + } + } + } + }, + "EthereumTypedDataValueRequest": { + "fields": { + "member_path": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + } + } + }, + "EthereumTypedDataValueAck": { + "fields": { + "value": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "EthereumSignTypedDataOneKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "primary_type": { + "rule": "required", + "type": "string", + "id": 2 + }, + "metamask_v4_compat": { + "type": "bool", + "id": 3, + "options": { + "default": true + } + }, + "chain_id": { + "type": "uint64", + "id": 4 + } + } + }, + "EthereumTypedDataStructRequestOneKey": { + "fields": { + "name": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "EthereumTypedDataStructAckOneKey": { + "fields": { + "members": { + "rule": "repeated", + "type": "EthereumStructMemberOneKey", + "id": 1 + } + }, + "nested": { + "EthereumStructMemberOneKey": { + "fields": { + "type": { + "rule": "required", + "type": "EthereumFieldTypeOneKey", + "id": 1 + }, + "name": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "EthereumFieldTypeOneKey": { + "fields": { + "data_type": { + "rule": "required", + "type": "EthereumDataTypeOneKey", + "id": 1 + }, + "size": { + "type": "uint32", + "id": 2 + }, + "entry_type": { + "type": "EthereumFieldTypeOneKey", + "id": 3 + }, + "struct_name": { + "type": "string", + "id": 4 + } + } + }, + "EthereumDataTypeOneKey": { + "values": { + "UINT": 1, + "INT": 2, + "BYTES": 3, + "STRING": 4, + "BOOL": 5, + "ADDRESS": 6, + "ARRAY": 7, + "STRUCT": 8 + } + } + } + }, + "EthereumTypedDataValueRequestOneKey": { + "fields": { + "member_path": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + } + } + }, + "EthereumTypedDataValueAckOneKey": { + "fields": { + "value": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "EthereumGnosisSafeTxOperation": { + "values": { + "CALL": 0, + "DELEGATE_CALL": 1 + } + }, + "EthereumGnosisSafeTxRequest": { + "fields": {} + }, + "EthereumGnosisSafeTxAck": { + "fields": { + "to": { + "rule": "required", + "type": "string", + "id": 1 + }, + "value": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "data": { + "type": "bytes", + "id": 3 + }, + "operation": { + "rule": "required", + "type": "EthereumGnosisSafeTxOperation", + "id": 4 + }, + "safeTxGas": { + "rule": "required", + "type": "bytes", + "id": 5 + }, + "baseGas": { + "rule": "required", + "type": "bytes", + "id": 6 + }, + "gasPrice": { + "rule": "required", + "type": "bytes", + "id": 7 + }, + "gasToken": { + "rule": "required", + "type": "string", + "id": 8 + }, + "refundReceiver": { + "rule": "required", + "type": "string", + "id": 9 + }, + "nonce": { + "rule": "required", + "type": "bytes", + "id": 10 + }, + "chain_id": { + "rule": "required", + "type": "uint64", + "id": 11 + }, + "verifyingContract": { + "rule": "required", + "type": "string", + "id": 12 + } + } + }, + "EthereumSignTypedDataQR": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "json_data": { + "type": "bytes", + "id": 2 + }, + "chain_id": { + "type": "uint64", + "id": 3 + }, + "metamask_v4_compat": { + "type": "bool", + "id": 4, + "options": { + "default": true + } + }, + "request_id": { + "type": "bytes", + "id": 5 + } + } + }, + "EthereumGetPublicKeyOneKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + }, + "chain_id": { + "type": "uint64", + "id": 3 + } + } + }, + "EthereumPublicKeyOneKey": { + "fields": { + "node": { + "rule": "required", + "type": "HDNodeType", + "id": 1 + }, + "xpub": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "EthereumGetAddressOneKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + }, + "chain_id": { + "type": "uint64", + "id": 3 + } + } + }, + "EthereumAddressOneKey": { + "fields": { + "_old_address": { + "type": "bytes", + "id": 1, + "options": { + "deprecated": true + } + }, + "address": { + "type": "string", + "id": 2 + } + } + }, + "EthereumSignTxOneKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "nonce": { + "type": "bytes", + "id": 2, + "options": { + "default": "" + } + }, + "gas_price": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "gas_limit": { + "rule": "required", + "type": "bytes", + "id": 4 + }, + "to": { + "type": "string", + "id": 11, + "options": { + "default": "" + } + }, + "value": { + "type": "bytes", + "id": 6, + "options": { + "default": "" + } + }, + "data_initial_chunk": { + "type": "bytes", + "id": 7, + "options": { + "default": "" + } + }, + "data_length": { + "type": "uint32", + "id": 8, + "options": { + "default": 0 + } + }, + "chain_id": { + "rule": "required", + "type": "uint64", + "id": 9 + }, + "tx_type": { + "type": "uint32", + "id": 10 + } + } + }, + "EthereumAccessListOneKey": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + }, + "storage_keys": { + "rule": "repeated", + "type": "bytes", + "id": 2 + } + } + }, + "EthereumSignTxEIP1559OneKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "nonce": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "max_gas_fee": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "max_priority_fee": { + "rule": "required", + "type": "bytes", + "id": 4 + }, + "gas_limit": { + "rule": "required", + "type": "bytes", + "id": 5 + }, + "to": { + "type": "string", + "id": 6, + "options": { + "default": "" + } + }, + "value": { + "rule": "required", + "type": "bytes", + "id": 7 + }, + "data_initial_chunk": { + "type": "bytes", + "id": 8, + "options": { + "default": "" + } + }, + "data_length": { + "rule": "required", + "type": "uint32", + "id": 9 + }, + "chain_id": { + "rule": "required", + "type": "uint64", + "id": 10 + }, + "access_list": { + "rule": "repeated", + "type": "EthereumAccessListOneKey", + "id": 11 + } + } + }, + "EthereumAuthorizationSignature": { + "fields": { + "y_parity": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "r": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "s": { + "rule": "required", + "type": "bytes", + "id": 3 + } + } + }, + "EthereumSignTxEIP7702OneKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "nonce": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "max_gas_fee": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "max_priority_fee": { + "rule": "required", + "type": "bytes", + "id": 4 + }, + "gas_limit": { + "rule": "required", + "type": "bytes", + "id": 5 + }, + "to": { + "rule": "required", + "type": "string", + "id": 6 + }, + "value": { + "rule": "required", + "type": "bytes", + "id": 7 + }, + "data_initial_chunk": { + "type": "bytes", + "id": 8, + "options": { + "default": "" + } + }, + "data_length": { + "rule": "required", + "type": "uint32", + "id": 9 + }, + "chain_id": { + "rule": "required", + "type": "uint64", + "id": 10 + }, + "access_list": { + "rule": "repeated", + "type": "EthereumAccessListOneKey", + "id": 11 + }, + "authorization_list": { + "rule": "repeated", + "type": "EthereumAuthorizationOneKey", + "id": 12 + } + }, + "nested": { + "EthereumAuthorizationOneKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "chain_id": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "address": { + "rule": "required", + "type": "string", + "id": 3 + }, + "nonce": { + "rule": "required", + "type": "bytes", + "id": 4 + }, + "signature": { + "type": "EthereumAuthorizationSignature", + "id": 5 + } + } + } + } + }, + "EthereumTxRequestOneKey": { + "fields": { + "data_length": { + "type": "uint32", + "id": 1 + }, + "signature_v": { + "type": "uint32", + "id": 2 + }, + "signature_r": { + "type": "bytes", + "id": 3 + }, + "signature_s": { + "type": "bytes", + "id": 4 + }, + "authorization_signatures": { + "rule": "repeated", + "type": "EthereumAuthorizationSignature", + "id": 10 + } + } + }, + "EthereumTxAckOneKey": { + "fields": { + "data_chunk": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "EthereumSignMessageOneKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "chain_id": { + "type": "uint64", + "id": 3 + } + } + }, + "EthereumMessageSignatureOneKey": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "address": { + "rule": "required", + "type": "string", + "id": 3 + } + } + }, + "EthereumVerifyMessageOneKey": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "address": { + "rule": "required", + "type": "string", + "id": 4 + }, + "chain_id": { + "type": "uint64", + "id": 5 + } + } + }, + "EthereumSignTypedHashOneKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "domain_separator_hash": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "message_hash": { + "type": "bytes", + "id": 3 + }, + "chain_id": { + "type": "uint64", + "id": 4 + } + } + }, + "EthereumTypedDataSignatureOneKey": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "address": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "FilecoinGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + }, + "testnet": { + "type": "bool", + "id": 3 + } + } + }, + "FilecoinAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + } + } + }, + "FilecoinSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "testnet": { + "type": "bool", + "id": 3 + } + } + }, + "FilecoinSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "InternalMyAddressRequest": { + "fields": { + "coin_type": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "chain_id": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "account_index": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "derive_type": { + "rule": "required", + "type": "uint32", + "id": 4 + } + } + }, + "KaspaGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + }, + "prefix": { + "type": "string", + "id": 3, + "options": { + "default": "kaspa" + } + }, + "scheme": { + "type": "string", + "id": 4, + "options": { + "default": "schnorr" + } + }, + "use_tweak": { + "type": "bool", + "id": 5, + "options": { + "default": true + } + } + } + }, + "KaspaAddress": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "KaspaSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_message": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "scheme": { + "type": "string", + "id": 3, + "options": { + "default": "schnorr" + } + }, + "prefix": { + "type": "string", + "id": 4, + "options": { + "default": "kaspa" + } + }, + "input_count": { + "type": "uint32", + "id": 5, + "options": { + "default": 1 + } + }, + "use_tweak": { + "type": "bool", + "id": 6, + "options": { + "default": true + } + } + } + }, + "KaspaTxInputRequest": { + "fields": { + "request_index": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "signature": { + "type": "bytes", + "id": 2 + } + } + }, + "KaspaTxInputAck": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_message": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "KaspaSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "LnurlAuth": { + "fields": { + "domain": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "data": { + "rule": "required", + "type": "bytes", + "id": 3 + } + } + }, + "LnurlAuthResp": { + "fields": { + "publickey": { + "type": "string", + "id": 1 + }, + "path": { + "type": "string", + "id": 2 + }, + "signature": { + "type": "bytes", + "id": 3 + } + } + }, + "BackupType": { + "values": { + "Bip39": 0, + "Slip39_Basic": 1, + "Slip39_Advanced": 2, + "Slip39_Single_Extendable": 3, + "Slip39_Basic_Extendable": 4, + "Slip39_Advanced_Extendable": 5 + } + }, + "SafetyCheckLevel": { + "values": { + "Strict": 0, + "PromptAlways": 1, + "PromptTemporarily": 2 + } + }, + "Initialize": { + "fields": { + "session_id": { + "type": "bytes", + "id": 1 + }, + "_skip_passphrase": { + "type": "bool", + "id": 2, + "options": { + "deprecated": true + } + }, + "derive_cardano": { + "type": "bool", + "id": 3 + }, + "passphrase_state": { + "type": "string", + "id": 8000 + }, + "is_contains_attach": { + "type": "bool", + "id": 8001 + } + } + }, + "StartSession": { + "fields": { + "session_id": { + "type": "bytes", + "id": 1 + }, + "_skip_passphrase": { + "type": "bool", + "id": 2, + "options": { + "deprecated": true + } + }, + "derive_cardano": { + "type": "bool", + "id": 3 + } + } + }, + "EndSession": { + "fields": {} + }, + "GetFeatures": { + "fields": {} + }, + "OnekeyGetFeatures": { + "fields": {} + }, + "OneKeyDeviceType": { + "values": { + "CLASSIC": 0, + "CLASSIC1S": 1, + "MINI": 2, + "TOUCH": 3, + "PRO": 5 + } + }, + "OneKeySeType": { + "values": { + "THD89": 0, + "SE608A": 1 + } + }, + "OneKeySEState": { + "values": { + "BOOT": 0, + "APP": 1 + } + }, + "Features": { + "fields": { + "vendor": { + "type": "string", + "id": 1 + }, + "major_version": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "minor_version": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "patch_version": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "bootloader_mode": { + "type": "bool", + "id": 5 + }, + "device_id": { + "type": "string", + "id": 6 + }, + "pin_protection": { + "type": "bool", + "id": 7 + }, + "passphrase_protection": { + "type": "bool", + "id": 8 + }, + "language": { + "type": "string", + "id": 9 + }, + "label": { + "type": "string", + "id": 10 + }, + "initialized": { + "type": "bool", + "id": 12 + }, + "revision": { + "type": "bytes", + "id": 13 + }, + "bootloader_hash": { + "type": "bytes", + "id": 14 + }, + "imported": { + "type": "bool", + "id": 15 + }, + "unlocked": { + "type": "bool", + "id": 16 + }, + "_passphrase_cached": { + "type": "bool", + "id": 17, + "options": { + "deprecated": true + } + }, + "firmware_present": { + "type": "bool", + "id": 18 + }, + "needs_backup": { + "type": "bool", + "id": 19 + }, + "flags": { + "type": "uint32", + "id": 20 + }, + "model": { + "type": "string", + "id": 21 + }, + "fw_major": { + "type": "uint32", + "id": 22 + }, + "fw_minor": { + "type": "uint32", + "id": 23 + }, + "fw_patch": { + "type": "uint32", + "id": 24 + }, + "fw_vendor": { + "type": "string", + "id": 25 + }, + "unfinished_backup": { + "type": "bool", + "id": 27 + }, + "no_backup": { + "type": "bool", + "id": 28 + }, + "recovery_mode": { + "type": "bool", + "id": 29 + }, + "capabilities": { + "rule": "repeated", + "type": "Capability", + "id": 30, + "options": { + "packed": false + } + }, + "backup_type": { + "type": "BackupType", + "id": 31 + }, + "sd_card_present": { + "type": "bool", + "id": 32 + }, + "sd_protection": { + "type": "bool", + "id": 33 + }, + "wipe_code_protection": { + "type": "bool", + "id": 34 + }, + "session_id": { + "type": "bytes", + "id": 35 + }, + "passphrase_always_on_device": { + "type": "bool", + "id": 36 + }, + "safety_checks": { + "type": "SafetyCheckLevel", + "id": 37 + }, + "auto_lock_delay_ms": { + "type": "uint32", + "id": 38 + }, + "display_rotation": { + "type": "uint32", + "id": 39 + }, + "experimental_features": { + "type": "bool", + "id": 40 + }, + "offset": { + "type": "uint32", + "id": 500 + }, + "coprocessor_bt_name": { + "type": "string", + "id": 501 + }, + "coprocessor_version": { + "type": "string", + "id": 502 + }, + "coprocessor_bt_enable": { + "type": "bool", + "id": 503 + }, + "se_enable": { + "type": "bool", + "id": 504 + }, + "se_ver": { + "type": "string", + "id": 506 + }, + "backup_only": { + "type": "bool", + "id": 507 + }, + "onekey_version": { + "type": "string", + "id": 508 + }, + "onekey_serial": { + "type": "string", + "id": 509 + }, + "bootloader_version": { + "type": "string", + "id": 510 + }, + "serial_no": { + "type": "string", + "id": 511 + }, + "spi_flash": { + "type": "string", + "id": 512 + }, + "initstates": { + "type": "uint32", + "id": 513 + }, + "NFT_voucher": { + "type": "bytes", + "id": 514 + }, + "cpu_info": { + "type": "string", + "id": 515 + }, + "pre_firmware": { + "type": "string", + "id": 516 + }, + "coin_switch": { + "type": "uint32", + "id": 517 + }, + "build_id": { + "type": "bytes", + "id": 518 + }, + "romloader_version": { + "type": "string", + "id": 519 + }, + "busy": { + "type": "bool", + "id": 41 + }, + "onekey_device_type": { + "type": "OneKeyDeviceType", + "id": 600 + }, + "onekey_se_type": { + "type": "OneKeySeType", + "id": 601 + }, + "onekey_romloader_version": { + "type": "string", + "id": 602 + }, + "onekey_romloader_hash": { + "type": "bytes", + "id": 603 + }, + "onekey_bootloader_version": { + "type": "string", + "id": 604 + }, + "onekey_bootloader_hash": { + "type": "bytes", + "id": 605 + }, + "onekey_se01_version": { + "type": "string", + "id": 606 + }, + "onekey_se01_hash": { + "type": "bytes", + "id": 607 + }, + "onekey_se01_build_id": { + "type": "string", + "id": 608 + }, + "onekey_firmware_version": { + "type": "string", + "id": 609 + }, + "onekey_firmware_hash": { + "type": "bytes", + "id": 610 + }, + "onekey_firmware_build_id": { + "type": "string", + "id": 611 + }, + "onekey_serial_no": { + "type": "string", + "id": 612 + }, + "onekey_bootloader_build_id": { + "type": "string", + "id": 613 + }, + "onekey_coprocessor_bt_name": { + "type": "string", + "id": 614 + }, + "onekey_coprocessor_version": { + "type": "string", + "id": 615 + }, + "onekey_coprocessor_build_id": { + "type": "string", + "id": 616 + }, + "onekey_coprocessor_hash": { + "type": "bytes", + "id": 617 + }, + "onekey_se02_version": { + "type": "string", + "id": 618 + }, + "onekey_se03_version": { + "type": "string", + "id": 619 + }, + "onekey_se04_version": { + "type": "string", + "id": 620 + }, + "onekey_se01_state": { + "type": "OneKeySEState", + "id": 621 + }, + "onekey_se02_state": { + "type": "OneKeySEState", + "id": 622 + }, + "onekey_se03_state": { + "type": "OneKeySEState", + "id": 623 + }, + "onekey_se04_state": { + "type": "OneKeySEState", + "id": 624 + }, + "attach_to_pin_user": { + "type": "bool", + "id": 625 + }, + "unlocked_attach_pin": { + "type": "bool", + "id": 626 + } + }, + "nested": { + "Capability": { + "options": { + "(has_bitcoin_only_values)": true + }, + "values": { + "Capability_Bitcoin": 1, + "Capability_Bitcoin_like": 2, + "Capability_Binance": 3, + "Capability_Cardano": 4, + "Capability_Crypto": 5, + "Capability_EOS": 6, + "Capability_Ethereum": 7, + "Capability_Lisk": 8, + "Capability_Monero": 9, + "Capability_NEM": 10, + "Capability_Ripple": 11, + "Capability_Stellar": 12, + "Capability_Tezos": 13, + "Capability_U2F": 14, + "Capability_Shamir": 15, + "Capability_ShamirGroups": 16, + "Capability_PassphraseEntry": 17, + "Capability_AttachToPin": 18, + "Capability_EthereumTypedData": 1000 + } + } + } + }, + "OnekeyFeatures": { + "fields": { + "onekey_device_type": { + "type": "OneKeyDeviceType", + "id": 1 + }, + "onekey_romloader_version": { + "type": "string", + "id": 2 + }, + "onekey_bootloader_version": { + "type": "string", + "id": 3 + }, + "onekey_firmware_version": { + "type": "string", + "id": 4 + }, + "onekey_romloader_hash": { + "type": "bytes", + "id": 5 + }, + "onekey_bootloader_hash": { + "type": "bytes", + "id": 6 + }, + "onekey_firmware_hash": { + "type": "bytes", + "id": 7 + }, + "onekey_romloader_build_id": { + "type": "string", + "id": 8 + }, + "onekey_bootloader_build_id": { + "type": "string", + "id": 9 + }, + "onekey_firmware_build_id": { + "type": "string", + "id": 10 + }, + "onekey_serial_no": { + "type": "string", + "id": 11 + }, + "onekey_coprocessor_bt_name": { + "type": "string", + "id": 12 + }, + "onekey_coprocessor_version": { + "type": "string", + "id": 13 + }, + "onekey_coprocessor_build_id": { + "type": "string", + "id": 14 + }, + "onekey_coprocessor_hash": { + "type": "bytes", + "id": 15 + }, + "onekey_se_type": { + "type": "OneKeySeType", + "id": 16 + }, + "onekey_se01_state": { + "type": "OneKeySEState", + "id": 17 + }, + "onekey_se02_state": { + "type": "OneKeySEState", + "id": 18 + }, + "onekey_se03_state": { + "type": "OneKeySEState", + "id": 19 + }, + "onekey_se04_state": { + "type": "OneKeySEState", + "id": 20 + }, + "onekey_se01_version": { + "type": "string", + "id": 21 + }, + "onekey_se02_version": { + "type": "string", + "id": 22 + }, + "onekey_se03_version": { + "type": "string", + "id": 23 + }, + "onekey_se04_version": { + "type": "string", + "id": 24 + }, + "onekey_se01_hash": { + "type": "bytes", + "id": 25 + }, + "onekey_se02_hash": { + "type": "bytes", + "id": 26 + }, + "onekey_se03_hash": { + "type": "bytes", + "id": 27 + }, + "onekey_se04_hash": { + "type": "bytes", + "id": 28 + }, + "onekey_se01_build_id": { + "type": "string", + "id": 29 + }, + "onekey_se02_build_id": { + "type": "string", + "id": 30 + }, + "onekey_se03_build_id": { + "type": "string", + "id": 31 + }, + "onekey_se04_build_id": { + "type": "string", + "id": 32 + }, + "onekey_se01_bootloader_version": { + "type": "string", + "id": 33 + }, + "onekey_se02_bootloader_version": { + "type": "string", + "id": 34 + }, + "onekey_se03_bootloader_version": { + "type": "string", + "id": 35 + }, + "onekey_se04_bootloader_version": { + "type": "string", + "id": 36 + }, + "onekey_se01_bootloader_hash": { + "type": "bytes", + "id": 37 + }, + "onekey_se02_bootloader_hash": { + "type": "bytes", + "id": 38 + }, + "onekey_se03_bootloader_hash": { + "type": "bytes", + "id": 39 + }, + "onekey_se04_bootloader_hash": { + "type": "bytes", + "id": 40 + }, + "onekey_se01_bootloader_build_id": { + "type": "string", + "id": 41 + }, + "onekey_se02_bootloader_build_id": { + "type": "string", + "id": 42 + }, + "onekey_se03_bootloader_build_id": { + "type": "string", + "id": 43 + }, + "onekey_se04_bootloader_build_id": { + "type": "string", + "id": 44 + } + } + }, + "LockDevice": { + "fields": {} + }, + "SetBusy": { + "fields": { + "expiry_ms": { + "type": "uint32", + "id": 1 + } + } + }, + "ApplySettings": { + "fields": { + "language": { + "type": "string", + "id": 1 + }, + "label": { + "type": "string", + "id": 2 + }, + "use_passphrase": { + "type": "bool", + "id": 3 + }, + "homescreen": { + "type": "bytes", + "id": 4 + }, + "_passphrase_source": { + "type": "uint32", + "id": 5, + "options": { + "deprecated": true + } + }, + "auto_lock_delay_ms": { + "type": "uint32", + "id": 6 + }, + "display_rotation": { + "type": "uint32", + "id": 7 + }, + "passphrase_always_on_device": { + "type": "bool", + "id": 8 + }, + "safety_checks": { + "type": "SafetyCheckLevel", + "id": 9 + }, + "experimental_features": { + "type": "bool", + "id": 10 + } + } + }, + "ApplyFlags": { + "fields": { + "flags": { + "rule": "required", + "type": "uint32", + "id": 1 + } + } + }, + "ChangePin": { + "fields": { + "remove": { + "type": "bool", + "id": 1 + } + } + }, + "ChangeWipeCode": { + "fields": { + "remove": { + "type": "bool", + "id": 1 + } + } + }, + "SdProtect": { + "fields": { + "operation": { + "rule": "required", + "type": "SdProtectOperationType", + "id": 1 + } + }, + "nested": { + "SdProtectOperationType": { + "values": { + "DISABLE": 0, + "ENABLE": 1, + "REFRESH": 2 + } + } + } + }, + "Cancel": { + "fields": {} + }, + "GetEntropy": { + "fields": { + "size": { + "rule": "required", + "type": "uint32", + "id": 1 + } + } + }, + "Entropy": { + "fields": { + "entropy": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "GetFirmwareHash": { + "fields": { + "challenge": { + "type": "bytes", + "id": 1 + } + } + }, + "FirmwareHash": { + "fields": { + "hash": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "WipeDevice": { + "fields": {} + }, + "LoadDevice": { + "fields": { + "mnemonics": { + "rule": "repeated", + "type": "string", + "id": 1 + }, + "pin": { + "type": "string", + "id": 3 + }, + "passphrase_protection": { + "type": "bool", + "id": 4 + }, + "language": { + "type": "string", + "id": 5, + "options": { + "default": "en-US" + } + }, + "label": { + "type": "string", + "id": 6 + }, + "skip_checksum": { + "type": "bool", + "id": 7 + }, + "u2f_counter": { + "type": "uint32", + "id": 8 + }, + "needs_backup": { + "type": "bool", + "id": 9 + }, + "no_backup": { + "type": "bool", + "id": 10 + } + } + }, + "ResetDevice": { + "fields": { + "display_random": { + "type": "bool", + "id": 1 + }, + "strength": { + "type": "uint32", + "id": 2, + "options": { + "default": 256 + } + }, + "passphrase_protection": { + "type": "bool", + "id": 3 + }, + "pin_protection": { + "type": "bool", + "id": 4 + }, + "language": { + "type": "string", + "id": 5, + "options": { + "default": "en-US" + } + }, + "label": { + "type": "string", + "id": 6 + }, + "u2f_counter": { + "type": "uint32", + "id": 7 + }, + "skip_backup": { + "type": "bool", + "id": 8 + }, + "no_backup": { + "type": "bool", + "id": 9 + }, + "backup_type": { + "type": "BackupType", + "id": 10, + "options": { + "default": "Bip39" + } + } + } + }, + "BackupDevice": { + "fields": {} + }, + "EntropyRequest": { + "fields": {} + }, + "EntropyAck": { + "fields": { + "entropy": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "RecoveryDevice": { + "fields": { + "word_count": { + "type": "uint32", + "id": 1 + }, + "passphrase_protection": { + "type": "bool", + "id": 2 + }, + "pin_protection": { + "type": "bool", + "id": 3 + }, + "language": { + "type": "string", + "id": 4 + }, + "label": { + "type": "string", + "id": 5 + }, + "enforce_wordlist": { + "type": "bool", + "id": 6 + }, + "type": { + "type": "RecoveryDeviceType", + "id": 8 + }, + "u2f_counter": { + "type": "uint32", + "id": 9 + }, + "dry_run": { + "type": "bool", + "id": 10 + } + }, + "nested": { + "RecoveryDeviceType": { + "values": { + "RecoveryDeviceType_ScrambledWords": 0, + "RecoveryDeviceType_Matrix": 1 + } + } + } + }, + "WordRequest": { + "fields": { + "type": { + "rule": "required", + "type": "WordRequestType", + "id": 1 + } + }, + "nested": { + "WordRequestType": { + "values": { + "WordRequestType_Plain": 0, + "WordRequestType_Matrix9": 1, + "WordRequestType_Matrix6": 2 + } + } + } + }, + "WordAck": { + "fields": { + "word": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "SetU2FCounter": { + "fields": { + "u2f_counter": { + "rule": "required", + "type": "uint32", + "id": 1 + } + } + }, + "GetNextU2FCounter": { + "fields": {} + }, + "NextU2FCounter": { + "fields": { + "u2f_counter": { + "rule": "required", + "type": "uint32", + "id": 1 + } + } + }, + "DoPreauthorized": { + "fields": {} + }, + "PreauthorizedRequest": { + "fields": {} + }, + "CancelAuthorization": { + "fields": {} + }, + "GetNonce": { + "options": { + "(experimental_message)": true + }, + "fields": {} + }, + "Nonce": { + "options": { + "(experimental_message)": true + }, + "fields": { + "nonce": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "WriteSEPrivateKey": { + "fields": { + "private_key": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "ReadSEPublicKey": { + "fields": {} + }, + "SEPublicKey": { + "fields": { + "public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "WriteSEPublicCert": { + "fields": { + "public_cert": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "ReadSEPublicCert": { + "fields": {} + }, + "SEPublicCert": { + "fields": { + "public_cert": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "SESignMessage": { + "fields": { + "message": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "SEMessageSignature": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "ResourceUpload": { + "fields": { + "extension": { + "rule": "required", + "type": "string", + "id": 1 + }, + "data_length": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "res_type": { + "rule": "required", + "type": "ResourceType", + "id": 3 + }, + "nft_meta_data": { + "type": "bytes", + "id": 4 + }, + "zoom_data_length": { + "rule": "required", + "type": "uint32", + "id": 5 + }, + "file_name_no_ext": { + "type": "string", + "id": 6 + }, + "blur_data_length": { + "type": "uint32", + "id": 7 + } + }, + "nested": { + "ResourceType": { + "values": { + "WallPaper": 0, + "Nft": 1 + } + } + } + }, + "ZoomRequest": { + "fields": { + "offset": { + "type": "uint32", + "id": 1 + }, + "data_length": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + }, + "BlurRequest": { + "fields": { + "offset": { + "type": "uint32", + "id": 1 + }, + "data_length": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + }, + "ResourceRequest": { + "fields": { + "offset": { + "type": "uint32", + "id": 1 + }, + "data_length": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + }, + "ResourceAck": { + "fields": { + "data_chunk": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "hash": { + "type": "bytes", + "id": 2 + } + } + }, + "WallpaperTarget": { + "values": { + "Home": 0, + "Lock": 1 + } + }, + "SetWallpaper": { + "fields": { + "target": { + "rule": "required", + "type": "WallpaperTarget", + "id": 1 + }, + "path": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "GetWallpaper": { + "fields": { + "target": { + "rule": "required", + "type": "WallpaperTarget", + "id": 1 + } + } + }, + "Wallpaper": { + "fields": { + "target": { + "rule": "required", + "type": "WallpaperTarget", + "id": 1 + }, + "path": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "ResourceUpdate": { + "fields": { + "file_name": { + "rule": "required", + "type": "string", + "id": 1 + }, + "data_length": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "initial_data_chunk": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "hash": { + "type": "bytes", + "id": 4 + } + } + }, + "ListResDir": { + "fields": { + "path": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "FileInfoList": { + "fields": { + "files": { + "rule": "repeated", + "type": "FileInfo", + "id": 1 + } + }, + "nested": { + "FileInfo": { + "fields": { + "name": { + "rule": "required", + "type": "string", + "id": 1 + }, + "size": { + "rule": "required", + "type": "uint64", + "id": 2 + } + } + } + } + }, + "UnlockPath": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "mac": { + "type": "bytes", + "id": 2 + } + } + }, + "UnlockedPathRequest": { + "fields": { + "mac": { + "type": "bytes", + "id": 1 + } + } + }, + "GetPassphraseState": { + "fields": { + "passphrase_state": { + "type": "string", + "id": 1 + } + } + }, + "PassphraseState": { + "fields": { + "passphrase_state": { + "type": "string", + "id": 1 + }, + "session_id": { + "type": "bytes", + "id": 2 + }, + "unlocked_attach_pin": { + "type": "bool", + "id": 3 + } + } + }, + "UnLockDevice": { + "fields": {} + }, + "UnLockDeviceResponse": { + "fields": { + "unlocked": { + "type": "bool", + "id": 1 + }, + "unlocked_attach_pin": { + "type": "bool", + "id": 2 + }, + "passphrase_protection": { + "type": "bool", + "id": 3 + } + } + }, + "MoneroNetworkType": { + "values": { + "MAINNET": 0, + "TESTNET": 1, + "STAGENET": 2, + "FAKECHAIN": 3 + } + }, + "MoneroTransactionSourceEntry": { + "fields": { + "outputs": { + "rule": "repeated", + "type": "MoneroOutputEntry", + "id": 1 + }, + "real_output": { + "type": "uint64", + "id": 2 + }, + "real_out_tx_key": { + "type": "bytes", + "id": 3 + }, + "real_out_additional_tx_keys": { + "rule": "repeated", + "type": "bytes", + "id": 4 + }, + "real_output_in_tx_index": { + "type": "uint64", + "id": 5 + }, + "amount": { + "type": "uint64", + "id": 6 + }, + "rct": { + "type": "bool", + "id": 7 + }, + "mask": { + "type": "bytes", + "id": 8 + }, + "multisig_kLRki": { + "type": "MoneroMultisigKLRki", + "id": 9 + }, + "subaddr_minor": { + "type": "uint32", + "id": 10 + } + }, + "nested": { + "MoneroOutputEntry": { + "fields": { + "idx": { + "type": "uint64", + "id": 1 + }, + "key": { + "type": "MoneroRctKeyPublic", + "id": 2 + } + }, + "nested": { + "MoneroRctKeyPublic": { + "fields": { + "dest": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "commitment": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + } + } + }, + "MoneroMultisigKLRki": { + "fields": { + "K": { + "type": "bytes", + "id": 1 + }, + "L": { + "type": "bytes", + "id": 2 + }, + "R": { + "type": "bytes", + "id": 3 + }, + "ki": { + "type": "bytes", + "id": 4 + } + } + } + } + }, + "MoneroTransactionDestinationEntry": { + "fields": { + "amount": { + "type": "uint64", + "id": 1 + }, + "addr": { + "type": "MoneroAccountPublicAddress", + "id": 2 + }, + "is_subaddress": { + "type": "bool", + "id": 3 + }, + "original": { + "type": "bytes", + "id": 4 + }, + "is_integrated": { + "type": "bool", + "id": 5 + } + }, + "nested": { + "MoneroAccountPublicAddress": { + "fields": { + "spend_public_key": { + "type": "bytes", + "id": 1 + }, + "view_public_key": { + "type": "bytes", + "id": 2 + } + } + } + } + }, + "MoneroTransactionRsigData": { + "fields": { + "rsig_type": { + "type": "uint32", + "id": 1 + }, + "offload_type": { + "type": "uint32", + "id": 2 + }, + "grouping": { + "rule": "repeated", + "type": "uint64", + "id": 3, + "options": { + "packed": false + } + }, + "mask": { + "type": "bytes", + "id": 4 + }, + "rsig": { + "type": "bytes", + "id": 5 + }, + "rsig_parts": { + "rule": "repeated", + "type": "bytes", + "id": 6 + }, + "bp_version": { + "type": "uint32", + "id": 7 + } + } + }, + "MoneroGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + }, + "network_type": { + "type": "MoneroNetworkType", + "id": 3, + "options": { + "default": "MAINNET" + } + }, + "account": { + "type": "uint32", + "id": 4 + }, + "minor": { + "type": "uint32", + "id": 5 + }, + "payment_id": { + "type": "bytes", + "id": 6 + } + } + }, + "MoneroAddress": { + "fields": { + "address": { + "type": "bytes", + "id": 1 + } + } + }, + "MoneroGetWatchKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "network_type": { + "type": "MoneroNetworkType", + "id": 2, + "options": { + "default": "MAINNET" + } + } + } + }, + "MoneroWatchKey": { + "fields": { + "watch_key": { + "type": "bytes", + "id": 1 + }, + "address": { + "type": "bytes", + "id": 2 + } + } + }, + "MoneroTransactionInitRequest": { + "fields": { + "version": { + "type": "uint32", + "id": 1 + }, + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 2, + "options": { + "packed": false + } + }, + "network_type": { + "type": "MoneroNetworkType", + "id": 3, + "options": { + "default": "MAINNET" + } + }, + "tsx_data": { + "type": "MoneroTransactionData", + "id": 4 + } + }, + "nested": { + "MoneroTransactionData": { + "fields": { + "version": { + "type": "uint32", + "id": 1 + }, + "payment_id": { + "type": "bytes", + "id": 2 + }, + "unlock_time": { + "type": "uint64", + "id": 3 + }, + "outputs": { + "rule": "repeated", + "type": "MoneroTransactionDestinationEntry", + "id": 4 + }, + "change_dts": { + "type": "MoneroTransactionDestinationEntry", + "id": 5 + }, + "num_inputs": { + "type": "uint32", + "id": 6 + }, + "mixin": { + "type": "uint32", + "id": 7 + }, + "fee": { + "type": "uint64", + "id": 8 + }, + "account": { + "type": "uint32", + "id": 9 + }, + "minor_indices": { + "rule": "repeated", + "type": "uint32", + "id": 10, + "options": { + "packed": false + } + }, + "rsig_data": { + "type": "MoneroTransactionRsigData", + "id": 11 + }, + "integrated_indices": { + "rule": "repeated", + "type": "uint32", + "id": 12, + "options": { + "packed": false + } + }, + "client_version": { + "type": "uint32", + "id": 13 + }, + "hard_fork": { + "type": "uint32", + "id": 14 + }, + "monero_version": { + "type": "bytes", + "id": 15 + } + } + } + } + }, + "MoneroTransactionInitAck": { + "fields": { + "hmacs": { + "rule": "repeated", + "type": "bytes", + "id": 1 + }, + "rsig_data": { + "type": "MoneroTransactionRsigData", + "id": 2 + } + } + }, + "MoneroTransactionSetInputRequest": { + "fields": { + "src_entr": { + "type": "MoneroTransactionSourceEntry", + "id": 1 + } + } + }, + "MoneroTransactionSetInputAck": { + "fields": { + "vini": { + "type": "bytes", + "id": 1 + }, + "vini_hmac": { + "type": "bytes", + "id": 2 + }, + "pseudo_out": { + "type": "bytes", + "id": 3 + }, + "pseudo_out_hmac": { + "type": "bytes", + "id": 4 + }, + "pseudo_out_alpha": { + "type": "bytes", + "id": 5 + }, + "spend_key": { + "type": "bytes", + "id": 6 + } + } + }, + "MoneroTransactionInputViniRequest": { + "fields": { + "src_entr": { + "type": "MoneroTransactionSourceEntry", + "id": 1 + }, + "vini": { + "type": "bytes", + "id": 2 + }, + "vini_hmac": { + "type": "bytes", + "id": 3 + }, + "pseudo_out": { + "type": "bytes", + "id": 4 + }, + "pseudo_out_hmac": { + "type": "bytes", + "id": 5 + }, + "orig_idx": { + "type": "uint32", + "id": 6 + } + } + }, + "MoneroTransactionInputViniAck": { + "fields": {} + }, + "MoneroTransactionAllInputsSetRequest": { + "fields": {} + }, + "MoneroTransactionAllInputsSetAck": { + "fields": { + "rsig_data": { + "type": "MoneroTransactionRsigData", + "id": 1 + } + } + }, + "MoneroTransactionSetOutputRequest": { + "fields": { + "dst_entr": { + "type": "MoneroTransactionDestinationEntry", + "id": 1 + }, + "dst_entr_hmac": { + "type": "bytes", + "id": 2 + }, + "rsig_data": { + "type": "MoneroTransactionRsigData", + "id": 3 + }, + "is_offloaded_bp": { + "type": "bool", + "id": 4 + } + } + }, + "MoneroTransactionSetOutputAck": { + "fields": { + "tx_out": { + "type": "bytes", + "id": 1 + }, + "vouti_hmac": { + "type": "bytes", + "id": 2 + }, + "rsig_data": { + "type": "MoneroTransactionRsigData", + "id": 3 + }, + "out_pk": { + "type": "bytes", + "id": 4 + }, + "ecdh_info": { + "type": "bytes", + "id": 5 + } + } + }, + "MoneroTransactionAllOutSetRequest": { + "fields": { + "rsig_data": { + "type": "MoneroTransactionRsigData", + "id": 1 + } + } + }, + "MoneroTransactionAllOutSetAck": { + "fields": { + "extra": { + "type": "bytes", + "id": 1 + }, + "tx_prefix_hash": { + "type": "bytes", + "id": 2 + }, + "rv": { + "type": "MoneroRingCtSig", + "id": 4 + }, + "full_message_hash": { + "type": "bytes", + "id": 5 + } + }, + "nested": { + "MoneroRingCtSig": { + "fields": { + "txn_fee": { + "type": "uint64", + "id": 1 + }, + "message": { + "type": "bytes", + "id": 2 + }, + "rv_type": { + "type": "uint32", + "id": 3 + } + } + } + } + }, + "MoneroTransactionSignInputRequest": { + "fields": { + "src_entr": { + "type": "MoneroTransactionSourceEntry", + "id": 1 + }, + "vini": { + "type": "bytes", + "id": 2 + }, + "vini_hmac": { + "type": "bytes", + "id": 3 + }, + "pseudo_out": { + "type": "bytes", + "id": 4 + }, + "pseudo_out_hmac": { + "type": "bytes", + "id": 5 + }, + "pseudo_out_alpha": { + "type": "bytes", + "id": 6 + }, + "spend_key": { + "type": "bytes", + "id": 7 + }, + "orig_idx": { + "type": "uint32", + "id": 8 + } + } + }, + "MoneroTransactionSignInputAck": { + "fields": { + "signature": { + "type": "bytes", + "id": 1 + }, + "pseudo_out": { + "type": "bytes", + "id": 2 + } + } + }, + "MoneroTransactionFinalRequest": { + "fields": {} + }, + "MoneroTransactionFinalAck": { + "fields": { + "cout_key": { + "type": "bytes", + "id": 1 + }, + "salt": { + "type": "bytes", + "id": 2 + }, + "rand_mult": { + "type": "bytes", + "id": 3 + }, + "tx_enc_keys": { + "type": "bytes", + "id": 4 + }, + "opening_key": { + "type": "bytes", + "id": 5 + } + } + }, + "MoneroKeyImageExportInitRequest": { + "fields": { + "num": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "hash": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 3, + "options": { + "packed": false + } + }, + "network_type": { + "type": "MoneroNetworkType", + "id": 4, + "options": { + "default": "MAINNET" + } + }, + "subs": { + "rule": "repeated", + "type": "MoneroSubAddressIndicesList", + "id": 5 + } + }, + "nested": { + "MoneroSubAddressIndicesList": { + "fields": { + "account": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "minor_indices": { + "rule": "repeated", + "type": "uint32", + "id": 2, + "options": { + "packed": false + } + } + } + } + } + }, + "MoneroKeyImageExportInitAck": { + "fields": {} + }, + "MoneroKeyImageSyncStepRequest": { + "fields": { + "tdis": { + "rule": "repeated", + "type": "MoneroTransferDetails", + "id": 1 + } + }, + "nested": { + "MoneroTransferDetails": { + "fields": { + "out_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "tx_pub_key": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "additional_tx_pub_keys": { + "rule": "repeated", + "type": "bytes", + "id": 3 + }, + "internal_output_index": { + "rule": "required", + "type": "uint64", + "id": 4 + }, + "sub_addr_major": { + "type": "uint32", + "id": 5 + }, + "sub_addr_minor": { + "type": "uint32", + "id": 6 + } + } + } + } + }, + "MoneroKeyImageSyncStepAck": { + "fields": { + "kis": { + "rule": "repeated", + "type": "MoneroExportedKeyImage", + "id": 1 + } + }, + "nested": { + "MoneroExportedKeyImage": { + "fields": { + "iv": { + "type": "bytes", + "id": 1 + }, + "blob": { + "type": "bytes", + "id": 3 + } + } + } + } + }, + "MoneroKeyImageSyncFinalRequest": { + "fields": {} + }, + "MoneroKeyImageSyncFinalAck": { + "fields": { + "enc_key": { + "type": "bytes", + "id": 1 + } + } + }, + "MoneroGetTxKeyRequest": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "network_type": { + "type": "MoneroNetworkType", + "id": 2, + "options": { + "default": "MAINNET" + } + }, + "salt1": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "salt2": { + "rule": "required", + "type": "bytes", + "id": 4 + }, + "tx_enc_keys": { + "rule": "required", + "type": "bytes", + "id": 5 + }, + "tx_prefix_hash": { + "rule": "required", + "type": "bytes", + "id": 6 + }, + "reason": { + "type": "uint32", + "id": 7 + }, + "view_public_key": { + "type": "bytes", + "id": 8 + } + } + }, + "MoneroGetTxKeyAck": { + "fields": { + "salt": { + "type": "bytes", + "id": 1 + }, + "tx_keys": { + "type": "bytes", + "id": 2 + }, + "tx_derivations": { + "type": "bytes", + "id": 3 + } + } + }, + "MoneroLiveRefreshStartRequest": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "network_type": { + "type": "MoneroNetworkType", + "id": 2, + "options": { + "default": "MAINNET" + } + } + } + }, + "MoneroLiveRefreshStartAck": { + "fields": {} + }, + "MoneroLiveRefreshStepRequest": { + "fields": { + "out_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "recv_deriv": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "real_out_idx": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "sub_addr_major": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "sub_addr_minor": { + "rule": "required", + "type": "uint32", + "id": 5 + } + } + }, + "MoneroLiveRefreshStepAck": { + "fields": { + "salt": { + "type": "bytes", + "id": 1 + }, + "key_image": { + "type": "bytes", + "id": 2 + } + } + }, + "MoneroLiveRefreshFinalRequest": { + "fields": {} + }, + "MoneroLiveRefreshFinalAck": { + "fields": {} + }, + "DebugMoneroDiagRequest": { + "fields": { + "ins": { + "type": "uint64", + "id": 1 + }, + "p1": { + "type": "uint64", + "id": 2 + }, + "p2": { + "type": "uint64", + "id": 3 + }, + "pd": { + "rule": "repeated", + "type": "uint64", + "id": 4, + "options": { + "packed": false + } + }, + "data1": { + "type": "bytes", + "id": 5 + }, + "data2": { + "type": "bytes", + "id": 6 + } + } + }, + "DebugMoneroDiagAck": { + "fields": { + "ins": { + "type": "uint64", + "id": 1 + }, + "p1": { + "type": "uint64", + "id": 2 + }, + "p2": { + "type": "uint64", + "id": 3 + }, + "pd": { + "rule": "repeated", + "type": "uint64", + "id": 4, + "options": { + "packed": false + } + }, + "data1": { + "type": "bytes", + "id": 5 + }, + "data2": { + "type": "bytes", + "id": 6 + } + } + }, + "NearGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "NearAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + } + } + }, + "NearSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "NearSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "NEMGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "network": { + "type": "uint32", + "id": 2, + "options": { + "default": 104 + } + }, + "show_display": { + "type": "bool", + "id": 3 + } + } + }, + "NEMAddress": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "NEMSignTx": { + "fields": { + "transaction": { + "rule": "required", + "type": "NEMTransactionCommon", + "id": 1 + }, + "multisig": { + "type": "NEMTransactionCommon", + "id": 2 + }, + "transfer": { + "type": "NEMTransfer", + "id": 3 + }, + "cosigning": { + "type": "bool", + "id": 4 + }, + "provision_namespace": { + "type": "NEMProvisionNamespace", + "id": 5 + }, + "mosaic_creation": { + "type": "NEMMosaicCreation", + "id": 6 + }, + "supply_change": { + "type": "NEMMosaicSupplyChange", + "id": 7 + }, + "aggregate_modification": { + "type": "NEMAggregateModification", + "id": 8 + }, + "importance_transfer": { + "type": "NEMImportanceTransfer", + "id": 9 + } + }, + "nested": { + "NEMTransactionCommon": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "network": { + "type": "uint32", + "id": 2, + "options": { + "default": 104 + } + }, + "timestamp": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "fee": { + "rule": "required", + "type": "uint64", + "id": 4 + }, + "deadline": { + "rule": "required", + "type": "uint32", + "id": 5 + }, + "signer": { + "type": "bytes", + "id": 6 + } + } + }, + "NEMTransfer": { + "fields": { + "recipient": { + "rule": "required", + "type": "string", + "id": 1 + }, + "amount": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "payload": { + "type": "bytes", + "id": 3, + "options": { + "default": "" + } + }, + "public_key": { + "type": "bytes", + "id": 4 + }, + "mosaics": { + "rule": "repeated", + "type": "NEMMosaic", + "id": 5 + } + }, + "nested": { + "NEMMosaic": { + "fields": { + "namespace": { + "rule": "required", + "type": "string", + "id": 1 + }, + "mosaic": { + "rule": "required", + "type": "string", + "id": 2 + }, + "quantity": { + "rule": "required", + "type": "uint64", + "id": 3 + } + } + } + } + }, + "NEMProvisionNamespace": { + "fields": { + "namespace": { + "rule": "required", + "type": "string", + "id": 1 + }, + "parent": { + "type": "string", + "id": 2 + }, + "sink": { + "rule": "required", + "type": "string", + "id": 3 + }, + "fee": { + "rule": "required", + "type": "uint64", + "id": 4 + } + } + }, + "NEMMosaicCreation": { + "fields": { + "definition": { + "rule": "required", + "type": "NEMMosaicDefinition", + "id": 1 + }, + "sink": { + "rule": "required", + "type": "string", + "id": 2 + }, + "fee": { + "rule": "required", + "type": "uint64", + "id": 3 + } + }, + "nested": { + "NEMMosaicDefinition": { + "fields": { + "name": { + "type": "string", + "id": 1 + }, + "ticker": { + "type": "string", + "id": 2 + }, + "namespace": { + "rule": "required", + "type": "string", + "id": 3 + }, + "mosaic": { + "rule": "required", + "type": "string", + "id": 4 + }, + "divisibility": { + "type": "uint32", + "id": 5 + }, + "levy": { + "type": "NEMMosaicLevy", + "id": 6 + }, + "fee": { + "type": "uint64", + "id": 7 + }, + "levy_address": { + "type": "string", + "id": 8 + }, + "levy_namespace": { + "type": "string", + "id": 9 + }, + "levy_mosaic": { + "type": "string", + "id": 10 + }, + "supply": { + "type": "uint64", + "id": 11 + }, + "mutable_supply": { + "type": "bool", + "id": 12 + }, + "transferable": { + "type": "bool", + "id": 13 + }, + "description": { + "rule": "required", + "type": "string", + "id": 14 + }, + "networks": { + "rule": "repeated", + "type": "uint32", + "id": 15, + "options": { + "packed": false + } + } + }, + "nested": { + "NEMMosaicLevy": { + "values": { + "MosaicLevy_Absolute": 1, + "MosaicLevy_Percentile": 2 + } + } + } + } + } + }, + "NEMMosaicSupplyChange": { + "fields": { + "namespace": { + "rule": "required", + "type": "string", + "id": 1 + }, + "mosaic": { + "rule": "required", + "type": "string", + "id": 2 + }, + "type": { + "rule": "required", + "type": "NEMSupplyChangeType", + "id": 3 + }, + "delta": { + "rule": "required", + "type": "uint64", + "id": 4 + } + }, + "nested": { + "NEMSupplyChangeType": { + "values": { + "SupplyChange_Increase": 1, + "SupplyChange_Decrease": 2 + } + } + } + }, + "NEMAggregateModification": { + "fields": { + "modifications": { + "rule": "repeated", + "type": "NEMCosignatoryModification", + "id": 1 + }, + "relative_change": { + "type": "sint32", + "id": 2 + } + }, + "nested": { + "NEMCosignatoryModification": { + "fields": { + "type": { + "rule": "required", + "type": "NEMModificationType", + "id": 1 + }, + "public_key": { + "rule": "required", + "type": "bytes", + "id": 2 + } + }, + "nested": { + "NEMModificationType": { + "values": { + "CosignatoryModification_Add": 1, + "CosignatoryModification_Delete": 2 + } + } + } + } + } + }, + "NEMImportanceTransfer": { + "fields": { + "mode": { + "rule": "required", + "type": "NEMImportanceTransferMode", + "id": 1 + }, + "public_key": { + "rule": "required", + "type": "bytes", + "id": 2 + } + }, + "nested": { + "NEMImportanceTransferMode": { + "values": { + "ImportanceTransfer_Activate": 1, + "ImportanceTransfer_Deactivate": 2 + } + } + } + } + } + }, + "NEMSignedTx": { + "fields": { + "data": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "NEMDecryptMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "network": { + "type": "uint32", + "id": 2 + }, + "public_key": { + "type": "bytes", + "id": 3 + }, + "payload": { + "type": "bytes", + "id": 4 + } + } + }, + "NEMDecryptedMessage": { + "fields": { + "payload": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "NeoGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "NeoAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + }, + "public_key": { + "type": "bytes", + "id": 2 + } + } + }, + "NeoSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "network_magic": { + "type": "uint32", + "id": 3, + "options": { + "default": 860833102 + } + } + } + }, + "NeoSignedTx": { + "fields": { + "public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "NervosGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "network": { + "rule": "required", + "type": "string", + "id": 2 + }, + "show_display": { + "type": "bool", + "id": 3 + } + } + }, + "NervosAddress": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "NervosSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "data_initial_chunk": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "witness_buffer": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "network": { + "rule": "required", + "type": "string", + "id": 4 + }, + "data_length": { + "type": "uint32", + "id": 5 + } + } + }, + "NervosSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "address": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "NervosTxRequest": { + "fields": { + "data_length": { + "type": "uint32", + "id": 1 + }, + "public_key": { + "type": "bytes", + "id": 2 + }, + "signature": { + "type": "bytes", + "id": 3 + } + } + }, + "NervosTxAck": { + "fields": { + "data_chunk": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "NexaGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + }, + "prefix": { + "type": "string", + "id": 3, + "options": { + "default": "nexa" + } + } + } + }, + "NexaAddress": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + }, + "public_key": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "NexaSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_message": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "prefix": { + "type": "string", + "id": 3, + "options": { + "default": "nexa" + } + }, + "input_count": { + "type": "uint32", + "id": 4, + "options": { + "default": 1 + } + } + } + }, + "NexaTxInputRequest": { + "fields": { + "request_index": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "signature": { + "type": "bytes", + "id": 2 + } + } + }, + "NexaTxInputAck": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_message": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "NexaSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "NostrGetPublicKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "NostrPublicKey": { + "fields": { + "publickey": { + "type": "string", + "id": 1 + }, + "npub": { + "type": "string", + "id": 2 + } + } + }, + "NostrSignEvent": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "event": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "NostrSignedEvent": { + "fields": { + "event": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "NostrSignSchnorr": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "hash": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "NostrSignedSchnorr": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "NostrEncryptMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "pubkey": { + "rule": "required", + "type": "string", + "id": 2 + }, + "msg": { + "rule": "required", + "type": "string", + "id": 3 + }, + "show_display": { + "type": "bool", + "id": 4 + } + } + }, + "NostrEncryptedMessage": { + "fields": { + "msg": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "NostrDecryptMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "pubkey": { + "rule": "required", + "type": "string", + "id": 2 + }, + "msg": { + "rule": "required", + "type": "string", + "id": 3 + }, + "show_display": { + "type": "bool", + "id": 4 + } + } + }, + "NostrDecryptedMessage": { + "fields": { + "msg": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "PassphraseRequest": { + "fields": { + "_on_device": { + "type": "bool", + "id": 1, + "options": { + "deprecated": true + } + }, + "exists_attach_pin_user": { + "type": "bool", + "id": 8000 + } + } + }, + "PassphraseAck": { + "fields": { + "passphrase": { + "type": "string", + "id": 1 + }, + "_state": { + "type": "bytes", + "id": 2, + "options": { + "deprecated": true + } + }, + "on_device": { + "type": "bool", + "id": 3 + }, + "on_device_attach_pin": { + "type": "bool", + "id": 8000 + } + } + }, + "PolkadotGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "prefix": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "network": { + "rule": "required", + "type": "string", + "id": 3 + }, + "show_display": { + "type": "bool", + "id": 4 + } + } + }, + "PolkadotAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + }, + "public_key": { + "type": "string", + "id": 2 + } + } + }, + "PolkadotSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "network": { + "rule": "required", + "type": "string", + "id": 3 + } + } + }, + "PolkadotSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "RippleGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "RippleAddress": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "RippleSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "fee": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "flags": { + "type": "uint32", + "id": 3, + "options": { + "default": 0 + } + }, + "sequence": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "last_ledger_sequence": { + "type": "uint32", + "id": 5 + }, + "payment": { + "rule": "required", + "type": "RipplePayment", + "id": 6 + } + }, + "nested": { + "RipplePayment": { + "fields": { + "amount": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "destination": { + "rule": "required", + "type": "string", + "id": 2 + }, + "destination_tag": { + "type": "uint32", + "id": 3 + } + } + } + } + }, + "RippleSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "serialized_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "SolanaOffChainMessageVersion": { + "values": { + "MESSAGE_VERSION_0": 0 + } + }, + "SolanaOffChainMessageFormat": { + "values": { + "V0_RESTRICTED_ASCII": 0, + "V0_LIMITED_UTF8": 1 + } + }, + "SolanaGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "SolanaAddress": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "SolanaSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "SolanaSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "SolanaSignOffChainMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "message_version": { + "type": "SolanaOffChainMessageVersion", + "id": 3, + "options": { + "default": "MESSAGE_VERSION_0" + } + }, + "message_format": { + "type": "SolanaOffChainMessageFormat", + "id": 4, + "options": { + "default": "V0_RESTRICTED_ASCII" + } + }, + "application_domain": { + "type": "bytes", + "id": 5 + } + } + }, + "SolanaSignUnsafeMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "SolanaMessageSignature": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "public_key": { + "type": "bytes", + "id": 2 + } + } + }, + "StarcoinGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "StarcoinAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + } + } + }, + "StarcoinGetPublicKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "StarcoinPublicKey": { + "fields": { + "public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "StarcoinSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "StarcoinSignedTx": { + "fields": { + "public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "StarcoinSignMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "type": "bytes", + "id": 2 + } + } + }, + "StarcoinMessageSignature": { + "fields": { + "public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "StarcoinVerifyMessage": { + "fields": { + "public_key": { + "type": "bytes", + "id": 1 + }, + "signature": { + "type": "bytes", + "id": 2 + }, + "message": { + "type": "bytes", + "id": 3 + } + } + }, + "StellarAssetType": { + "values": { + "NATIVE": 0, + "ALPHANUM4": 1, + "ALPHANUM12": 2 + } + }, + "StellarAsset": { + "fields": { + "type": { + "rule": "required", + "type": "StellarAssetType", + "id": 1 + }, + "code": { + "type": "string", + "id": 2 + }, + "issuer": { + "type": "string", + "id": 3 + } + } + }, + "StellarGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "StellarAddress": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "StellarSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 2, + "options": { + "packed": false + } + }, + "network_passphrase": { + "rule": "required", + "type": "string", + "id": 3 + }, + "source_account": { + "rule": "required", + "type": "string", + "id": 4 + }, + "fee": { + "rule": "required", + "type": "uint32", + "id": 5 + }, + "sequence_number": { + "rule": "required", + "type": "uint64", + "id": 6 + }, + "timebounds_start": { + "rule": "required", + "type": "uint32", + "id": 8 + }, + "timebounds_end": { + "rule": "required", + "type": "uint32", + "id": 9 + }, + "memo_type": { + "rule": "required", + "type": "StellarMemoType", + "id": 10 + }, + "memo_text": { + "type": "string", + "id": 11 + }, + "memo_id": { + "type": "uint64", + "id": 12 + }, + "memo_hash": { + "type": "bytes", + "id": 13 + }, + "num_operations": { + "rule": "required", + "type": "uint32", + "id": 14 + } + }, + "nested": { + "StellarMemoType": { + "values": { + "NONE": 0, + "TEXT": 1, + "ID": 2, + "HASH": 3, + "RETURN": 4 + } + } + } + }, + "StellarTxOpRequest": { + "fields": {} + }, + "StellarPaymentOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "destination_account": { + "rule": "required", + "type": "string", + "id": 2 + }, + "asset": { + "rule": "required", + "type": "StellarAsset", + "id": 3 + }, + "amount": { + "rule": "required", + "type": "sint64", + "id": 4 + } + } + }, + "StellarCreateAccountOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "new_account": { + "rule": "required", + "type": "string", + "id": 2 + }, + "starting_balance": { + "rule": "required", + "type": "sint64", + "id": 3 + } + } + }, + "StellarPathPaymentStrictReceiveOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "send_asset": { + "rule": "required", + "type": "StellarAsset", + "id": 2 + }, + "send_max": { + "rule": "required", + "type": "sint64", + "id": 3 + }, + "destination_account": { + "rule": "required", + "type": "string", + "id": 4 + }, + "destination_asset": { + "rule": "required", + "type": "StellarAsset", + "id": 5 + }, + "destination_amount": { + "rule": "required", + "type": "sint64", + "id": 6 + }, + "paths": { + "rule": "repeated", + "type": "StellarAsset", + "id": 7 + } + } + }, + "StellarPathPaymentStrictSendOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "send_asset": { + "rule": "required", + "type": "StellarAsset", + "id": 2 + }, + "send_amount": { + "rule": "required", + "type": "sint64", + "id": 3 + }, + "destination_account": { + "rule": "required", + "type": "string", + "id": 4 + }, + "destination_asset": { + "rule": "required", + "type": "StellarAsset", + "id": 5 + }, + "destination_min": { + "rule": "required", + "type": "sint64", + "id": 6 + }, + "paths": { + "rule": "repeated", + "type": "StellarAsset", + "id": 7 + } + } + }, + "StellarManageSellOfferOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "selling_asset": { + "rule": "required", + "type": "StellarAsset", + "id": 2 + }, + "buying_asset": { + "rule": "required", + "type": "StellarAsset", + "id": 3 + }, + "amount": { + "rule": "required", + "type": "sint64", + "id": 4 + }, + "price_n": { + "rule": "required", + "type": "uint32", + "id": 5 + }, + "price_d": { + "rule": "required", + "type": "uint32", + "id": 6 + }, + "offer_id": { + "rule": "required", + "type": "uint64", + "id": 7 + } + } + }, + "StellarManageBuyOfferOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "selling_asset": { + "rule": "required", + "type": "StellarAsset", + "id": 2 + }, + "buying_asset": { + "rule": "required", + "type": "StellarAsset", + "id": 3 + }, + "amount": { + "rule": "required", + "type": "sint64", + "id": 4 + }, + "price_n": { + "rule": "required", + "type": "uint32", + "id": 5 + }, + "price_d": { + "rule": "required", + "type": "uint32", + "id": 6 + }, + "offer_id": { + "rule": "required", + "type": "uint64", + "id": 7 + } + } + }, + "StellarCreatePassiveSellOfferOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "selling_asset": { + "rule": "required", + "type": "StellarAsset", + "id": 2 + }, + "buying_asset": { + "rule": "required", + "type": "StellarAsset", + "id": 3 + }, + "amount": { + "rule": "required", + "type": "sint64", + "id": 4 + }, + "price_n": { + "rule": "required", + "type": "uint32", + "id": 5 + }, + "price_d": { + "rule": "required", + "type": "uint32", + "id": 6 + } + } + }, + "StellarSetOptionsOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "inflation_destination_account": { + "type": "string", + "id": 2 + }, + "clear_flags": { + "type": "uint32", + "id": 3 + }, + "set_flags": { + "type": "uint32", + "id": 4 + }, + "master_weight": { + "type": "uint32", + "id": 5 + }, + "low_threshold": { + "type": "uint32", + "id": 6 + }, + "medium_threshold": { + "type": "uint32", + "id": 7 + }, + "high_threshold": { + "type": "uint32", + "id": 8 + }, + "home_domain": { + "type": "string", + "id": 9 + }, + "signer_type": { + "type": "StellarSignerType", + "id": 10 + }, + "signer_key": { + "type": "bytes", + "id": 11 + }, + "signer_weight": { + "type": "uint32", + "id": 12 + } + }, + "nested": { + "StellarSignerType": { + "values": { + "ACCOUNT": 0, + "PRE_AUTH": 1, + "HASH": 2 + } + } + } + }, + "StellarChangeTrustOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "asset": { + "rule": "required", + "type": "StellarAsset", + "id": 2 + }, + "limit": { + "rule": "required", + "type": "uint64", + "id": 3 + } + } + }, + "StellarAllowTrustOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "trusted_account": { + "rule": "required", + "type": "string", + "id": 2 + }, + "asset_type": { + "rule": "required", + "type": "StellarAssetType", + "id": 3 + }, + "asset_code": { + "type": "string", + "id": 4 + }, + "is_authorized": { + "rule": "required", + "type": "bool", + "id": 5 + } + } + }, + "StellarAccountMergeOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "destination_account": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "StellarManageDataOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "key": { + "rule": "required", + "type": "string", + "id": 2 + }, + "value": { + "type": "bytes", + "id": 3 + } + } + }, + "StellarBumpSequenceOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "bump_to": { + "rule": "required", + "type": "uint64", + "id": 2 + } + } + }, + "StellarSignedTx": { + "fields": { + "public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "SuiGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "SuiAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + } + } + }, + "SuiSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "data_initial_chunk": { + "type": "bytes", + "id": 3, + "options": { + "default": "" + } + }, + "data_length": { + "type": "uint32", + "id": 4 + } + } + }, + "SuiSignedTx": { + "fields": { + "public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "SuiTxRequest": { + "fields": { + "data_length": { + "type": "uint32", + "id": 1 + }, + "public_key": { + "type": "bytes", + "id": 2 + }, + "signature": { + "type": "bytes", + "id": 3 + } + } + }, + "SuiTxAck": { + "fields": { + "data_chunk": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "SuiSignMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "SuiMessageSignature": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "address": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "TezosGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "TezosAddress": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "TezosGetPublicKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "TezosPublicKey": { + "fields": { + "public_key": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "TezosSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "branch": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "reveal": { + "type": "TezosRevealOp", + "id": 3 + }, + "transaction": { + "type": "TezosTransactionOp", + "id": 4 + }, + "origination": { + "type": "TezosOriginationOp", + "id": 5 + }, + "delegation": { + "type": "TezosDelegationOp", + "id": 6 + }, + "proposal": { + "type": "TezosProposalOp", + "id": 7 + }, + "ballot": { + "type": "TezosBallotOp", + "id": 8 + } + }, + "nested": { + "TezosContractID": { + "fields": { + "tag": { + "rule": "required", + "type": "TezosContractType", + "id": 1 + }, + "hash": { + "rule": "required", + "type": "bytes", + "id": 2 + } + }, + "nested": { + "TezosContractType": { + "values": { + "Implicit": 0, + "Originated": 1 + } + } + } + }, + "TezosRevealOp": { + "fields": { + "source": { + "rule": "required", + "type": "bytes", + "id": 7 + }, + "fee": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "counter": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "gas_limit": { + "rule": "required", + "type": "uint64", + "id": 4 + }, + "storage_limit": { + "rule": "required", + "type": "uint64", + "id": 5 + }, + "public_key": { + "rule": "required", + "type": "bytes", + "id": 6 + } + } + }, + "TezosTransactionOp": { + "fields": { + "source": { + "rule": "required", + "type": "bytes", + "id": 9 + }, + "fee": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "counter": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "gas_limit": { + "rule": "required", + "type": "uint64", + "id": 4 + }, + "storage_limit": { + "rule": "required", + "type": "uint64", + "id": 5 + }, + "amount": { + "rule": "required", + "type": "uint64", + "id": 6 + }, + "destination": { + "rule": "required", + "type": "TezosContractID", + "id": 7 + }, + "parameters": { + "type": "bytes", + "id": 8 + }, + "parameters_manager": { + "type": "TezosParametersManager", + "id": 10 + } + }, + "nested": { + "TezosParametersManager": { + "fields": { + "set_delegate": { + "type": "bytes", + "id": 1 + }, + "cancel_delegate": { + "type": "bool", + "id": 2 + }, + "transfer": { + "type": "TezosManagerTransfer", + "id": 3 + } + }, + "nested": { + "TezosManagerTransfer": { + "fields": { + "destination": { + "rule": "required", + "type": "TezosContractID", + "id": 1 + }, + "amount": { + "rule": "required", + "type": "uint64", + "id": 2 + } + } + } + } + } + } + }, + "TezosOriginationOp": { + "fields": { + "source": { + "rule": "required", + "type": "bytes", + "id": 12 + }, + "fee": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "counter": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "gas_limit": { + "rule": "required", + "type": "uint64", + "id": 4 + }, + "storage_limit": { + "rule": "required", + "type": "uint64", + "id": 5 + }, + "manager_pubkey": { + "type": "bytes", + "id": 6 + }, + "balance": { + "rule": "required", + "type": "uint64", + "id": 7 + }, + "spendable": { + "type": "bool", + "id": 8 + }, + "delegatable": { + "type": "bool", + "id": 9 + }, + "delegate": { + "type": "bytes", + "id": 10 + }, + "script": { + "rule": "required", + "type": "bytes", + "id": 11 + } + } + }, + "TezosDelegationOp": { + "fields": { + "source": { + "rule": "required", + "type": "bytes", + "id": 7 + }, + "fee": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "counter": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "gas_limit": { + "rule": "required", + "type": "uint64", + "id": 4 + }, + "storage_limit": { + "rule": "required", + "type": "uint64", + "id": 5 + }, + "delegate": { + "rule": "required", + "type": "bytes", + "id": 6 + } + } + }, + "TezosProposalOp": { + "fields": { + "source": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "period": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "proposals": { + "rule": "repeated", + "type": "bytes", + "id": 4 + } + } + }, + "TezosBallotOp": { + "fields": { + "source": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "period": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "proposal": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "ballot": { + "rule": "required", + "type": "TezosBallotType", + "id": 4 + } + }, + "nested": { + "TezosBallotType": { + "values": { + "Yay": 0, + "Nay": 1, + "Pass": 2 + } + } + } + } + } + }, + "TezosSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "string", + "id": 1 + }, + "sig_op_contents": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "operation_hash": { + "rule": "required", + "type": "string", + "id": 3 + } + } + }, + "TonWalletVersion": { + "values": { + "V4R2": 3 + } + }, + "TonWorkChain": { + "values": { + "BASECHAIN": 0, + "MASTERCHAIN": 1 + } + }, + "TonGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + }, + "wallet_version": { + "type": "TonWalletVersion", + "id": 3, + "options": { + "default": "V4R2" + } + }, + "is_bounceable": { + "type": "bool", + "id": 4, + "options": { + "default": false + } + }, + "is_testnet_only": { + "type": "bool", + "id": 5, + "options": { + "default": false + } + }, + "workchain": { + "type": "TonWorkChain", + "id": 6, + "options": { + "default": "BASECHAIN" + } + }, + "wallet_id": { + "type": "uint32", + "id": 7, + "options": { + "default": 698983191 + } + } + } + }, + "TonAddress": { + "fields": { + "public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "address": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "TonSignMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "destination": { + "rule": "required", + "type": "string", + "id": 2 + }, + "jetton_master_address": { + "type": "string", + "id": 3 + }, + "jetton_wallet_address": { + "type": "string", + "id": 4 + }, + "ton_amount": { + "rule": "required", + "type": "uint64", + "id": 5 + }, + "jetton_amount": { + "type": "uint64", + "id": 6 + }, + "fwd_fee": { + "type": "uint64", + "id": 7, + "options": { + "default": 0 + } + }, + "comment": { + "type": "string", + "id": 8 + }, + "is_raw_data": { + "type": "bool", + "id": 9, + "options": { + "default": false + } + }, + "mode": { + "type": "uint32", + "id": 10, + "options": { + "default": 3 + } + }, + "seqno": { + "rule": "required", + "type": "uint32", + "id": 11 + }, + "expire_at": { + "rule": "required", + "type": "uint64", + "id": 12 + }, + "wallet_version": { + "type": "TonWalletVersion", + "id": 13, + "options": { + "default": "V4R2" + } + }, + "wallet_id": { + "type": "uint32", + "id": 14, + "options": { + "default": 698983191 + } + }, + "workchain": { + "type": "TonWorkChain", + "id": 15, + "options": { + "default": "BASECHAIN" + } + }, + "is_bounceable": { + "type": "bool", + "id": 16, + "options": { + "default": false + } + }, + "is_testnet_only": { + "type": "bool", + "id": 17, + "options": { + "default": false + } + }, + "ext_destination": { + "rule": "repeated", + "type": "string", + "id": 18 + }, + "ext_ton_amount": { + "rule": "repeated", + "type": "uint64", + "id": 19, + "options": { + "packed": false + } + }, + "ext_payload": { + "rule": "repeated", + "type": "string", + "id": 20 + }, + "jetton_amount_bytes": { + "type": "bytes", + "id": 21 + }, + "init_data_initial_chunk": { + "type": "bytes", + "id": 22 + }, + "init_data_length": { + "type": "uint32", + "id": 23 + }, + "signing_message_repr": { + "type": "bytes", + "id": 24 + } + } + }, + "TonSignedMessage": { + "fields": { + "signature": { + "type": "bytes", + "id": 1 + }, + "signing_message": { + "type": "bytes", + "id": 2 + }, + "init_data_length": { + "type": "uint32", + "id": 3 + } + } + }, + "TonTxAck": { + "fields": { + "init_data_chunk": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "TonSignProof": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "appdomain": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "comment": { + "type": "bytes", + "id": 3 + }, + "expire_at": { + "rule": "required", + "type": "uint64", + "id": 4 + }, + "wallet_version": { + "type": "TonWalletVersion", + "id": 5, + "options": { + "default": "V4R2" + } + }, + "wallet_id": { + "type": "uint32", + "id": 6, + "options": { + "default": 698983191 + } + }, + "workchain": { + "type": "TonWorkChain", + "id": 7, + "options": { + "default": "BASECHAIN" + } + }, + "is_bounceable": { + "type": "bool", + "id": 8, + "options": { + "default": false + } + }, + "is_testnet_only": { + "type": "bool", + "id": 9, + "options": { + "default": false + } + } + } + }, + "TonSignedProof": { + "fields": { + "signature": { + "type": "bytes", + "id": 1 + } + } + }, + "TronGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "TronAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + } + } + }, + "TronSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "ref_block_bytes": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "ref_block_hash": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "expiration": { + "rule": "required", + "type": "uint64", + "id": 4 + }, + "data": { + "type": "bytes", + "id": 5 + }, + "contract": { + "rule": "required", + "type": "TronContract", + "id": 6 + }, + "timestamp": { + "rule": "required", + "type": "uint64", + "id": 7 + }, + "fee_limit": { + "type": "uint64", + "id": 8 + } + }, + "nested": { + "TronContract": { + "fields": { + "transfer_contract": { + "type": "TronTransferContract", + "id": 2 + }, + "vote_witness_contract": { + "type": "TronVoteWitnessContract", + "id": 4 + }, + "freeze_balance_contract": { + "type": "TronFreezeBalanceContract", + "id": 11 + }, + "unfreeze_balance_contract": { + "type": "TronUnfreezeBalanceContract", + "id": 12 + }, + "withdraw_balance_contract": { + "type": "TronWithdrawBalanceContract", + "id": 13 + }, + "trigger_smart_contract": { + "type": "TronTriggerSmartContract", + "id": 31 + }, + "freeze_balance_v2_contract": { + "type": "TronFreezeBalanceV2Contract", + "id": 54 + }, + "unfreeze_balance_v2_contract": { + "type": "TronUnfreezeBalanceV2Contract", + "id": 55 + }, + "withdraw_expire_unfreeze_contract": { + "type": "TronWithdrawExpireUnfreezeContract", + "id": 56 + }, + "delegate_resource_contract": { + "type": "TronDelegateResourceContract", + "id": 57 + }, + "undelegate_resource_contract": { + "type": "TronUnDelegateResourceContract", + "id": 58 + }, + "cancel_all_unfreeze_v2_contract": { + "type": "TronCancelAllUnfreezeV2Contract", + "id": 59 + }, + "provider": { + "type": "bytes", + "id": 3 + }, + "contract_name": { + "type": "bytes", + "id": 5 + }, + "permission_id": { + "type": "uint32", + "id": 6 + } + }, + "nested": { + "TronTransferContract": { + "fields": { + "to_address": { + "type": "string", + "id": 2 + }, + "amount": { + "type": "uint64", + "id": 3 + } + } + }, + "TronTriggerSmartContract": { + "fields": { + "contract_address": { + "type": "string", + "id": 2 + }, + "call_value": { + "type": "uint64", + "id": 3 + }, + "data": { + "type": "bytes", + "id": 4 + }, + "call_token_value": { + "type": "uint64", + "id": 5 + }, + "asset_id": { + "type": "uint64", + "id": 6 + } + } + }, + "TronResourceCode": { + "values": { + "BANDWIDTH": 0, + "ENERGY": 1, + "TRON_POWER": 2 + } + }, + "TronFreezeBalanceContract": { + "fields": { + "frozen_balance": { + "type": "uint64", + "id": 1 + }, + "frozen_duration": { + "type": "uint64", + "id": 2 + }, + "resource": { + "type": "TronResourceCode", + "id": 3 + }, + "receiver_address": { + "type": "string", + "id": 4 + } + } + }, + "TronUnfreezeBalanceContract": { + "fields": { + "resource": { + "type": "TronResourceCode", + "id": 1 + }, + "receiver_address": { + "type": "string", + "id": 2 + } + } + }, + "TronWithdrawBalanceContract": { + "fields": { + "owner_address": { + "type": "bytes", + "id": 1 + } + } + }, + "TronFreezeBalanceV2Contract": { + "fields": { + "frozen_balance": { + "type": "uint64", + "id": 2 + }, + "resource": { + "type": "TronResourceCode", + "id": 3 + } + } + }, + "TronUnfreezeBalanceV2Contract": { + "fields": { + "unfreeze_balance": { + "type": "uint64", + "id": 2 + }, + "resource": { + "type": "TronResourceCode", + "id": 3 + } + } + }, + "TronWithdrawExpireUnfreezeContract": { + "fields": {} + }, + "TronDelegateResourceContract": { + "fields": { + "resource": { + "type": "TronResourceCode", + "id": 2 + }, + "balance": { + "type": "uint64", + "id": 3 + }, + "receiver_address": { + "type": "string", + "id": 4 + }, + "lock": { + "type": "bool", + "id": 5 + }, + "lock_period": { + "type": "uint64", + "id": 6 + } + } + }, + "TronUnDelegateResourceContract": { + "fields": { + "resource": { + "type": "TronResourceCode", + "id": 2 + }, + "balance": { + "type": "uint64", + "id": 3 + }, + "receiver_address": { + "type": "string", + "id": 4 + } + } + }, + "TronCancelAllUnfreezeV2Contract": { + "fields": {} + }, + "TronVoteWitnessContract": { + "fields": { + "votes": { + "rule": "repeated", + "type": "Vote", + "id": 2 + }, + "support": { + "type": "bool", + "id": 3 + } + }, + "nested": { + "Vote": { + "fields": { + "vote_address": { + "rule": "required", + "type": "string", + "id": 1 + }, + "vote_count": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + } + } + } + } + } + } + }, + "TronSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "serialized_tx": { + "type": "bytes", + "id": 2 + } + } + }, + "TronMessageType": { + "values": { + "V1": 1, + "V2": 2 + } + }, + "TronSignMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "message_type": { + "type": "TronMessageType", + "id": 3, + "options": { + "default": "V1" + } + } + } + }, + "TronMessageSignature": { + "fields": { + "address": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "ViewAmount": { + "fields": { + "is_unlimited": { + "rule": "required", + "type": "bool", + "id": 1 + }, + "num": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "ViewDetail": { + "fields": { + "key": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "value": { + "rule": "required", + "type": "string", + "id": 2 + }, + "is_overview": { + "rule": "required", + "type": "bool", + "id": 3 + }, + "has_icon": { + "rule": "required", + "type": "bool", + "id": 4 + } + } + }, + "ViewTipType": { + "values": { + "Default": 0, + "Highlight": 1, + "Recommend": 2, + "Warning": 3, + "Danger": 4 + } + }, + "ViewTip": { + "fields": { + "type": { + "rule": "required", + "type": "ViewTipType", + "id": 1 + }, + "text": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "ViewRawData": { + "fields": { + "initial_data": { + "rule": "required", + "type": "string", + "id": 1 + }, + "placeholder": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + }, + "ViewSignLayout": { + "values": { + "LayoutDefault": 0, + "LayoutSafeTxCreate": 1, + "LayoutFinalConfirm": 2, + "Layout7702": 3, + "LayoutFlat": 4 + } + }, + "ViewSignPage": { + "fields": { + "title": { + "rule": "required", + "type": "string", + "id": 1 + }, + "amount": { + "type": "ViewAmount", + "id": 2 + }, + "general": { + "rule": "repeated", + "type": "ViewDetail", + "id": 3 + }, + "tip": { + "type": "ViewTip", + "id": 4 + }, + "raw_data": { + "type": "ViewRawData", + "id": 5 + }, + "slide_to_confirm": { + "type": "bool", + "id": 6, + "options": { + "default": true + } + }, + "layout": { + "type": "ViewSignLayout", + "id": 7, + "options": { + "default": "LayoutDefault" + } + } + } + }, + "ViewVerifyPage": { + "fields": { + "title": { + "rule": "required", + "type": "string", + "id": 1 + }, + "address": { + "rule": "required", + "type": "string", + "id": 2 + }, + "path": { + "rule": "required", + "type": "string", + "id": 3 + } + } + }, + "WebAuthnListResidentCredentials": { + "fields": {} + }, + "WebAuthnAddResidentCredential": { + "fields": { + "credential_id": { + "type": "bytes", + "id": 1 + } + } + }, + "WebAuthnRemoveResidentCredential": { + "fields": { + "index": { + "type": "uint32", + "id": 1 + } + } + }, + "WebAuthnCredentials": { + "fields": { + "credentials": { + "rule": "repeated", + "type": "WebAuthnCredential", + "id": 1 + } + }, + "nested": { + "WebAuthnCredential": { + "fields": { + "index": { + "type": "uint32", + "id": 1 + }, + "id": { + "type": "bytes", + "id": 2 + }, + "rp_id": { + "type": "string", + "id": 3 + }, + "rp_name": { + "type": "string", + "id": 4 + }, + "user_id": { + "type": "bytes", + "id": 5 + }, + "user_name": { + "type": "string", + "id": 6 + }, + "user_display_name": { + "type": "string", + "id": 7 + }, + "creation_time": { + "type": "uint32", + "id": 8 + }, + "hmac_secret": { + "type": "bool", + "id": 9 + }, + "use_sign_count": { + "type": "bool", + "id": 10 + }, + "algorithm": { + "type": "sint32", + "id": 11 + }, + "curve": { + "type": "sint32", + "id": 12 + } + } + } + } + }, + "ProtocolInfoRequest": { + "fields": {} + }, + "ProtocolInfo": { + "fields": { + "version": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "supported_messages": { + "rule": "repeated", + "type": "uint32", + "id": 2, + "options": { + "packed": false + } + }, + "protobuf_definition": { + "type": "string", + "id": 3 + } + } + }, + "Ping": { + "fields": { + "message": { + "type": "string", + "id": 1, + "options": { + "default": "" + } + } + } + }, + "Success": { + "fields": { + "message": { + "type": "string", + "id": 1, + "options": { + "default": "" + } + } + } + }, + "Failure": { + "fields": { + "code": { + "rule": "required", + "type": "FailureType", + "id": 1 + }, + "subcode": { + "type": "uint32", + "id": 2 + }, + "message": { + "type": "string", + "id": 3 + } + }, + "nested": { + "FailureType": { + "values": { + "Failure_InvalidMessage": 1, + "Failure_UndefinedError": 2, + "Failure_UsageError": 3, + "Failure_DataError": 4, + "Failure_ProcessError": 5 + } + } + } + }, + "DeviceErrorCode": { + "values": { + "DeviceError_None": 0, + "DeviceError_Busy": 1, + "DeviceError_NotInitialized": 2, + "DeviceError_ActionCancelled": 3, + "DeviceError_PinAlreadyUsed": 4, + "DeviceError_PersistFailed": 5, + "DeviceError_SeError": 6, + "DeviceError_InvalidLanguage": 7, + "DeviceError_WallpaperNotUsable": 8, + "DeviceError_DeviceLocked": 9 + } + }, + "DeviceRebootType": { + "values": { + "Normal": 0, + "Romloader": 1, + "Bootloader": 2 + } + }, + "DeviceReboot": { + "fields": { + "reboot_type": { + "rule": "required", + "type": "DeviceRebootType", + "id": 1 + } + } + }, + "DeviceSettings": { + "fields": { + "label": { + "type": "string", + "id": 1 + }, + "bt_enable": { + "type": "bool", + "id": 2 + }, + "language": { + "type": "string", + "id": 3 + }, + "wallpaper_path": { + "type": "string", + "id": 4 + }, + "passphrase_enable": { + "type": "bool", + "id": 6 + }, + "brightness": { + "type": "uint32", + "id": 7 + }, + "autolock_delay_ms": { + "type": "uint32", + "id": 8 + }, + "autoshutdown_delay_ms": { + "type": "uint32", + "id": 9 + }, + "animation_enable": { + "type": "bool", + "id": 10 + }, + "tap_to_wake": { + "type": "bool", + "id": 11 + }, + "haptic_feedback": { + "type": "bool", + "id": 12 + }, + "device_name_display_enabled": { + "type": "bool", + "id": 13 + }, + "airgap_mode": { + "type": "bool", + "id": 14 + }, + "fido_enabled": { + "type": "bool", + "id": 15 + }, + "experimental_features": { + "type": "bool", + "id": 16 + }, + "usb_lock_enable": { + "type": "bool", + "id": 17 + }, + "random_keypad": { + "type": "bool", + "id": 18 + } + } + }, + "DeviceSettingsGet": { + "fields": {} + }, + "DeviceSettingsSet": { + "fields": { + "settings": { + "rule": "required", + "type": "DeviceSettings", + "id": 1 + } + } + }, + "DeviceSettingsPage": { + "values": { + "DeviceReset": 0, + "DevicePinChange": 1, + "DevicePassphrase": 2, + "DeviceAirgap": 3 + } + }, + "DeviceSettingsPageShow": { + "fields": { + "page": { + "rule": "required", + "type": "DeviceSettingsPage", + "id": 1 + }, + "field_name": { + "type": "string", + "id": 2 + } + } + }, + "DeviceCertificate": { + "fields": { + "cert_and_pubkey": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "private_key": { + "type": "bytes", + "id": 2 + } + } + }, + "DeviceCertificateWrite": { + "fields": { + "cert": { + "rule": "required", + "type": "DeviceCertificate", + "id": 1 + } + } + }, + "DeviceCertificateRead": { + "fields": {} + }, + "DeviceCertificateSignature": { + "fields": { + "data": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "DeviceCertificateSign": { + "fields": { + "data": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "DeviceFirmwareTargetType": { + "values": { + "FW_MGMT_TARGET_INVALID": 0, + "FW_MGMT_TARGET_CRATE": 1, + "FW_MGMT_TARGET_ROMLOADER": 2, + "FW_MGMT_TARGET_BOOTLOADER": 3, + "FW_MGMT_TARGET_APPLICATION_P1": 4, + "FW_MGMT_TARGET_APPLICATION_P2": 5, + "FW_MGMT_TARGET_COPROCESSOR": 6, + "FW_MGMT_TARGET_SE01": 7, + "FW_MGMT_TARGET_SE02": 8, + "FW_MGMT_TARGET_SE03": 9, + "FW_MGMT_TARGET_SE04": 10 + } + }, + "DeviceFirmwareUpdateTaskStatus": { + "values": { + "FW_MGMT_UPDATER_TASK_STATUS_PENDING": 0, + "FW_MGMT_UPDATER_TASK_STATUS_IN_PROGRESS": 1, + "FW_MGMT_UPDATER_TASK_STATUS_FINISHED": 2, + "FW_MGMT_UPDATER_TASK_STATUS_FAILED_FILE_NOT_FOUND": 3, + "FW_MGMT_UPDATER_TASK_STATUS_FAILED_FILE_READ": 4, + "FW_MGMT_UPDATER_TASK_STATUS_FAILED_FILE_WRITE": 5, + "FW_MGMT_UPDATER_TASK_STATUS_FAILED_VERIFY": 6, + "FW_MGMT_UPDATER_TASK_STATUS_FAILED_INSTALL": 7, + "FW_MGMT_UPDATER_TASK_STATUS_FAILED_ABORT": 8, + "FW_MGMT_UPDATER_TASK_STATUS_FAILED_BUSY": 9, + "FW_MGMT_UPDATER_TASK_STATUS_FAILED_ENTRY_OUT_OF_BOUNDS": 10 + } + }, + "DeviceFirmwareTarget": { + "fields": { + "target_id": { + "rule": "required", + "type": "DeviceFirmwareTargetType", + "id": 1 + }, + "path": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "DeviceFirmwareUpdateRequest": { + "fields": { + "targets": { + "rule": "repeated", + "type": "DeviceFirmwareTarget", + "id": 1 + } + } + }, + "DeviceFirmwareUpdateRecord": { + "fields": { + "target_id": { + "rule": "required", + "type": "DeviceFirmwareTargetType", + "id": 1 + }, + "status": { + "type": "DeviceFirmwareUpdateTaskStatus", + "id": 10 + }, + "payload_version": { + "type": "uint32", + "id": 20 + }, + "path": { + "type": "string", + "id": 30 + } + } + }, + "DeviceFirmwareUpdateRecordFields": { + "fields": { + "status": { + "type": "bool", + "id": 10 + }, + "payload_version": { + "type": "bool", + "id": 20 + }, + "path": { + "type": "bool", + "id": 30 + } + } + }, + "DeviceFirmwareUpdateStatusGet": { + "fields": { + "fields": { + "type": "DeviceFirmwareUpdateRecordFields", + "id": 1 + } + } + }, + "DeviceFirmwareUpdateStatus": { + "fields": { + "records": { + "rule": "repeated", + "type": "DeviceFirmwareUpdateRecord", + "id": 1 + } + } + }, + "DeviceFactoryAck": { + "values": { + "FACTORY_ACK_SUCCESS": 0, + "FACTORY_ACK_FAIL": 1 + } + }, + "DeviceFactoryInfoManufactureTime": { + "fields": { + "year": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "month": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "day": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "hour": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "minute": { + "rule": "required", + "type": "uint32", + "id": 5 + }, + "second": { + "rule": "required", + "type": "uint32", + "id": 6 + } + } + }, + "DeviceFactoryInfo": { + "fields": { + "version": { + "type": "uint32", + "id": 1 + }, + "serial_number": { + "type": "string", + "id": 2 + }, + "burn_in_completed": { + "type": "bool", + "id": 3 + }, + "factory_test_completed": { + "type": "bool", + "id": 4 + }, + "manufacture_time": { + "type": "DeviceFactoryInfoManufactureTime", + "id": 5 + } + } + }, + "DeviceFactoryInfoSet": { + "fields": { + "info": { + "rule": "required", + "type": "DeviceFactoryInfo", + "id": 1 + } + } + }, + "DeviceFactoryInfoGet": { + "fields": {} + }, + "DeviceFactoryPermanentLock": { + "fields": { + "check_a": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "check_b": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "DeviceFactoryTest": { + "fields": { + "burn_in_test": { + "rule": "required", + "type": "bool", + "id": 1 + } + } + }, + "DeviceType": { + "values": { + "CLASSIC1": 0, + "CLASSIC1S": 1, + "MINI": 2, + "TOUCH": 3, + "PRO": 5, + "CLASSIC1S_PURE": 6, + "PRO2": 7, + "NEO": 8 + } + }, + "DeviceSeType": { + "values": { + "THD89": 0, + "SE608A": 1 + } + }, + "DeviceSEState": { + "values": { + "BOOT": 0, + "APP_FACTORY": 51, + "APP": 85 + } + }, + "DeviceFirmwareImageInfo": { + "fields": { + "version": { + "type": "string", + "id": 10 + }, + "build_id": { + "type": "string", + "id": 20 + }, + "hash": { + "type": "bytes", + "id": 30 + } + } + }, + "DeviceHardwareInfo": { + "fields": { + "Device_type": { + "type": "DeviceType", + "id": 10 + }, + "serial_no": { + "type": "string", + "id": 20 + }, + "hardware_version": { + "type": "string", + "id": 100 + }, + "hardware_version_raw_adc": { + "type": "uint32", + "id": 110 + } + } + }, + "DeviceMainMcuInfo": { + "fields": { + "romloader": { + "type": "DeviceFirmwareImageInfo", + "id": 10 + }, + "bootloader": { + "type": "DeviceFirmwareImageInfo", + "id": 20 + }, + "application": { + "type": "DeviceFirmwareImageInfo", + "id": 30 + }, + "application_data": { + "type": "DeviceFirmwareImageInfo", + "id": 40 + } + } + }, + "DeviceCoprocessorInfo": { + "fields": { + "bootloader": { + "type": "DeviceFirmwareImageInfo", + "id": 20 + }, + "application": { + "type": "DeviceFirmwareImageInfo", + "id": 30 + }, + "bt_adv_name": { + "type": "string", + "id": 100 + }, + "bt_mac": { + "type": "bytes", + "id": 110 + } + } + }, + "DeviceSEInfo": { + "fields": { + "bootloader": { + "type": "DeviceFirmwareImageInfo", + "id": 20 + }, + "application": { + "type": "DeviceFirmwareImageInfo", + "id": 30 + }, + "type": { + "type": "DeviceSeType", + "id": 100 + }, + "state": { + "type": "DeviceSEState", + "id": 110 + } + } + }, + "DeviceInfoTargets": { + "fields": { + "hw": { + "type": "bool", + "id": 100 + }, + "fw": { + "type": "bool", + "id": 200 + }, + "coprocessor": { + "type": "bool", + "id": 300 + }, + "se1": { + "type": "bool", + "id": 400 + }, + "se2": { + "type": "bool", + "id": 410 + }, + "se3": { + "type": "bool", + "id": 420 + }, + "se4": { + "type": "bool", + "id": 430 + }, + "status": { + "type": "bool", + "id": 10000 + } + } + }, + "DeviceInfoTypes": { + "fields": { + "version": { + "type": "bool", + "id": 10 + }, + "build_id": { + "type": "bool", + "id": 20 + }, + "hash": { + "type": "bool", + "id": 30 + }, + "specific": { + "type": "bool", + "id": 40 + } + } + }, + "DeviceInfoGet": { + "fields": { + "targets": { + "type": "DeviceInfoTargets", + "id": 1 + }, + "types": { + "type": "DeviceInfoTypes", + "id": 2 + } + } + }, + "DeviceInfo": { + "fields": { + "protocol_version": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "hw": { + "type": "DeviceHardwareInfo", + "id": 100 + }, + "fw": { + "type": "DeviceMainMcuInfo", + "id": 200 + }, + "coprocessor": { + "type": "DeviceCoprocessorInfo", + "id": 300 + }, + "se1": { + "type": "DeviceSEInfo", + "id": 400 + }, + "se2": { + "type": "DeviceSEInfo", + "id": 410 + }, + "se3": { + "type": "DeviceSEInfo", + "id": 420 + }, + "se4": { + "type": "DeviceSEInfo", + "id": 430 + }, + "status": { + "type": "DeviceStatus", + "id": 10000 + } + } + }, + "DeviceSessionGet": { + "fields": { + "session_id": { + "type": "bytes", + "id": 1 + } + } + }, + "DeviceSession": { + "fields": { + "session_id": { + "type": "bytes", + "id": 1 + }, + "btc_test_address": { + "type": "string", + "id": 2 + } + } + }, + "DeviceSessionAskPin": { + "fields": {} + }, + "DeviceSessionAskPin_FailureSubCodes": { + "values": { + "UserCancel": 1 + } + }, + "DeviceStatus": { + "fields": { + "device_id": { + "type": "string", + "id": 1 + }, + "unlocked": { + "type": "bool", + "id": 2 + }, + "init_states": { + "type": "bool", + "id": 3 + }, + "backup_required": { + "type": "bool", + "id": 4 + }, + "passphrase_enabled": { + "type": "bool", + "id": 10 + }, + "attach_to_pin_enabled": { + "type": "bool", + "id": 11 + }, + "unlocked_by_attach_to_pin": { + "type": "bool", + "id": 12 + } + } + }, + "DeviceStatusGet": { + "fields": {} + }, + "DevOnboardingStage": { + "values": { + "DEV_ONBOARDING_STAGE_UNKNOWN": 0, + "DEV_ONBOARDING_STAGE_SAFETY_CHECK": 1, + "DEV_ONBOARDING_STAGE_PERSONALIZATION": 2, + "DEV_ONBOARDING_STAGE_SELECT_SETUP_METHOD": 3, + "DEV_ONBOARDING_STAGE_NEW_DEVICE": 4, + "DEV_ONBOARDING_STAGE_SELECT_RESTORE_METHOD": 5, + "DEV_ONBOARDING_STAGE_RESTORE_MNEMONIC": 6, + "DEV_ONBOARDING_STAGE_RESTORE_SEEDCARD": 7, + "DEV_ONBOARDING_STAGE_WALLET_READY": 8, + "DEV_ONBOARDING_STAGE_SEEDCARD_BACKUP_PROMPT": 9, + "DEV_ONBOARDING_STAGE_SELECT_SEEDCARD_BACKUP_METHOD": 10, + "DEV_ONBOARDING_STAGE_SEEDCARD_BACKUP": 11, + "DEV_ONBOARDING_STAGE_DONE": 12 + } + }, + "DevGetOnboardingStatus": { + "fields": {} + }, + "DevOnboardingStatus": { + "fields": { + "stage": { + "rule": "required", + "type": "DevOnboardingStage", + "id": 1 + }, + "status_code": { + "type": "uint32", + "id": 2 + }, + "detail_code": { + "type": "uint32", + "id": 3 + } + } + }, + "FilesystemPermissionFix": { + "fields": {} + }, + "FilesystemPathInfo": { + "fields": { + "exist": { + "rule": "required", + "type": "bool", + "id": 1 + }, + "size": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "year": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "month": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "day": { + "rule": "required", + "type": "uint32", + "id": 5 + }, + "hour": { + "rule": "required", + "type": "uint32", + "id": 6 + }, + "minute": { + "rule": "required", + "type": "uint32", + "id": 7 + }, + "second": { + "rule": "required", + "type": "uint32", + "id": 8 + }, + "readonly": { + "rule": "required", + "type": "bool", + "id": 9 + }, + "hidden": { + "rule": "required", + "type": "bool", + "id": 10 + }, + "system": { + "rule": "required", + "type": "bool", + "id": 11 + }, + "archive": { + "rule": "required", + "type": "bool", + "id": 12 + }, + "directory": { + "rule": "required", + "type": "bool", + "id": 13 + } + } + }, + "FilesystemPathInfoQuery": { + "fields": { + "path": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "FilesystemFile": { + "fields": { + "path": { + "rule": "required", + "type": "string", + "id": 1 + }, + "offset": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "total_size": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "data": { + "type": "bytes", + "id": 4 + }, + "data_hash": { + "type": "uint32", + "id": 5 + }, + "processed_byte": { + "type": "uint32", + "id": 6 + } + } + }, + "FilesystemFileRead": { + "fields": { + "file": { + "rule": "required", + "type": "FilesystemFile", + "id": 1 + }, + "chunk_len": { + "type": "uint32", + "id": 2 + }, + "ui_percentage": { + "type": "uint32", + "id": 3 + } + } + }, + "FilesystemFileWrite": { + "fields": { + "file": { + "rule": "required", + "type": "FilesystemFile", + "id": 1 + }, + "overwrite": { + "rule": "required", + "type": "bool", + "id": 2 + }, + "append": { + "rule": "required", + "type": "bool", + "id": 3 + }, + "ui_percentage": { + "type": "uint32", + "id": 4 + } + } + }, + "FilesystemFileDelete": { + "fields": { + "path": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "FilesystemDir": { + "fields": { + "path": { + "rule": "required", + "type": "string", + "id": 1 + }, + "child_dirs": { + "type": "string", + "id": 2 + }, + "child_files": { + "type": "string", + "id": 3 + } + } + }, + "FilesystemDirList": { + "fields": { + "path": { + "rule": "required", + "type": "string", + "id": 1 + }, + "depth": { + "type": "uint32", + "id": 2, + "options": { + "default": 0 + } + } + } + }, + "FilesystemDirMake": { + "fields": { + "path": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "FilesystemDirRemove": { + "fields": { + "path": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "FilesystemFormat": { + "fields": { + "data": { + "rule": "required", + "type": "bool", + "id": 1 + }, + "user": { + "rule": "required", + "type": "bool", + "id": 2 + } + } + }, + "PortfolioUpdate": { + "fields": {} + }, + "google": { + "nested": { + "protobuf": { + "nested": { + "FileDescriptorSet": { + "fields": { + "file": { + "rule": "repeated", + "type": "FileDescriptorProto", + "id": 1 + } + } + }, + "FileDescriptorProto": { + "fields": { + "name": { + "type": "string", + "id": 1 + }, + "package": { + "type": "string", + "id": 2 + }, + "dependency": { + "rule": "repeated", + "type": "string", + "id": 3 + }, + "public_dependency": { + "rule": "repeated", + "type": "int32", + "id": 10, + "options": { + "packed": false + } + }, + "weak_dependency": { + "rule": "repeated", + "type": "int32", + "id": 11, + "options": { + "packed": false + } + }, + "message_type": { + "rule": "repeated", + "type": "DescriptorProto", + "id": 4 + }, + "enum_type": { + "rule": "repeated", + "type": "EnumDescriptorProto", + "id": 5 + }, + "service": { + "rule": "repeated", + "type": "ServiceDescriptorProto", + "id": 6 + }, + "extension": { + "rule": "repeated", + "type": "FieldDescriptorProto", + "id": 7 + }, + "options": { + "type": "FileOptions", + "id": 8 + }, + "source_code_info": { + "type": "SourceCodeInfo", + "id": 9 + }, + "syntax": { + "type": "string", + "id": 12 + } + } + }, + "DescriptorProto": { + "fields": { + "name": { + "type": "string", + "id": 1 + }, + "field": { + "rule": "repeated", + "type": "FieldDescriptorProto", + "id": 2 + }, + "extension": { + "rule": "repeated", + "type": "FieldDescriptorProto", + "id": 6 + }, + "nested_type": { + "rule": "repeated", + "type": "DescriptorProto", + "id": 3 + }, + "enum_type": { + "rule": "repeated", + "type": "EnumDescriptorProto", + "id": 4 + }, + "extension_range": { + "rule": "repeated", + "type": "ExtensionRange", + "id": 5 + }, + "oneof_decl": { + "rule": "repeated", + "type": "OneofDescriptorProto", + "id": 8 + }, + "options": { + "type": "MessageOptions", + "id": 7 + }, + "reserved_range": { + "rule": "repeated", + "type": "ReservedRange", + "id": 9 + }, + "reserved_name": { + "rule": "repeated", + "type": "string", + "id": 10 + } + }, + "nested": { + "ExtensionRange": { + "fields": { + "start": { + "type": "int32", + "id": 1 + }, + "end": { + "type": "int32", + "id": 2 + } + } + }, + "ReservedRange": { + "fields": { + "start": { + "type": "int32", + "id": 1 + }, + "end": { + "type": "int32", + "id": 2 + } + } + } + } + }, + "FieldDescriptorProto": { + "fields": { + "name": { + "type": "string", + "id": 1 + }, + "number": { + "type": "int32", + "id": 3 + }, + "label": { + "type": "Label", + "id": 4 + }, + "type": { + "type": "Type", + "id": 5 + }, + "type_name": { + "type": "string", + "id": 6 + }, + "extendee": { + "type": "string", + "id": 2 + }, + "default_value": { + "type": "string", + "id": 7 + }, + "oneof_index": { + "type": "int32", + "id": 9 + }, + "json_name": { + "type": "string", + "id": 10 + }, + "options": { + "type": "FieldOptions", + "id": 8 + } + }, + "nested": { + "Type": { + "values": { + "TYPE_DOUBLE": 1, + "TYPE_FLOAT": 2, + "TYPE_INT64": 3, + "TYPE_UINT64": 4, + "TYPE_INT32": 5, + "TYPE_FIXED64": 6, + "TYPE_FIXED32": 7, + "TYPE_BOOL": 8, + "TYPE_STRING": 9, + "TYPE_GROUP": 10, + "TYPE_MESSAGE": 11, + "TYPE_BYTES": 12, + "TYPE_UINT32": 13, + "TYPE_ENUM": 14, + "TYPE_SFIXED32": 15, + "TYPE_SFIXED64": 16, + "TYPE_SINT32": 17, + "TYPE_SINT64": 18 + } + }, + "Label": { + "values": { + "LABEL_OPTIONAL": 1, + "LABEL_REQUIRED": 2, + "LABEL_REPEATED": 3 + } + } + } + }, + "OneofDescriptorProto": { + "fields": { + "name": { + "type": "string", + "id": 1 + }, + "options": { + "type": "OneofOptions", + "id": 2 + } + } + }, + "EnumDescriptorProto": { + "fields": { + "name": { + "type": "string", + "id": 1 + }, + "value": { + "rule": "repeated", + "type": "EnumValueDescriptorProto", + "id": 2 + }, + "options": { + "type": "EnumOptions", + "id": 3 + } + } + }, + "EnumValueDescriptorProto": { + "fields": { + "name": { + "type": "string", + "id": 1 + }, + "number": { + "type": "int32", + "id": 2 + }, + "options": { + "type": "EnumValueOptions", + "id": 3 + } + } + }, + "ServiceDescriptorProto": { + "fields": { + "name": { + "type": "string", + "id": 1 + }, + "method": { + "rule": "repeated", + "type": "MethodDescriptorProto", + "id": 2 + }, + "options": { + "type": "ServiceOptions", + "id": 3 + } + } + }, + "MethodDescriptorProto": { + "fields": { + "name": { + "type": "string", + "id": 1 + }, + "input_type": { + "type": "string", + "id": 2 + }, + "output_type": { + "type": "string", + "id": 3 + }, + "options": { + "type": "MethodOptions", + "id": 4 + }, + "client_streaming": { + "type": "bool", + "id": 5 + }, + "server_streaming": { + "type": "bool", + "id": 6 + } + } + }, + "FileOptions": { + "fields": { + "java_package": { + "type": "string", + "id": 1 + }, + "java_outer_classname": { + "type": "string", + "id": 8 + }, + "java_multiple_files": { + "type": "bool", + "id": 10 + }, + "java_generate_equals_and_hash": { + "type": "bool", + "id": 20, + "options": { + "deprecated": true + } + }, + "java_string_check_utf8": { + "type": "bool", + "id": 27 + }, + "optimize_for": { + "type": "OptimizeMode", + "id": 9, + "options": { + "default": "SPEED" + } + }, + "go_package": { + "type": "string", + "id": 11 + }, + "cc_generic_services": { + "type": "bool", + "id": 16 + }, + "java_generic_services": { + "type": "bool", + "id": 17 + }, + "py_generic_services": { + "type": "bool", + "id": 18 + }, + "deprecated": { + "type": "bool", + "id": 23 + }, + "cc_enable_arenas": { + "type": "bool", + "id": 31 + }, + "objc_class_prefix": { + "type": "string", + "id": 36 + }, + "csharp_namespace": { + "type": "string", + "id": 37 + }, + "uninterpreted_option": { + "rule": "repeated", + "type": "UninterpretedOption", + "id": 999 + } + }, + "extensions": [[1000, 536870911]], + "reserved": [[38, 38]], + "nested": { + "OptimizeMode": { + "values": { + "SPEED": 1, + "CODE_SIZE": 2, + "LITE_RUNTIME": 3 + } + } + } + }, + "MessageOptions": { + "fields": { + "message_set_wire_format": { + "type": "bool", + "id": 1 + }, + "no_standard_descriptor_accessor": { + "type": "bool", + "id": 2 + }, + "deprecated": { + "type": "bool", + "id": 3 + }, + "map_entry": { + "type": "bool", + "id": 7 + }, + "uninterpreted_option": { + "rule": "repeated", + "type": "UninterpretedOption", + "id": 999 + } + }, + "extensions": [[1000, 536870911]], + "reserved": [[8, 8]] + }, + "FieldOptions": { + "fields": { + "ctype": { + "type": "CType", + "id": 1, + "options": { + "default": "STRING" + } + }, + "packed": { + "type": "bool", + "id": 2 + }, + "jstype": { + "type": "JSType", + "id": 6, + "options": { + "default": "JS_NORMAL" + } + }, + "lazy": { + "type": "bool", + "id": 5 + }, + "deprecated": { + "type": "bool", + "id": 3 + }, + "weak": { + "type": "bool", + "id": 10 + }, + "uninterpreted_option": { + "rule": "repeated", + "type": "UninterpretedOption", + "id": 999 + } + }, + "extensions": [[1000, 536870911]], + "reserved": [[4, 4]], + "nested": { + "CType": { + "values": { + "STRING": 0, + "CORD": 1, + "STRING_PIECE": 2 + } + }, + "JSType": { + "values": { + "JS_NORMAL": 0, + "JS_STRING": 1, + "JS_NUMBER": 2 + } + } + } + }, + "OneofOptions": { + "fields": { + "uninterpreted_option": { + "rule": "repeated", + "type": "UninterpretedOption", + "id": 999 + } + }, + "extensions": [[1000, 536870911]] + }, + "EnumOptions": { + "fields": { + "allow_alias": { + "type": "bool", + "id": 2 + }, + "deprecated": { + "type": "bool", + "id": 3 + }, + "uninterpreted_option": { + "rule": "repeated", + "type": "UninterpretedOption", + "id": 999 + } + }, + "extensions": [[1000, 536870911]] + }, + "EnumValueOptions": { + "fields": { + "deprecated": { + "type": "bool", + "id": 1 + }, + "uninterpreted_option": { + "rule": "repeated", + "type": "UninterpretedOption", + "id": 999 + } + }, + "extensions": [[1000, 536870911]] + }, + "ServiceOptions": { + "fields": { + "deprecated": { + "type": "bool", + "id": 33 + }, + "uninterpreted_option": { + "rule": "repeated", + "type": "UninterpretedOption", + "id": 999 + } + }, + "extensions": [[1000, 536870911]] + }, + "MethodOptions": { + "fields": { + "deprecated": { + "type": "bool", + "id": 33 + }, + "uninterpreted_option": { + "rule": "repeated", + "type": "UninterpretedOption", + "id": 999 + } + }, + "extensions": [[1000, 536870911]] + }, + "UninterpretedOption": { + "fields": { + "name": { + "rule": "repeated", + "type": "NamePart", + "id": 2 + }, + "identifier_value": { + "type": "string", + "id": 3 + }, + "positive_int_value": { + "type": "uint64", + "id": 4 + }, + "negative_int_value": { + "type": "int64", + "id": 5 + }, + "double_value": { + "type": "double", + "id": 6 + }, + "string_value": { + "type": "bytes", + "id": 7 + }, + "aggregate_value": { + "type": "string", + "id": 8 + } + }, + "nested": { + "NamePart": { + "fields": { + "name_part": { + "rule": "required", + "type": "string", + "id": 1 + }, + "is_extension": { + "rule": "required", + "type": "bool", + "id": 2 + } + } + } + } + }, + "SourceCodeInfo": { + "fields": { + "location": { + "rule": "repeated", + "type": "Location", + "id": 1 + } + }, + "nested": { + "Location": { + "fields": { + "path": { + "rule": "repeated", + "type": "int32", + "id": 1 + }, + "span": { + "rule": "repeated", + "type": "int32", + "id": 2 + }, + "leading_comments": { + "type": "string", + "id": 3 + }, + "trailing_comments": { + "type": "string", + "id": 4 + }, + "leading_detached_comments": { + "rule": "repeated", + "type": "string", + "id": 6 + } + } + } + } + }, + "GeneratedCodeInfo": { + "fields": { + "annotation": { + "rule": "repeated", + "type": "Annotation", + "id": 1 + } + }, + "nested": { + "Annotation": { + "fields": { + "path": { + "rule": "repeated", + "type": "int32", + "id": 1 + }, + "source_file": { + "type": "string", + "id": 2 + }, + "begin": { + "type": "int32", + "id": 3 + }, + "end": { + "type": "int32", + "id": 4 + } + } + } + } + } + } + } + } + } + } +} diff --git a/packages/core/src/device/Device.ts b/packages/core/src/device/Device.ts index de3d78fff..77fc176dc 100644 --- a/packages/core/src/device/Device.ts +++ b/packages/core/src/device/Device.ts @@ -9,6 +9,7 @@ import { HardwareError, HardwareErrorCode, createDeferred, + createDeviceNotSupportMethodError, } from '@onekeyfe/hd-shared'; import { @@ -19,21 +20,30 @@ import { getDeviceLabel, getDeviceType, getDeviceUUID, + getFirmwareType, getLogger, - getMethodVersionRange, } from '../utils'; import { fixFeaturesFirmwareVersion, getPassphraseStateWithRefreshDeviceInfo, + supportInputPinOnSoftware, + supportModifyHomescreen, } from '../utils/deviceFeaturesUtils'; import { generateInstanceId } from '../utils/tracing'; // eslint-disable-next-line import/no-cycle import { DeviceCommands } from './DeviceCommands'; +import { deviceWalletSessionStore } from './DeviceWalletSessionStore'; import { type DeviceFirmwareRange, + DeviceModelToTypes, + DeviceTypeToModels, type Device as DeviceTyped, EOneKeyDeviceMode, type Features, + type IDeviceModel, + type IDeviceType, + type IVersionRange, + type SupportFeatureType, type UnavailableCapabilities, } from '../types'; import { DEVICE, UI_REQUEST } from '../events'; @@ -41,6 +51,11 @@ import { DataManager } from '../data-manager'; import TransportManager from '../data-manager/TransportManager'; import { toHardened } from '../api/helpers/pathUtils'; import { existCapability } from '../utils/capabilitieUtils'; +import { + PROTOCOL_V2_STATUS_DEVICE_INFO_REQUEST, + requestProtocolV2DeviceInfo, +} from '../protocols/protocol-v2/features'; +import { buildProtocolV1FeaturesPayload, buildProtocolV2FeaturesPayload } from '../deviceProfile'; import type { PROTO } from '../constants'; import type { @@ -49,8 +64,13 @@ import type { PassphraseRequestPayload, } from '../events'; import type { PassphrasePromptResponse } from './DeviceCommands'; -import type { Deferred } from '@onekeyfe/hd-shared'; -import type { OneKeyDeviceInfo as DeviceDescriptor } from '@onekeyfe/hd-transport'; +import type { Deferred, HardwareConnectProtocol } from '@onekeyfe/hd-shared'; +import type { + OneKeyDeviceInfo as DeviceDescriptor, + DeviceStatus, + ProtocolV2DeviceInfo, + Success, +} from '@onekeyfe/hd-transport'; import type DeviceConnector from './DeviceConnector'; export type InitOptions = { @@ -58,6 +78,8 @@ export type InitOptions = { deviceId?: string; passphraseState?: string; deriveCardano?: boolean; + connectProtocol?: HardwareConnectProtocol; + protocolV2DeviceInfoTimeoutMs?: number; }; export type RunOptions = { @@ -100,8 +122,6 @@ export interface Device { emit(type: K, ...args: DeviceEvents[K]): boolean; } -const deviceSessionCache: Record = {}; - /** * Pre-populate the device session cache with a known session ID. * @@ -119,8 +139,7 @@ export function preloadSessionCache( passphraseState: string, sessionId: string ): void { - const key = `${deviceId}@${passphraseState}`; - deviceSessionCache[key] = sessionId; + deviceWalletSessionStore.set(deviceId, passphraseState, sessionId); } export class Device extends EventEmitter { @@ -167,12 +186,19 @@ export class Device extends EventEmitter { private deviceAcquired = false; /** - * 设备信息 + * 唯一设备状态缓存。 + * + * V1 直接保存原生 Features;V2 保存由 DeviceInfoGet 映射出的 + * Features 视图。Device 不再保存 profile,结构化 DeviceProfile 只作为 + * getDeviceInfo() 的 API 返回值存在。 */ features: Features | undefined = undefined; /** - * 是否需要更新设备信息 + * 是否需要更新设备信息。 + * + * 历史名称保留用于兼容现有调用语义;对 V2 表示 features 需要由 + * DeviceInfoGet 刷新。 */ featuresNeedsReload = false; @@ -228,32 +254,38 @@ export class Device extends EventEmitter { // simplified object to pass via postMessage toMessageObject(): DeviceTyped | null { - if (this.isUnacquired() || !this.features) return null; + if (this.isUnacquired()) return null; const env = DataManager.getSettings('env'); - const deviceType = getDeviceType(this.features); + const deviceType = this.getCurrentDeviceType(); - const bleName = getDeviceBleName(this.features); - const label = getDeviceLabel(this.features); + const bleName = this.getCurrentBleName(); + const label = this.getCurrentLabel(); + const serialNo = this.getCurrentSerialNo(); + const connectId = this.getConnectId(); + const deviceId = this.getCurrentDeviceId() || null; + + const { features } = this; return { /** Android uses Mac address, iOS uses uuid, USB uses uuid */ - connectId: DataManager.isBleConnect(env) ? this.mainId || null : getDeviceUUID(this.features), + connectId: DataManager.isBleConnect(env) ? this.mainId || null : connectId, /** Hardware ID, will not change at any time */ - uuid: getDeviceUUID(this.features), + uuid: serialNo, commType: this.originalDescriptor.commType, sdkInstanceId: this.sdkInstanceId, instanceId: this.instanceId, createdAt: this.createdAt, deviceType, /** ID for current seeds, will clear after replace a new seed at device */ - deviceId: this.features.device_id || null, + deviceId, path: this.originalDescriptor?.path, bleName, name: bleName || label || `OneKey ${deviceType?.toUpperCase()}`, label: label || 'OneKey', mode: this.getMode(), - features: this.features, + features, + sessionId: this.features?.sessionId ?? null, firmwareVersion: this.getFirmwareVersion(), bleFirmwareVersion: this.getBLEFirmwareVersion(), unavailableCapabilities: this.unavailableCapabilities, @@ -264,13 +296,13 @@ export class Device extends EventEmitter { * Device connect * @returns {Promise} */ - connect() { + connect(connectProtocol?: HardwareConnectProtocol) { const env = DataManager.getSettings('env'); // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { if (DataManager.isBleConnect(env)) { try { - await this.acquire(); + await this.acquire(connectProtocol); resolve(true); } catch (error) { reject(error); @@ -280,7 +312,7 @@ export class Device extends EventEmitter { // 不存在 Session ID 或存在 Session ID 但设备在别处使用,都需要 acquire 获取最新 sessionID if (!this.mainId || (!this.isUsedHere() && this.originalDescriptor)) { try { - await this.acquire(); + await this.acquire(connectProtocol); resolve(true); } catch (error) { reject(error); @@ -295,29 +327,61 @@ export class Device extends EventEmitter { }); } - async acquire() { + async acquire( + connectProtocol?: HardwareConnectProtocol, + options?: { throwOnRunPromiseError?: boolean } + ) { const env = DataManager.getSettings('env'); const mainIdKey = DataManager.isBleConnect(env) ? 'id' : 'session'; + const expectedProtocol = connectProtocol ?? this.originalDescriptor.protocolType; try { + let acquireResult: unknown; if (DataManager.isBleConnect(env)) { - const res = await this.deviceConnector?.acquire(this.originalDescriptor.id); - this.mainId = (res as unknown as any).uuid ?? ''; + // forceCleanRunPromise=true(自 e21b83c6 引入,修复 Pro2 BLE 重连): + // acquire 意味着开启一个全新会话,transport 里残留的上一次 runPromise + // 必然属于已死亡的会话(如固件升级重启、探测中断),不清理会让新会话的 + // 调用被旧 promise 卡死。无法在 Device 层面区分“重连恢复”与普通 acquire, + // 因此对 BLE acquire 恒清理是有意为之。 + acquireResult = await this.deviceConnector?.acquire( + this.originalDescriptor.id, + undefined, + true, + expectedProtocol + ); + this.mainId = (acquireResult as any)?.uuid ?? ''; Log.debug('Expected uuid:', this.mainId); } else { - this.mainId = await this.deviceConnector?.acquire( + acquireResult = await this.deviceConnector?.acquire( this.originalDescriptor.path, - this.originalDescriptor.session + this.originalDescriptor.session, + undefined, + expectedProtocol ); + this.mainId = acquireResult as string | undefined; Log.debug('Expected session id:', this.mainId); } this.deviceAcquired = true; this.updateDescriptor({ [mainIdKey]: this.mainId } as unknown as DeviceDescriptor); + + // Propagate protocol version detected during acquire. + const detectedProtocol = + (acquireResult as { protocolType?: HardwareConnectProtocol } | undefined)?.protocolType ?? + TransportManager.transport?.getProtocolType?.( + DataManager.isBleConnect(env) ? this.originalDescriptor.id : this.originalDescriptor.path + ); + if (detectedProtocol) { + this.originalDescriptor.protocolType = detectedProtocol; + } + if (this.commands) { await this.commands.dispose(false); } this.commands = new DeviceCommands(this, this.mainId ?? ''); } catch (error) { + if (options?.throwOnRunPromiseError) { + throw error; + } if (this.runPromise) { this.runPromise.reject(error); } else { @@ -368,12 +432,10 @@ export class Device extends EventEmitter { } /** - * Pre-initialize: connect + Initialize ahead of the sign. Only runs the - * fallback init when features are missing (gate on `!this.features`, not - * isUsedHere which is always false on BLE); otherwise just records the mark. + * Pre-initialize: connect + Initialize ahead of the sign. */ async preInitialize(initOptions?: InitOptions) { - if (!this.features) { + if (this.isUnacquired()) { await this.acquire(); await this.initialize(initOptions); } @@ -419,23 +481,171 @@ export class Device extends EventEmitter { return this.commands; } - private generateStateKey(deviceId: string, passphraseState?: string) { - if (passphraseState) { - return `${deviceId}@${passphraseState}`; + /** + * 唯一协议判别器。 + * + * descriptor.protocolType 是协议探测后的结果;V2 features 由 DeviceInfoGet + * 映射产生。 + * 全 SDK 的协议分支都必须走这里,不要直接读 originalDescriptor.protocolType + * 或从 features 反推。 + */ + getProtocol(): 'V1' | 'V2' { + return this.originalDescriptor.protocolType === 'V2' ? 'V2' : 'V1'; + } + + isProtocolV2() { + return this.getProtocol() === 'V2'; + } + + getCurrentDeviceType() { + return getDeviceType(this.features); + } + + getCurrentDeviceId() { + return this.features?.deviceId || undefined; + } + + getCurrentSerialNo() { + return this.features ? getDeviceUUID(this.features) : ''; + } + + getConnectId() { + const serialNo = this.getCurrentSerialNo(); + if (serialNo) return serialNo; + + // connectId 是 SDK 内部连接路由 key。这里兜底到 transport descriptor, + // 不改变 features.serialNo / deviceId 的业务语义。 + return this.originalDescriptor.path || this.originalDescriptor.id || ''; + } + + getCurrentBleName() { + return getDeviceBleName(this.features); + } + + getCurrentLabel() { + return getDeviceLabel(this.features); + } + + getCurrentPassphraseProtection() { + return this.features?.passphraseProtection; + } + + getCurrentFirmwareType() { + return getFirmwareType(this.features); + } + + getCurrentFirmwareVersionString() { + return getDeviceFirmwareVersion(this.features)?.join('.'); + } + + getCurrentBLEFirmwareVersionString() { + if (!this.features) return undefined; + return getDeviceBLEFirmwareVersion(this.features).join('.'); + } + + getCurrentSafetyChecks() { + return this.features?.safetyChecks; + } + + getCurrentMethodVersionRange( + getVersionRange: (deviceModel: IDeviceType | IDeviceModel) => IVersionRange | undefined + ) { + const deviceType = this.getCurrentDeviceType(); + const versionRange = getVersionRange(deviceType); + if (versionRange) return versionRange; + + // Most-specific model first; must match getMethodVersionRange in deviceInfoUtils, + // otherwise e.g. Classic1s resolves the looser model_mini range before model_classic1s. + const modelFallbacks: IDeviceModel[] = [ + 'model_classic1s', + 'model_classic', + 'model_mini', + 'model_touch', + ]; + for (const model of modelFallbacks) { + if (DeviceTypeToModels[deviceType]?.includes(model)) { + const fallbackRange = getVersionRange(model); + if (fallbackRange) return fallbackRange; + } + } + return undefined; + } + + supportNewPassphrase(): SupportFeatureType { + const deviceType = this.getCurrentDeviceType(); + if ( + deviceType === EDeviceType.Touch || + deviceType === EDeviceType.Pro || + deviceType === EDeviceType.Pro2 + ) { + return { support: true }; + } + + const firmwareVersion = this.getCurrentFirmwareVersionString(); + return { + support: Boolean(firmwareVersion && semver.gte(firmwareVersion, '2.4.0')), + require: '2.4.0', + }; + } + + supportInputPinOnSoftware(): SupportFeatureType { + if (this.features) return supportInputPinOnSoftware(this.features); + + const deviceType = this.getCurrentDeviceType(); + if ( + deviceType === EDeviceType.Touch || + deviceType === EDeviceType.Pro || + deviceType === EDeviceType.Pro2 + ) { + return { support: false }; + } + + const firmwareVersion = this.getCurrentFirmwareVersionString(); + return { + support: Boolean(firmwareVersion && semver.gte(firmwareVersion, '2.3.0')), + require: '2.3.0', + }; + } + + supportModifyHomescreen(): SupportFeatureType { + // Pro2 走独立 1.x 版本线,不能套用 Touch/Pro 的 3.4.0 门槛(恒 false 且未来会误判)。 + // 依据:firmware-pro2 协议 schema(messages-protocol-v2.json)的 ApplySettings + // 包含 homescreen 字段,V2 固件从首个版本即支持修改主屏。 + if (this.isProtocolV2()) { + return { support: true }; + } + + if (this.features) return supportModifyHomescreen(this.features); + + const deviceType = this.getCurrentDeviceType(); + if (DeviceModelToTypes.model_mini.includes(deviceType)) { + return { support: true }; + } + + const firmwareVersion = this.getCurrentFirmwareVersionString(); + return { + support: Boolean(firmwareVersion && semver.gte(firmwareVersion, '3.4.0')), + }; + } + + private getSessionCacheDeviceKey(_deviceId?: string) { + const deviceId = _deviceId || this.getCurrentDeviceId(); + if (deviceId) return deviceId; + if (this.isProtocolV2()) { + return this.originalDescriptor.path || this.originalDescriptor.id; } - return deviceId; + return undefined; } getInternalState(_deviceId?: string) { - Log.debug('getInternalState session cache: ', deviceSessionCache); Log.debug( 'getInternalState session param: ', `device_id: ${_deviceId}`, - `features.device_id: ${this.features?.device_id}`, + `features.deviceId: ${this.features?.deviceId}`, `passphraseState: ${this.passphraseState}` ); - const deviceId = _deviceId || this.features?.device_id; + const deviceId = this.getSessionCacheDeviceKey(_deviceId); if (!deviceId) return undefined; // Security invariant: no passphraseState → no session lookup. // A previous fallback that scanned `${deviceId}@*` keys could silently @@ -445,15 +655,14 @@ export class Device extends EventEmitter { // downstream call carries it and hits the primary lookup below. if (!this.passphraseState) return undefined; - const usePassKey = this.generateStateKey(deviceId, this.passphraseState); - return deviceSessionCache[usePassKey]; + return deviceWalletSessionStore.get(deviceId, this.passphraseState); } // attach to pin to fix internal state updateInternalState( enablePassphrase: boolean, passphraseState: string | undefined, - deviceId: string, + deviceId: string | undefined, sessionId: string | null = null, featuresSessionId: string | null = null ) { @@ -462,66 +671,73 @@ export class Device extends EventEmitter { `device_id: ${deviceId}`, `enablePassphrase: ${enablePassphrase}`, `passphraseState: ${passphraseState}`, - `sessionId: ${sessionId}`, - `featuresSessionId: ${featuresSessionId}` + `hasSessionId: ${Boolean(sessionId)}`, + `hasFeaturesSessionId: ${Boolean(featuresSessionId)}` ); - if (enablePassphrase) { - // update the sessionId - if (sessionId) { - deviceSessionCache[this.generateStateKey(deviceId, passphraseState)] = sessionId; - } else if (featuresSessionId) { - deviceSessionCache[this.generateStateKey(deviceId, passphraseState)] = featuresSessionId; - } - } + const cacheDeviceKey = this.getSessionCacheDeviceKey(deviceId); + if (!cacheDeviceKey) return; - // delete the old sessionId - const oldKey = `${deviceId}`; - if (deviceSessionCache[oldKey]) { - delete deviceSessionCache[oldKey]; + if (enablePassphrase) { + const walletSessionId = + sessionId || featuresSessionId || deviceWalletSessionStore.getPending(cacheDeviceKey); + deviceWalletSessionStore.set(cacheDeviceKey, passphraseState, walletSessionId ?? undefined); } - Log.debug('updateInternalState session cache: ', deviceSessionCache); + deviceWalletSessionStore.deletePending(cacheDeviceKey); } private setInternalState(state: string, initSession?: boolean) { Log.debug( 'setInternalState session param: ', - `state: ${state}`, + `hasState: ${Boolean(state)}`, `initSession: ${initSession}`, - `device_id: ${this.features?.device_id}`, + `deviceId: ${this.features?.deviceId}`, `passphraseState: ${this.passphraseState}` ); - if (!this.features) return; if (!this.passphraseState && !initSession) return; - const deviceId = this.features?.device_id; + const deviceId = this.getSessionCacheDeviceKey(); if (!deviceId) return; - const key = this.generateStateKey(deviceId, this.passphraseState); - - if (state) { - deviceSessionCache[key] = state; + if (this.passphraseState) { + deviceWalletSessionStore.set(deviceId, this.passphraseState, state); + } else if (initSession) { + deviceWalletSessionStore.setPending(deviceId, state); } - Log.debug('setInternalState done session cache: ', deviceSessionCache); } clearInternalState(_deviceId?: string) { Log.debug('clearInternalState param: ', _deviceId); - const deviceId = _deviceId || this.features?.device_id; + const deviceId = this.getSessionCacheDeviceKey(_deviceId); if (!deviceId) return; - const key = `${deviceId}`; - delete deviceSessionCache[key]; + deviceWalletSessionStore.deletePending(deviceId); if (this.passphraseState) { - const usePassKey = this.generateStateKey(deviceId, this.passphraseState); - delete deviceSessionCache[usePassKey]; + deviceWalletSessionStore.delete(deviceId, this.passphraseState); } } async initialize(options?: InitOptions) { + // Protocol V2 不支持传统 Initialize,直接使用协议专用初始化流程。 + if (this.isProtocolV2()) { + this.passphraseState = options?.passphraseState; + if (this.features && !this.featuresNeedsReload && !options?.initSession) { + if (this.features.bootloaderMode) { + return; + } + // 不能直接信任缓存 features:设备端 wipe / 完成初始化 / 改 label 后 + // features 会永久陈旧。每次 run 做一次轻量 status 刷新(不含 fw/SE), + // 用字段级合并保留已有版本和 SE 信息。 + await this._refreshProtocolV2Status(options); + return; + } + await this._initializeProtocolV2(options); + return; + } + // Log.debug('initialize param:', options); this.passphraseState = options?.passphraseState; @@ -565,28 +781,124 @@ export class Device extends EventEmitter { } } + /** + * Device initialization over Protocol V2. + * + * Protocol V2 不走传统 Initialize/GetFeatures;直接用 DeviceInfoGet + * 生成唯一的 features 状态。 + */ + private async _initializeProtocolV2(options?: InitOptions) { + Log.debug('Initialize device via Protocol V2 features adapter'); + + try { + // 超时由 requestProtocolV2DeviceInfo 内部的 typedCall timeoutMs(默认 10s)负责, + // 不再额外包一层 Promise.race:外层 race 的 timer 不会清理, + // 且 reject 后底层调用仍会残留。 + const deviceInfo = await requestProtocolV2DeviceInfo({ + commands: this.commands, + timeoutMs: options?.protocolV2DeviceInfoTimeoutMs, + }); + // 默认请求不含 SE/hash 数据,scope 如实标注为 basic; + // 完整数据由 getDeviceInfo(scope:'verify'|'full') 获取。 + const features = this.updateProtocolV2Features(deviceInfo); + Log.debug('Protocol V2 features:', features); + this.featuresNeedsReload = false; + } catch (error) { + Log.error('Protocol V2 initialization failed:', error); + throw error; + } + } + + /** + * Protocol V2 的轻量状态刷新(每次 run 前调用)。 + * + * 请求 hw + coprocessor + status(不含 fw/SE target):status 提供 init_states / label / + * passphrase_enabled 等会在设备端变化的字段;hw/coprocessor 提供 serialNo / bleName。 + * versions 为空时按字段级合并保留旧值,verify 数据不会被降级。 + */ + private async _refreshProtocolV2Status(options?: InitOptions) { + try { + const deviceInfo = await requestProtocolV2DeviceInfo({ + commands: this.commands, + request: PROTOCOL_V2_STATUS_DEVICE_INFO_REQUEST, + timeoutMs: options?.protocolV2DeviceInfoTimeoutMs, + }); + const features = this.updateProtocolV2Features(deviceInfo); + Log.debug('Protocol V2 features (status refresh):', features); + } catch (error) { + Log.error('Protocol V2 status refresh failed:', error); + throw error; + } + } + async getFeatures() { + if (this.isProtocolV2()) { + const deviceInfo = await requestProtocolV2DeviceInfo({ + commands: this.commands, + }); + return this.updateProtocolV2Features(deviceInfo); + } + const { message } = await this.commands.typedCall('GetFeatures', 'Features', {}); this._updateFeatures(message); + return this.features; } - _updateFeatures(feat: Features, initSession?: boolean) { + _updateFeatures(protoFeatures: PROTO.Features | Features, initSession?: boolean) { + const previousCacheDeviceKey = this.getSessionCacheDeviceKey(); + let feat = + 'protocol' in protoFeatures + ? protoFeatures + : buildProtocolV1FeaturesPayload(protoFeatures, this.features); + // GetFeatures doesn't return 'session_id' - if (this.features && this.features.session_id && !feat.session_id) { - feat.session_id = this.features.session_id; - } - if (this.features && this.features.device_id && feat.session_id) { - this.setInternalState(feat.session_id, initSession); + if (this.features?.sessionId && !feat.sessionId) { + feat.sessionId = this.features.sessionId; } feat.unlocked = feat.unlocked ?? true; feat = fixFeaturesFirmwareVersion(feat); this.features = feat; + const nextCacheDeviceKey = this.getSessionCacheDeviceKey(); + if (previousCacheDeviceKey && nextCacheDeviceKey) { + deviceWalletSessionStore.migrateDeviceKey(previousCacheDeviceKey, nextCacheDeviceKey); + } + if (feat.deviceId && feat.sessionId) { + this.setInternalState(feat.sessionId, initSession); + } this.featuresNeedsReload = false; this.emit(DEVICE.FEATURES, this, feat); } + updateProtocolV2Features(deviceInfo?: ProtocolV2DeviceInfo) { + const previousCacheDeviceKey = this.getSessionCacheDeviceKey(); + const features = fixFeaturesFirmwareVersion( + buildProtocolV2FeaturesPayload(deviceInfo, this.features) + ); + this.features = features; + const nextCacheDeviceKey = this.getSessionCacheDeviceKey(); + if (previousCacheDeviceKey && nextCacheDeviceKey) { + deviceWalletSessionStore.migrateDeviceKey(previousCacheDeviceKey, nextCacheDeviceKey); + } + this.featuresNeedsReload = false; + this.emit(DEVICE.FEATURES, this, features); + return features; + } + + updateProtocolV2Status(status: DeviceStatus) { + const previousDeviceInfo = this.features?.raw?.protocolV2DeviceInfo; + const previousStatus = previousDeviceInfo?.status; + return this.updateProtocolV2Features({ + ...(previousDeviceInfo ?? {}), + protocol_version: previousDeviceInfo?.protocol_version ?? this.features?.protocolVersion ?? 2, + status: { + ...previousStatus, + ...status, + }, + }); + } + /** * 暂时只在 acquire 后更新 Session ID * 后续看是否有需要依据 listen 返回结果更新 @@ -605,7 +917,12 @@ export class Device extends EventEmitter { } if (forceUpdate) { - this.originalDescriptor = descriptor; + // 枚举得到的 descriptor 可能不带 protocolType(如 WebUSB enumerate), + // 不能让覆盖丢掉已探测的协议结果。 + this.originalDescriptor = { + ...descriptor, + protocolType: descriptor.protocolType ?? this.originalDescriptor.protocolType, + }; } } @@ -635,7 +952,7 @@ export class Device extends EventEmitter { const env = DataManager.getSettings('env'); if (env !== 'react-native') { try { - await this.acquire(); + await this.acquire(options.connectProtocol); } catch (error) { this.runPromise = null; return Promise.reject(error); @@ -748,7 +1065,7 @@ export class Device extends EventEmitter { } getMode() { - if (this.features?.bootloader_mode) { + if (this.features?.bootloaderMode) { // bootloader mode return EOneKeyDeviceMode.bootloader; } @@ -758,7 +1075,7 @@ export class Device extends EventEmitter { return EOneKeyDeviceMode.notInitialized; } - if (this.features?.no_backup) { + if (this.features?.noBackup) { // backup mode return EOneKeyDeviceMode.backupMode; } @@ -802,7 +1119,7 @@ export class Device extends EventEmitter { } isBootloader() { - return this.features && !!this.features.bootloader_mode; + return this.features && !!this.features.bootloaderMode; } isInitialized() { @@ -810,7 +1127,7 @@ export class Device extends EventEmitter { } isSeedless() { - return this.features && !!this.features.no_backup; + return this.features && !!this.features.noBackup; } isUnacquired(): boolean { @@ -819,7 +1136,7 @@ export class Device extends EventEmitter { hasUnexpectedMode(allow: string[], require: string[]) { // both allow and require cases might generate single unexpected mode - if (this.features) { + if (!this.isUnacquired()) { // allow cases if (this.isBootloader() && !allow.includes(UI_REQUEST.BOOTLOADER)) { return UI_REQUEST.BOOTLOADER; @@ -840,27 +1157,29 @@ export class Device extends EventEmitter { } hasUsePassphrase() { + const deviceType = this.getCurrentDeviceType(); const isModeT = - getDeviceType(this.features) === EDeviceType.Touch || - getDeviceType(this.features) === EDeviceType.Pro; - const preCheckTouch = isModeT && this.features?.unlocked === false; - - return this.features && (!!this.features.passphrase_protection || preCheckTouch); + deviceType === EDeviceType.Touch || + deviceType === EDeviceType.Pro || + deviceType === EDeviceType.Pro2; + const unlocked = this.features?.unlocked; + const preCheckTouch = isModeT && unlocked === false; + const passphraseProtection = this.getCurrentPassphraseProtection(); + + return Boolean(passphraseProtection === true || preCheckTouch); } checkDeviceId(deviceId: string) { - if (this.features) { - return this.features.device_id === deviceId; - } - return false; + return this.getCurrentDeviceId() === deviceId; } - async lockDevice() { + async lockDevice(): Promise { const res = await this.commands.typedCall('LockDevice', 'Success', {}); return res.message; } supportUnlockVersionRange(): DeviceFirmwareRange { + // 仅适用于 Protocol V1 的 Pro 系列;Pro2 走独立的 Protocol V2 解锁流程。 return { pro: { min: '4.15.0', @@ -869,9 +1188,30 @@ export class Device extends EventEmitter { } async unlockDevice() { - const firmwareVersion = getDeviceFirmwareVersion(this.features)?.join('.'); - const versionRange = getMethodVersionRange( - this.features, + if (this.isProtocolV2()) { + try { + await this.commands.typedCall('DeviceSessionAskPin', 'Success'); + } catch (error) { + const errorText = + error instanceof Error + ? `${error.name} ${error.message}` + : String((error as { message?: unknown } | null)?.message ?? error ?? ''); + if (errorText.includes('Failure_UnexpectedMessage')) { + throw createDeviceNotSupportMethodError('deviceUnlock', this.getCurrentFirmwareType()); + } + throw error; + } + + const { message: status } = await this.commands.typedCall( + 'DeviceStatusGet', + 'DeviceStatus', + {} + ); + return this.updateProtocolV2Status(status); + } + + const firmwareVersion = this.getCurrentFirmwareVersionString() ?? '0.0.0'; + const versionRange = this.getCurrentMethodVersionRange( type => this.supportUnlockVersionRange()[type] ); @@ -879,25 +1219,26 @@ export class Device extends EventEmitter { this.features, Enum_Capability.Capability_AttachToPin ); - const supportUnlock = - supportAttachPinCapability || (versionRange && semver.gte(firmwareVersion, versionRange.min)); + supportAttachPinCapability || + (versionRange && + semver.valid(firmwareVersion) && + semver.gte(firmwareVersion, versionRange.min)); if (supportUnlock) { const res = await this.commands.typedCall('UnLockDevice', 'UnLockDeviceResponse'); if (this.features) { this.features.unlocked = res.message.unlocked == null ? null : res.message.unlocked; - this.features.unlocked_attach_pin = + this.features.unlockedAttachPin = res.message.unlocked_attach_pin == null ? undefined : res.message.unlocked_attach_pin; - this.features.passphrase_protection = + this.features.passphraseProtection = res.message.passphrase_protection == null ? null : res.message.passphrase_protection; return Promise.resolve(this.features); } - const featuresRes = await this.commands.typedCall('GetFeatures', 'Features'); - this._updateFeatures(featuresRes.message); - return Promise.resolve(featuresRes.message); + const features = await this.getFeatures(); + return Promise.resolve(features); } const { type } = await this.commands.typedCall('GetAddress', 'Address', { @@ -911,9 +1252,8 @@ export class Device extends EventEmitter { if (type === 'CallMethodError') { throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'unlock device error'); } - const res = await this.commands.typedCall('GetFeatures', 'Features'); - this._updateFeatures(res.message); - return Promise.resolve(res.message); + const features = await this.getFeatures(); + return Promise.resolve(features); } async checkPassphraseStateSafety( @@ -921,7 +1261,7 @@ export class Device extends EventEmitter { useEmptyPassphrase?: boolean, skipPassphraseCheck?: boolean ) { - if (!this.features) return false; + if (this.isUnacquired()) return false; const { passphraseState: newPassphraseState, unlockedAttachPin } = await getPassphraseStateWithRefreshDeviceInfo(this, { @@ -929,14 +1269,11 @@ export class Device extends EventEmitter { onlyMainPin: useEmptyPassphrase, }); - if (skipPassphraseCheck) { - return true; - } - // Main wallet and unlock Attach Pin, throw safe error const mainWalletUseAttachPin = unlockedAttachPin && useEmptyPassphrase; const useErrorAttachPin = unlockedAttachPin && passphraseState && passphraseState !== newPassphraseState; + const passphraseStateMismatch = !!passphraseState && passphraseState !== newPassphraseState; Log.debug('Check passphrase state safety: ', { passphraseState, @@ -945,6 +1282,14 @@ export class Device extends EventEmitter { useEmptyPassphrase, }); + if (skipPassphraseCheck) { + if (passphraseStateMismatch) { + this.clearInternalState(); + return false; + } + return true; + } + if (mainWalletUseAttachPin || useErrorAttachPin) { try { await this.lockDevice(); @@ -956,7 +1301,7 @@ export class Device extends EventEmitter { } // When exists passphraseState, check passphraseState - if (passphraseState && passphraseState !== newPassphraseState) { + if (passphraseStateMismatch) { this.clearInternalState(); return false; } diff --git a/packages/core/src/device/DeviceCommands.ts b/packages/core/src/device/DeviceCommands.ts index 5a8e1ba30..fa2431be6 100644 --- a/packages/core/src/device/DeviceCommands.ts +++ b/packages/core/src/device/DeviceCommands.ts @@ -2,7 +2,7 @@ import { ERRORS, HardwareError, HardwareErrorCode } from '@onekeyfe/hd-shared'; import TransportManager from '../data-manager/TransportManager'; import DataManager from '../data-manager/DataManager'; -import { LoggerNames, getDeviceType, getLogger, patchFeatures } from '../utils'; +import { LoggerNames, getLogger, patchFeatures } from '../utils'; import { DEVICE, type PassphraseRequestPayload } from '../events'; import { DeviceModelToTypes } from '../types'; import { @@ -12,7 +12,12 @@ import { } from '../utils/tracing'; import type { Device } from './Device'; -import type { FailureType, Messages, Transport } from '@onekeyfe/hd-transport'; +import type { + FailureType, + Messages, + Transport, + TransportCallOptions, +} from '@onekeyfe/hd-transport'; export type PassphrasePromptResponse = { passphrase?: string; @@ -22,15 +27,132 @@ export type PassphrasePromptResponse = { }; type MessageType = Messages.MessageType; -type MessageKey = keyof MessageType; +type MessageKey = Extract; export type TypedResponseMessage = { type: T; message: MessageType[T]; }; type TypedCallResponseMap = { - [K in keyof MessageType]: TypedResponseMessage; + [K in MessageKey]: TypedResponseMessage; +}; +export type DefaultMessageResponse = TypedCallResponseMap[MessageKey]; + +const MAX_DEBUG_ARRAY_ITEMS = 20; +const MAX_DEBUG_OBJECT_KEYS = 40; +const MAX_DEBUG_STRING_LENGTH = 512; +const MAX_DEBUG_DEPTH = 4; +const HIGH_VOLUME_DEBUG_CALLS = new Set([ + 'FilesystemFileRead', + 'FilesystemFileWrite', + 'FileRead', + 'FileWrite', + 'EmmcFileRead', + 'EmmcFileWrite', + 'FirmwareUpload', + 'ResourceAck', +]); + +function shouldReduceDebugForCall(type: string) { + return HIGH_VOLUME_DEBUG_CALLS.has(type); +} + +function getBinaryByteLength(value: unknown): number | undefined { + if (value instanceof ArrayBuffer) { + return value.byteLength; + } + + if (ArrayBuffer.isView(value)) { + return value.byteLength; + } + + if (typeof Blob !== 'undefined' && value instanceof Blob) { + return value.size; + } + + return undefined; +} + +function summarizeRedactedData(value: unknown): string { + const byteLength = getBinaryByteLength(value); + if (byteLength !== undefined) { + return `[redacted data: ${byteLength} bytes]`; + } + + if (typeof value === 'string') { + return `[redacted data: string length=${value.length}]`; + } + + if (Array.isArray(value)) { + return `[redacted data: array length=${value.length}]`; + } + + if (value && typeof value === 'object') { + return `[redacted data: object keys=${Object.keys(value).length}]`; + } + + return `[redacted data: ${typeof value}]`; +} + +function sanitizeDebugPayload(value: unknown, key = '', depth = 0): unknown { + if (key === 'data' && value !== null && value !== undefined) { + return summarizeRedactedData(value); + } + + const byteLength = getBinaryByteLength(value); + if (byteLength !== undefined) { + return `[binary: ${byteLength} bytes]`; + } + + if (typeof value === 'string') { + return value.length > MAX_DEBUG_STRING_LENGTH + ? `${value.slice(0, MAX_DEBUG_STRING_LENGTH)}... (len=${value.length})` + : value; + } + + if (!value || typeof value !== 'object') { + return value; + } + + if (depth >= MAX_DEBUG_DEPTH) { + return Array.isArray(value) + ? `[array length=${value.length}]` + : `[object keys=${Object.keys(value).length}]`; + } + + if (Array.isArray(value)) { + const items = value + .slice(0, MAX_DEBUG_ARRAY_ITEMS) + .map(item => sanitizeDebugPayload(item, key, depth + 1)); + if (value.length > MAX_DEBUG_ARRAY_ITEMS) { + items.push(`... (${value.length - MAX_DEBUG_ARRAY_ITEMS} more)`); + } + return items; + } + + const entries = Object.entries(value).slice(0, MAX_DEBUG_OBJECT_KEYS); + const sanitized: Record = {}; + entries.forEach(([entryKey, entryValue]) => { + sanitized[entryKey] = sanitizeDebugPayload(entryValue, entryKey, depth + 1); + }); + if (Object.keys(value).length > MAX_DEBUG_OBJECT_KEYS) { + sanitized.__truncated__ = `${Object.keys(value).length - MAX_DEBUG_OBJECT_KEYS} more keys`; + } + return sanitized; +} + +/** + * 交互式 Ack(ButtonAck / PinMatrixAck / PassphraseAck 等)不应继承原调用的 + * timeoutMs:设备在等待用户操作(确认、输入 PIN/passphrase),思考时间不可预估, + * 沿用业务调用的超时会把用户操作截断。仅保留 expectedTypes / intermediateTypes + * 等与响应类型相关的选项。 + */ +const stripInteractiveAckTimeout = ( + options?: TransportCallOptions +): TransportCallOptions | undefined => { + if (!options) return options; + const { timeoutMs: _timeoutMs, ...rest } = options; + return rest; }; -export type DefaultMessageResponse = TypedCallResponseMap[keyof MessageType]; const assertType = (res: DefaultMessageResponse, resType: string | string[]) => { const splitResTypes = Array.isArray(resType) ? resType : resType.split('|'); @@ -224,17 +346,21 @@ export class DeviceCommands { // Sends an async message to the opened device. async call( type: MessageKey, - msg: DefaultMessageResponse['message'] = {} + msg?: DefaultMessageResponse['message'], + options?: TransportCallOptions ): Promise { - Log.debug('[DeviceCommands] [call] Sending', type); + const shouldReduceDebug = shouldReduceDebugForCall(type); + if (!shouldReduceDebug) { + Log.debug('[DeviceCommands] [call] Sending', type); + } try { - const promise = this.transport.call(this.mainId, type, msg) as any; + const promise = this.transport.call(this.mainId, type, msg ?? {}, options) as any; this.callPromise = promise; const res = await promise; if (res.type === 'Failure') { LogCore.debug('[DeviceCommands] [call] Received', res.type, res.message); - } else { + } else if (!shouldReduceDebug) { LogCore.debug('[DeviceCommands] [call] Received', res.type); } return res; @@ -283,19 +409,22 @@ export class DeviceCommands { typedCall( type: T, resType: R, - msg?: MessageType[T] + msg?: MessageType[T], + options?: TransportCallOptions ): Promise; typedCall( type: T, resType: R, - msg?: MessageType[T] + msg?: MessageType[T], + options?: TransportCallOptions ): Promise>; async typedCall( type: MessageKey, resType: MessageKey | MessageKey[], - msg?: DefaultMessageResponse['message'] + msg?: DefaultMessageResponse['message'], + options?: TransportCallOptions ) { if (this.disposed) { throw ERRORS.TypedError( @@ -312,16 +441,28 @@ export class DeviceCommands { 'PassphraseAck', 'Cancel', 'BixinPinInputOnDevice', + 'FilesystemFileRead', + 'FilesystemFileWrite', + 'FileRead', + 'FileWrite', + 'EmmcFileRead', + 'EmmcFileWrite', + 'FirmwareUpload', + 'ResourceAck', ] as any; if (!skipTypes.includes(type) && msg) { // Use debug channel to avoid noise escalation - Log.debug('[DeviceCommands] [typedCall] Sending payload', type, msg); + Log.debug('[DeviceCommands] [typedCall] Sending payload', type, sanitizeDebugPayload(msg)); } } catch (e) { // ignore logging errors } - const response = await this._commonCall(type, msg); + const expectedTypes = Array.isArray(resType) ? resType : resType.split('|'); + const response = await this._commonCall(type, msg, { + ...options, + expectedTypes: options?.expectedTypes ?? expectedTypes, + }); try { assertType(response, resType); } catch (error) { @@ -334,6 +475,12 @@ export class DeviceCommands { // throw bridge network error if (error instanceof HardwareError) { if (error.errorCode === HardwareErrorCode.ResponseUnexpectTypeError) { + Log.debug('[DeviceCommands] [typedCall] Unexpected response type', { + request: type, + expected: resType, + received: response.type, + response: sanitizeDebugPayload(response.message), + }); // Do not intercept CallMethodError // Do not intercept “assertType: Response of unexpected type” error // Blocking the above two messages will not know what the specific error message is, and the specific error should be handled by the subsequent business logic. @@ -347,7 +494,7 @@ export class DeviceCommands { if (error.message.indexOf('BridgeDeviceDisconnected') > -1) { throw ERRORS.TypedError(HardwareErrorCode.BridgeDeviceDisconnected); } - throw ERRORS.TypedError(HardwareErrorCode.ResponseUnexpectTypeError); + throw error; } } else { // throw error anyway, next call should be resolved properly// throw error anyway, next call should be resolved properly @@ -357,20 +504,27 @@ export class DeviceCommands { return response; } - async _commonCall(type: MessageKey, msg?: DefaultMessageResponse['message']) { - const resp = await this.call(type, msg); - return this._filterCommonTypes(resp, type); + async _commonCall( + type: MessageKey, + msg?: DefaultMessageResponse['message'], + options?: TransportCallOptions + ) { + const resp = await this.call(type, msg, options); + return this._filterCommonTypes(resp, type, options); } _filterCommonTypes( res: DefaultMessageResponse, - callType: MessageKey + callType: MessageKey, + options?: TransportCallOptions ): Promise { try { - if (DataManager.getSettings('env') === 'react-native') { - Log.debug('_filterCommonTypes: ', JSON.stringify(res)); + if (shouldReduceDebugForCall(callType)) { + // 高频文件写入每个 chunk 都会经过这里,避免 debug log 反向拖慢传输。 + } else if (DataManager.getSettings('env') === 'react-native') { + Log.debug('_filterCommonTypes: ', JSON.stringify(sanitizeDebugPayload(res))); } else { - Log.debug('_filterCommonTypes: ', res); + Log.debug('_filterCommonTypes: ', sanitizeDebugPayload(res)); } } catch (error) { // ignore @@ -378,8 +532,9 @@ export class DeviceCommands { this.device.clearCancelableAction(); if (res.type === 'Failure') { - const { code, message } = res.message as { + const { code, subcode, message } = res.message as { code?: string | FailureType; + subcode?: number; message?: string; }; let error: HardwareError | null = null; @@ -423,8 +578,13 @@ export class DeviceCommands { } if (code === 'Failure_ProcessError') { - // Handle firmware verification failures - if ( + if (subcode === 9) { + error = ERRORS.TypedError(HardwareErrorCode.DeviceLocked, message, { + failureCode: code, + subcode, + firmwareMessage: message, + }); + } else if ( message?.includes('Bootloader file verify failed') || message?.includes('verify failed') ) { @@ -461,7 +621,7 @@ export class DeviceCommands { } if (res.type === 'ButtonRequest') { - const deviceType = getDeviceType(this.device.features); + const deviceType = this.device.getCurrentDeviceType(); if (DeviceModelToTypes.model_mini.includes(deviceType)) { this.device.setCancelableAction(() => this.cancelDeviceOnOneKeyDevice()); } else { @@ -472,7 +632,7 @@ export class DeviceCommands { } else { this.device.emit(DEVICE.BUTTON, this.device, res.message); } - return this._commonCall('ButtonAck', {}); + return this._commonCall('ButtonAck', {}, stripInteractiveAckTimeout(options)); } if (res.type === 'EntropyRequest') { @@ -485,11 +645,15 @@ export class DeviceCommands { if (pin === '@@ONEKEY_INPUT_PIN_IN_DEVICE') { // only classic\1s\mini\pure this.device.setCancelableAction(() => this.cancelDeviceOnOneKeyDevice()); - return this._commonCall('BixinPinInputOnDevice').finally(() => { + return this._commonCall( + 'BixinPinInputOnDevice', + {}, + stripInteractiveAckTimeout(options) + ).finally(() => { this.device.clearCancelableAction(); }); } - return this._commonCall('PinMatrixAck', { pin }); + return this._commonCall('PinMatrixAck', { pin }, stripInteractiveAckTimeout(options)); }, error => Promise.reject(error) ); @@ -504,12 +668,20 @@ export class DeviceCommands { // Attach PIN on device if (attachPinOnDevice && existsAttachPinUser) { - return this._commonCall('PassphraseAck', { on_device_attach_pin: true }); + return this._commonCall( + 'PassphraseAck', + { on_device_attach_pin: true }, + stripInteractiveAckTimeout(options) + ); } return !passphraseOnDevice - ? this._commonCall('PassphraseAck', { passphrase }) - : this._commonCall('PassphraseAck', { on_device: true }); + ? this._commonCall('PassphraseAck', { passphrase }, stripInteractiveAckTimeout(options)) + : this._commonCall( + 'PassphraseAck', + { on_device: true }, + stripInteractiveAckTimeout(options) + ); }); } diff --git a/packages/core/src/device/DeviceConnector.ts b/packages/core/src/device/DeviceConnector.ts index 1aed69f62..5f1e8f7ba 100644 --- a/packages/core/src/device/DeviceConnector.ts +++ b/packages/core/src/device/DeviceConnector.ts @@ -1,3 +1,5 @@ +import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared'; + import { safeThrowError } from '../constants'; import { DataManager } from '../data-manager'; import TransportManager from '../data-manager/TransportManager'; @@ -6,6 +8,7 @@ import { resolveAfter } from '../utils/promiseUtils'; import { LoggerNames, getLogger } from '../utils'; import type { DeviceDescriptorDiff } from './DevicePool'; +import type { HardwareConnectProtocol } from '@onekeyfe/hd-shared'; import type { OneKeyDeviceInfo as DeviceDescriptor, Transport } from '@onekeyfe/hd-transport'; const Log = getLogger(LoggerNames.DeviceConnector); @@ -75,15 +78,37 @@ export default class DeviceConnector { this.listening = false; } - async acquire(path: string, session?: string | null, forceCleanRunPromise?: boolean) { - Log.debug('acquire', path, session); + async acquire( + path: string, + session?: string | null, + forceCleanRunPromise?: boolean, + connectProtocol?: HardwareConnectProtocol + ) { + Log.debug('acquire', path, session, connectProtocol); const env = DataManager.getSettings('env'); try { let res; if (DataManager.isBleConnect(env)) { - res = await this.transport.acquire({ uuid: path, forceCleanRunPromise }); + res = await this.transport.acquire({ + uuid: path, + forceCleanRunPromise, + expectedProtocol: connectProtocol, + }); } else { - res = await this.transport.acquire({ path, previous: session ?? null }); + res = await this.transport.acquire({ + path, + previous: session ?? null, + expectedProtocol: connectProtocol, + }); + } + if (connectProtocol) { + const detectedProtocol = this.transport.getProtocolType(path); + if (detectedProtocol !== connectProtocol) { + throw ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + `Device protocol mismatch: expected ${connectProtocol}, detected ${detectedProtocol}` + ); + } } return res; } catch (error) { diff --git a/packages/core/src/device/DevicePool.ts b/packages/core/src/device/DevicePool.ts index 1f027f46e..209d8097f 100644 --- a/packages/core/src/device/DevicePool.ts +++ b/packages/core/src/device/DevicePool.ts @@ -3,7 +3,7 @@ import EventEmitter from 'events'; // eslint-disable-next-line import/no-cycle import { Device } from './Device'; import { DEVICE } from '../events'; -import { LoggerNames, getDeviceUUID, getLogger } from '../utils'; +import { LoggerNames, getLogger } from '../utils'; import type { InitOptions } from './Device'; import type { OneKeyDeviceInfo as DeviceDescriptor } from '@onekeyfe/hd-transport'; @@ -118,14 +118,14 @@ export class DevicePool extends EventEmitter { for await (const descriptor of descriptorList) { const device = await this._createDevice(descriptor, initOptions); - if (device.features) { - const uuid = getDeviceUUID(device.features); - if (this.devicesCache[uuid]) { - const cache = this.devicesCache[uuid]; + const connectId = device.getConnectId(); + if (connectId) { + if (this.devicesCache[connectId]) { + const cache = this.devicesCache[connectId]; cache.updateDescriptor(descriptor, true); } - this.devicesCache[uuid] = device; - devices[uuid] = device; + this.devicesCache[connectId] = device; + devices[connectId] = device; } deviceList.push(device); @@ -150,7 +150,7 @@ export class DevicePool extends EventEmitter { if (!device) { device = Device.fromDescriptor(descriptor); device.deviceConnector = this.connector; - await device.connect(); + await device.connect(initOptions?.connectProtocol); await device.initialize(initOptions); await device.release(); } diff --git a/packages/core/src/device/DeviceWalletSessionStore.ts b/packages/core/src/device/DeviceWalletSessionStore.ts new file mode 100644 index 000000000..d12c96912 --- /dev/null +++ b/packages/core/src/device/DeviceWalletSessionStore.ts @@ -0,0 +1,78 @@ +export class DeviceWalletSessionStore { + private readonly walletSessions = new Map>(); + + private readonly pendingSessions = new Map(); + + get(deviceKey: string, passphraseState?: string) { + if (!deviceKey || !passphraseState) return undefined; + return this.walletSessions.get(deviceKey)?.get(passphraseState); + } + + set(deviceKey: string, passphraseState?: string, sessionId?: string) { + if (!deviceKey || !passphraseState || !sessionId) return; + let deviceSessions = this.walletSessions.get(deviceKey); + if (!deviceSessions) { + deviceSessions = new Map(); + this.walletSessions.set(deviceKey, deviceSessions); + } + deviceSessions.set(passphraseState, sessionId); + } + + setPending(deviceKey: string, sessionId?: string) { + if (!deviceKey || !sessionId) return; + this.pendingSessions.set(deviceKey, sessionId); + } + + getPending(deviceKey: string) { + if (!deviceKey) return undefined; + return this.pendingSessions.get(deviceKey); + } + + delete(deviceKey: string, passphraseState?: string) { + if (!deviceKey || !passphraseState) return; + const deviceSessions = this.walletSessions.get(deviceKey); + if (!deviceSessions) return; + deviceSessions.delete(passphraseState); + if (deviceSessions.size === 0) { + this.walletSessions.delete(deviceKey); + } + } + + deletePending(deviceKey: string) { + if (!deviceKey) return; + this.pendingSessions.delete(deviceKey); + } + + deleteDevice(deviceKey: string) { + if (!deviceKey) return; + this.walletSessions.delete(deviceKey); + this.pendingSessions.delete(deviceKey); + } + + migrateDeviceKey(from: string, to: string) { + if (!from || !to || from === to) return; + + const sourceSessions = this.walletSessions.get(from); + if (sourceSessions) { + const targetSessions = this.walletSessions.get(to) ?? new Map(); + this.walletSessions.set(to, targetSessions); + sourceSessions.forEach((sessionId, passphraseState) => { + targetSessions.set(passphraseState, sessionId); + }); + this.walletSessions.delete(from); + } + + const pendingSession = this.pendingSessions.get(from); + if (pendingSession) { + this.pendingSessions.set(to, pendingSession); + this.pendingSessions.delete(from); + } + } + + clear() { + this.walletSessions.clear(); + this.pendingSessions.clear(); + } +} + +export const deviceWalletSessionStore = new DeviceWalletSessionStore(); diff --git a/packages/core/src/deviceProfile/buildDeviceFeatures.ts b/packages/core/src/deviceProfile/buildDeviceFeatures.ts new file mode 100644 index 000000000..72c7f49ae --- /dev/null +++ b/packages/core/src/deviceProfile/buildDeviceFeatures.ts @@ -0,0 +1,373 @@ +import { EDeviceType, EFirmwareType } from '@onekeyfe/hd-shared'; + +import { + isProtocolV2BootloaderDeviceInfo, + isProtocolV2RomloaderDeviceInfo, +} from '../protocols/protocol-v2/features'; + +import type { DeviceFirmwareImageInfo, ProtocolV2DeviceInfo } from '@onekeyfe/hd-transport'; +import type { PROTO } from '../constants'; +import type { DeviceFeaturesMode, Features } from '../types'; + +type ProtocolV1FeaturesCompat = PROTO.Features & + Partial & { + protocol_version?: number | null; + onekey_version?: string; + onekey_serial?: string; + }; + +const getImageVersion = (image?: DeviceFirmwareImageInfo | null) => image?.version ?? null; + +const bytesToHex = (value: unknown): string | undefined => { + if (!value) return undefined; + if (typeof value === 'string') return value; + if (value instanceof Uint8Array) { + return Array.from(value) + .map(byte => byte.toString(16).padStart(2, '0')) + .join(''); + } + if (Array.isArray(value)) { + return value.map(byte => Number(byte).toString(16).padStart(2, '0')).join(''); + } + return undefined; +}; + +const getImageBuildId = (image?: DeviceFirmwareImageInfo | null) => image?.build_id ?? undefined; + +const getImageHash = (image?: DeviceFirmwareImageInfo | null) => bytesToHex(image?.hash); + +const firstValue = (...values: Array) => + values.find(value => value !== undefined && value !== null); + +const firstMeaningfulVersion = (...versions: Array) => + versions.find(version => Boolean(version && version !== '0.0.0')) ?? null; + +const versionFromParts = (major?: number | null, minor?: number | null, patch?: number | null) => + [major, minor, patch].map(part => Number(part) || 0).join('.'); + +const getProtocolV1Mode = (features: ProtocolV1FeaturesCompat): Features['mode'] => { + if (features.bootloader_mode === true) return 'bootloader'; + if (features.initialized === false) return 'notInitialized'; + if (features.initialized === true) return 'normal'; + return 'unknown'; +}; + +const getProtocolV1DeviceType = (features: ProtocolV1FeaturesCompat): Features['deviceType'] => { + const onekeyDeviceType = features.onekey_device_type as string | null | undefined; + switch (onekeyDeviceType) { + case 'CLASSIC': + return EDeviceType.Classic; + case 'CLASSIC1S': + return EDeviceType.Classic1s; + case 'MINI': + return EDeviceType.Mini; + case 'TOUCH': + return EDeviceType.Touch; + case 'PRO': + return EDeviceType.Pro; + case 'PRO2': + case 'pro2': + return EDeviceType.Pro2; + case 'PURE': + return EDeviceType.ClassicPure; + default: + break; + } + + const serialNo = features.onekey_serial_no || features.onekey_serial || features.serial_no || ''; + const prefix = serialNo.slice(0, 2).toLowerCase(); + if (prefix === 'bi' || prefix === 'cl') return EDeviceType.Classic; + if (prefix === 'cp') return EDeviceType.ClassicPure; + if (prefix === 'mi') return EDeviceType.Mini; + if (prefix === 'tc') return EDeviceType.Touch; + if (prefix === 'pr') return EDeviceType.Pro; + if (prefix === 'p2') return EDeviceType.Pro2; + if (!serialNo && features.bootloader_mode === true && features.model === '1') { + return EDeviceType.Classic; + } + return EDeviceType.Unknown; +}; + +const getProtocolV1FirmwareType = ( + features: ProtocolV1FeaturesCompat +): Features['firmwareType'] => { + if (features.fw_vendor === 'OneKey Bitcoin-only') return EFirmwareType.BitcoinOnly; + return EFirmwareType.Universal; +}; + +const getProtocolV1Label = ( + features: ProtocolV1FeaturesCompat, + deviceType: Features['deviceType'], + bleName: string | null +) => { + if (features.label) return features.label; + if (bleName) return bleName; + if (deviceType === EDeviceType.Unknown) return null; + if (deviceType === EDeviceType.ClassicPure) return 'OneKey Classic 1S'; + return `OneKey ${deviceType.charAt(0).toUpperCase()}${deviceType.slice(1)}`; +}; + +export const buildProtocolV1FeaturesPayload = ( + protocolV1Features: PROTO.Features, + previous?: Features +): Features => { + const features = protocolV1Features as ProtocolV1FeaturesCompat; + const firmwareVersion = firstMeaningfulVersion( + features.onekey_firmware_version, + features.onekey_version, + versionFromParts(features.major_version, features.minor_version, features.patch_version) + ); + const bootloaderVersion = firstMeaningfulVersion( + features.onekey_boot_version, + features.bootloader_version, + features.bootloader_mode + ? versionFromParts(features.major_version, features.minor_version, features.patch_version) + : null + ); + const boardVersion = firstMeaningfulVersion( + features.onekey_board_version, + features.boardloader_version + ); + const bleVersion = firstMeaningfulVersion(features.onekey_ble_version, features.ble_ver); + const serialNo = features.onekey_serial_no || features.onekey_serial || features.serial_no || ''; + const bleName = features.onekey_ble_name || features.ble_name || null; + const deviceType = getProtocolV1DeviceType(features); + const sessionId = features.session_id ?? previous?.sessionId ?? null; + + return { + protocol: 'V1', + protocolVersion: features.protocol_version ?? previous?.protocolVersion ?? 1, + deviceType, + firmwareType: getProtocolV1FirmwareType(features), + model: features.model ?? null, + vendor: features.vendor ?? null, + deviceId: features.device_id ?? null, + serialNo, + label: getProtocolV1Label(features, deviceType, bleName), + bleName, + capabilities: features.capabilities ?? [], + mode: getProtocolV1Mode(features), + initialized: features.initialized ?? null, + bootloaderMode: features.bootloader_mode ?? null, + unlocked: features.unlocked ?? true, + firmwarePresent: features.firmware_present ?? null, + passphraseProtection: features.passphrase_protection ?? null, + pinProtection: features.pin_protection ?? null, + backupRequired: features.needs_backup ?? null, + noBackup: features.no_backup ?? null, + unfinishedBackup: features.unfinished_backup ?? null, + recoveryMode: features.recovery_mode ?? null, + language: features.language ?? null, + bleEnabled: features.ble_enable ?? null, + sdCardPresent: features.sd_card_present ?? null, + sdProtection: features.sd_protection ?? null, + wipeCodeProtection: features.wipe_code_protection ?? null, + passphraseAlwaysOnDevice: features.passphrase_always_on_device ?? null, + safetyChecks: features.safety_checks ?? null, + autoLockDelayMs: features.auto_lock_delay_ms ?? null, + displayRotation: features.display_rotation ?? null, + experimentalFeatures: features.experimental_features ?? null, + firmwareVersion, + bootloaderVersion, + boardVersion, + bleVersion, + se01Version: firstMeaningfulVersion(features.onekey_se01_version), + se02Version: firstMeaningfulVersion(features.onekey_se02_version), + se03Version: firstMeaningfulVersion(features.onekey_se03_version), + se04Version: firstMeaningfulVersion(features.onekey_se04_version), + se01BootVersion: firstMeaningfulVersion(features.onekey_se01_boot_version), + se02BootVersion: firstMeaningfulVersion(features.onekey_se02_boot_version), + se03BootVersion: firstMeaningfulVersion(features.onekey_se03_boot_version), + se04BootVersion: firstMeaningfulVersion(features.onekey_se04_boot_version), + seVersion: features.se_ver ?? null, + verify: { + firmwareBuildId: features.onekey_firmware_build_id, + firmwareHash: features.onekey_firmware_hash, + bootloaderBuildId: features.onekey_boot_build_id, + bootloaderHash: features.onekey_boot_hash, + boardBuildId: features.onekey_board_build_id, + boardHash: features.onekey_board_hash, + bleBuildId: features.onekey_ble_build_id, + bleHash: features.onekey_ble_hash, + se01BuildId: features.onekey_se01_build_id, + se01Hash: features.onekey_se01_hash, + se02BuildId: features.onekey_se02_build_id, + se02Hash: features.onekey_se02_hash, + se03BuildId: features.onekey_se03_build_id, + se03Hash: features.onekey_se03_hash, + se04BuildId: features.onekey_se04_build_id, + se04Hash: features.onekey_se04_hash, + se01BootBuildId: features.onekey_se01_boot_build_id, + se01BootHash: features.onekey_se01_boot_hash, + se02BootBuildId: features.onekey_se02_boot_build_id, + se02BootHash: features.onekey_se02_boot_hash, + se03BootBuildId: features.onekey_se03_boot_build_id, + se03BootHash: features.onekey_se03_boot_hash, + se04BootBuildId: features.onekey_se04_boot_build_id, + se04BootHash: features.onekey_se04_boot_hash, + }, + sessionId, + raw: { + protocolV1Features, + }, + }; +}; + +/** + * Protocol V2 的结构化 `Features` 构建器。 + * + * 这是 Device 内部唯一缓存状态。字段只来自 DeviceInfoGet 或前一次 features + * 缓存的同名字段级合并;不存在协议等价语义的字段保持 null/空值,不再通过 + * DeviceProfile 或 transport path 做身份兜底。 + */ +export const buildProtocolV2FeaturesPayload = ( + deviceInfo?: ProtocolV2DeviceInfo, + previous?: Features +): Features => { + const info = deviceInfo; + const fwApplication = info?.fw?.application; + const fwBootloader = info?.fw?.bootloader; + const fwBoard = info?.fw?.romloader; + const bleApplication = info?.coprocessor?.application; + const status = info?.status; + + const firmwareVersion = firstMeaningfulVersion( + getImageVersion(fwApplication), + previous?.firmwareVersion + ); + const bootloaderVersion = firstMeaningfulVersion( + getImageVersion(fwBootloader), + previous?.bootloaderVersion + ); + const boardVersion = firstMeaningfulVersion(getImageVersion(fwBoard), previous?.boardVersion); + const bleVersion = firstMeaningfulVersion(getImageVersion(bleApplication), previous?.bleVersion); + const deviceId = status?.device_id ?? null; + const serialNo = firstValue(info?.hw?.serial_no, previous?.serialNo) ?? ''; + const label = previous?.label ?? null; + const bleName = firstValue(info?.coprocessor?.bt_adv_name, previous?.bleName); + const initialized = firstValue(status?.init_states, previous?.initialized) ?? null; + const passphraseProtection = status?.passphrase_enabled ?? null; + const language = previous?.language ?? null; + const backupRequired = firstValue(status?.backup_required, previous?.backupRequired) ?? null; + const bleEnabled = previous?.bleEnabled; + const unlocked = firstValue(status?.unlocked, previous?.unlocked) ?? null; + const attachToPinEnabled = status?.attach_to_pin_enabled ?? null; + const unlockedAttachPin = status?.unlocked_by_attach_to_pin ?? undefined; + const romloaderMode = isProtocolV2RomloaderDeviceInfo(info); + const bootloaderMode = isProtocolV2BootloaderDeviceInfo(info); + let mode: DeviceFeaturesMode = 'unknown'; + if (romloaderMode) { + mode = 'romloader'; + } else if (bootloaderMode) { + mode = 'bootloader'; + } else if (initialized === false) { + mode = 'notInitialized'; + } else if (initialized === true) { + mode = 'normal'; + } + + return { + protocol: 'V2', + protocolVersion: deviceInfo?.protocol_version ?? previous?.protocolVersion ?? null, + deviceType: EDeviceType.Pro2, + firmwareType: previous?.firmwareType ?? EFirmwareType.Universal, + model: 'pro2', + vendor: 'onekey.so', + deviceId, + serialNo, + label, + bleName: bleName ?? null, + capabilities: [], + mode, + initialized, + bootloaderMode, + unlocked, + firmwarePresent: previous?.firmwarePresent ?? null, + passphraseProtection, + pinProtection: null, + backupRequired, + noBackup: null, + unfinishedBackup: null, + recoveryMode: null, + language, + bleEnabled: bleEnabled ?? null, + sdCardPresent: null, + sdProtection: null, + wipeCodeProtection: null, + passphraseAlwaysOnDevice: null, + attachToPinEnabled, + safetyChecks: null, + autoLockDelayMs: null, + displayRotation: null, + experimentalFeatures: null, + firmwareVersion, + bootloaderVersion, + boardVersion, + bleVersion, + se01Version: firstMeaningfulVersion( + getImageVersion(info?.se1?.application), + previous?.se01Version + ), + se02Version: firstMeaningfulVersion( + getImageVersion(info?.se2?.application), + previous?.se02Version + ), + se03Version: firstMeaningfulVersion( + getImageVersion(info?.se3?.application), + previous?.se03Version + ), + se04Version: firstMeaningfulVersion( + getImageVersion(info?.se4?.application), + previous?.se04Version + ), + se01BootVersion: firstMeaningfulVersion( + getImageVersion(info?.se1?.bootloader), + previous?.se01BootVersion + ), + se02BootVersion: firstMeaningfulVersion( + getImageVersion(info?.se2?.bootloader), + previous?.se02BootVersion + ), + se03BootVersion: firstMeaningfulVersion( + getImageVersion(info?.se3?.bootloader), + previous?.se03BootVersion + ), + se04BootVersion: firstMeaningfulVersion( + getImageVersion(info?.se4?.bootloader), + previous?.se04BootVersion + ), + seVersion: previous?.seVersion ?? null, + verify: { + firmwareBuildId: getImageBuildId(fwApplication) ?? previous?.verify?.firmwareBuildId, + firmwareHash: getImageHash(fwApplication) ?? previous?.verify?.firmwareHash, + bootloaderBuildId: getImageBuildId(fwBootloader) ?? previous?.verify?.bootloaderBuildId, + bootloaderHash: getImageHash(fwBootloader) ?? previous?.verify?.bootloaderHash, + boardBuildId: getImageBuildId(fwBoard) ?? previous?.verify?.boardBuildId, + boardHash: getImageHash(fwBoard) ?? previous?.verify?.boardHash, + bleBuildId: getImageBuildId(bleApplication) ?? previous?.verify?.bleBuildId, + bleHash: getImageHash(bleApplication) ?? previous?.verify?.bleHash, + se01BuildId: getImageBuildId(info?.se1?.application) ?? previous?.verify?.se01BuildId, + se01Hash: getImageHash(info?.se1?.application) ?? previous?.verify?.se01Hash, + se02BuildId: getImageBuildId(info?.se2?.application) ?? previous?.verify?.se02BuildId, + se02Hash: getImageHash(info?.se2?.application) ?? previous?.verify?.se02Hash, + se03BuildId: getImageBuildId(info?.se3?.application) ?? previous?.verify?.se03BuildId, + se03Hash: getImageHash(info?.se3?.application) ?? previous?.verify?.se03Hash, + se04BuildId: getImageBuildId(info?.se4?.application) ?? previous?.verify?.se04BuildId, + se04Hash: getImageHash(info?.se4?.application) ?? previous?.verify?.se04Hash, + se01BootBuildId: getImageBuildId(info?.se1?.bootloader) ?? previous?.verify?.se01BootBuildId, + se01BootHash: getImageHash(info?.se1?.bootloader) ?? previous?.verify?.se01BootHash, + se02BootBuildId: getImageBuildId(info?.se2?.bootloader) ?? previous?.verify?.se02BootBuildId, + se02BootHash: getImageHash(info?.se2?.bootloader) ?? previous?.verify?.se02BootHash, + se03BootBuildId: getImageBuildId(info?.se3?.bootloader) ?? previous?.verify?.se03BootBuildId, + se03BootHash: getImageHash(info?.se3?.bootloader) ?? previous?.verify?.se03BootHash, + se04BootBuildId: getImageBuildId(info?.se4?.bootloader) ?? previous?.verify?.se04BootBuildId, + se04BootHash: getImageHash(info?.se4?.bootloader) ?? previous?.verify?.se04BootHash, + }, + sessionId: previous?.sessionId ?? null, + passphraseState: previous?.passphraseState, + unlockedAttachPin, + raw: { + protocolV2DeviceInfo: deviceInfo, + }, + }; +}; diff --git a/packages/core/src/deviceProfile/buildDeviceProfile.ts b/packages/core/src/deviceProfile/buildDeviceProfile.ts new file mode 100644 index 000000000..1bf0217e2 --- /dev/null +++ b/packages/core/src/deviceProfile/buildDeviceProfile.ts @@ -0,0 +1,354 @@ +import { EDeviceType, EFirmwareType } from '@onekeyfe/hd-shared'; + +import { + getDeviceBLEFirmwareVersion, + getDeviceBoardloaderVersion, + getDeviceBootloaderVersion, + getDeviceFirmwareVersion, +} from '../utils/deviceVersionUtils'; +import { + getDeviceBleName, + getDeviceLabel, + getDeviceType, + getDeviceUUID, + getFirmwareType, +} from '../utils/deviceInfoUtils'; +import { + isProtocolV2BootloaderDeviceInfo, + isProtocolV2RomloaderDeviceInfo, +} from '../protocols/protocol-v2/features'; + +import type { + DeviceInfoProtocol, + DeviceInfoSource, + DeviceInfoStatus, + DeviceProfile, + DeviceProfileRaw, + DeviceProfileVerify, + DeviceProfileVersions, + GetDeviceInfoParams, +} from '../types/api/getDeviceInfo'; +import type { Features, OnekeyFeatures } from '../types'; +import type { DeviceFirmwareImageInfo, ProtocolV2DeviceInfo } from '@onekeyfe/hd-transport'; + +type BuildProtocolV1ProfileParams = { + protocol?: DeviceInfoProtocol; + features?: Features; + protocolV1OneKeyFeatures?: OnekeyFeatures; + sources?: DeviceInfoSource[]; + scope?: GetDeviceInfoParams['scope']; + includeRaw?: boolean; +}; + +type BuildProtocolV2ProfileParams = { + deviceInfo?: ProtocolV2DeviceInfo; + sources?: DeviceInfoSource[]; + scope?: GetDeviceInfoParams['scope']; + includeRaw?: boolean; +}; + +const isMeaningfulVersion = (version?: string | null) => Boolean(version && version !== '0.0.0'); + +const firstMeaningfulVersion = (...versions: Array) => + versions.find(isMeaningfulVersion) ?? null; + +const versionArrayToString = (version?: Array | null) => { + if (!version || version.length === 0) return null; + const value = version.join('.'); + return isMeaningfulVersion(value) ? value : null; +}; + +const bytesToHex = (value: unknown): string | undefined => { + if (!value) return undefined; + if (typeof value === 'string') return value; + if (value instanceof Uint8Array) { + return Array.from(value) + .map(byte => byte.toString(16).padStart(2, '0')) + .join(''); + } + if (Array.isArray(value)) { + return value.map(byte => Number(byte).toString(16).padStart(2, '0')).join(''); + } + return undefined; +}; + +const getImageVersion = (image?: DeviceFirmwareImageInfo | null) => image?.version ?? null; + +const getImageBuildId = (image?: DeviceFirmwareImageInfo | null) => image?.build_id ?? undefined; + +const getImageHash = (image?: DeviceFirmwareImageInfo | null) => bytesToHex(image?.hash); + +const shouldIncludeVerify = (scope?: GetDeviceInfoParams['scope']) => + scope === 'verify' || scope === 'full'; + +const getDeviceMode = (features?: Features): DeviceInfoStatus['mode'] => { + if (!features) return 'unknown'; + if (features.bootloaderMode === true) return 'bootloader'; + if (features.initialized === false) return 'notInitialized'; + if (features.initialized === true) return 'normal'; + return 'unknown'; +}; + +const getProtocolV2Mode = (deviceInfo?: ProtocolV2DeviceInfo): DeviceInfoStatus['mode'] => { + if (isProtocolV2RomloaderDeviceInfo(deviceInfo)) return 'romloader'; + if (isProtocolV2BootloaderDeviceInfo(deviceInfo)) return 'bootloader'; + const initialized = deviceInfo?.status?.init_states; + if (initialized === false) return 'notInitialized'; + if (initialized === true) return 'normal'; + return 'unknown'; +}; + +const normalizeV1Versions = ( + features?: Features, + protocolV1OneKeyFeatures?: OnekeyFeatures +): DeviceProfileVersions => ({ + firmware: firstMeaningfulVersion( + protocolV1OneKeyFeatures?.onekey_firmware_version, + versionArrayToString(getDeviceFirmwareVersion(features)) + ), + bootloader: firstMeaningfulVersion( + protocolV1OneKeyFeatures?.onekey_boot_version, + versionArrayToString(getDeviceBootloaderVersion(features)) + ), + board: firstMeaningfulVersion( + protocolV1OneKeyFeatures?.onekey_board_version, + versionArrayToString(features ? getDeviceBoardloaderVersion(features) : undefined) + ), + ble: firstMeaningfulVersion( + protocolV1OneKeyFeatures?.onekey_ble_version, + features?.bleVersion, + versionArrayToString(features ? getDeviceBLEFirmwareVersion(features) : undefined) + ), + se01: firstMeaningfulVersion( + protocolV1OneKeyFeatures?.onekey_se01_version, + features?.se01Version + ), + se02: firstMeaningfulVersion( + protocolV1OneKeyFeatures?.onekey_se02_version, + features?.se02Version + ), + se03: firstMeaningfulVersion( + protocolV1OneKeyFeatures?.onekey_se03_version, + features?.se03Version + ), + se04: firstMeaningfulVersion( + protocolV1OneKeyFeatures?.onekey_se04_version, + features?.se04Version + ), + se01Boot: firstMeaningfulVersion( + protocolV1OneKeyFeatures?.onekey_se01_boot_version, + features?.se01BootVersion + ), + se02Boot: firstMeaningfulVersion( + protocolV1OneKeyFeatures?.onekey_se02_boot_version, + features?.se02BootVersion + ), + se03Boot: firstMeaningfulVersion( + protocolV1OneKeyFeatures?.onekey_se03_boot_version, + features?.se03BootVersion + ), + se04Boot: firstMeaningfulVersion( + protocolV1OneKeyFeatures?.onekey_se04_boot_version, + features?.se04BootVersion + ), +}); + +const normalizeV2Versions = (deviceInfo?: ProtocolV2DeviceInfo): DeviceProfileVersions => { + const info = deviceInfo; + return { + firmware: firstMeaningfulVersion(getImageVersion(info?.fw?.application)), + bootloader: firstMeaningfulVersion(getImageVersion(info?.fw?.bootloader)), + board: firstMeaningfulVersion(getImageVersion(info?.fw?.romloader)), + ble: firstMeaningfulVersion(getImageVersion(info?.coprocessor?.application)), + se01: firstMeaningfulVersion(getImageVersion(info?.se1?.application)), + se02: firstMeaningfulVersion(getImageVersion(info?.se2?.application)), + se03: firstMeaningfulVersion(getImageVersion(info?.se3?.application)), + se04: firstMeaningfulVersion(getImageVersion(info?.se4?.application)), + se01Boot: firstMeaningfulVersion(getImageVersion(info?.se1?.bootloader)), + se02Boot: firstMeaningfulVersion(getImageVersion(info?.se2?.bootloader)), + se03Boot: firstMeaningfulVersion(getImageVersion(info?.se3?.bootloader)), + se04Boot: firstMeaningfulVersion(getImageVersion(info?.se4?.bootloader)), + }; +}; + +// V2 状态由 normalizeV2Status 处理,这里只服务 buildProfileFromProtocolV1 的 V1 路径。 +const normalizeV1Status = (features?: Features): DeviceInfoStatus => ({ + mode: getDeviceMode(features), + initialized: features?.initialized ?? null, + bootloaderMode: features?.bootloaderMode ?? null, + unlocked: features?.unlocked ?? null, + passphraseProtection: features?.passphraseProtection ?? null, + attachToPinEnabled: features?.attachToPinEnabled ?? null, + unlockedAttachPin: features?.unlockedAttachPin ?? null, + backupRequired: features?.backupRequired ?? null, + noBackup: features?.noBackup ?? null, + language: features?.language ?? null, + bleEnabled: features?.bleEnabled ?? null, +}); + +const normalizeV2Status = (deviceInfo?: ProtocolV2DeviceInfo): DeviceInfoStatus => { + const status = deviceInfo?.status; + const bootloaderMode = isProtocolV2BootloaderDeviceInfo(deviceInfo); + return { + mode: getProtocolV2Mode(deviceInfo), + initialized: status?.init_states ?? null, + bootloaderMode, + unlocked: status?.unlocked ?? null, + passphraseProtection: status?.passphrase_enabled ?? null, + attachToPinEnabled: status?.attach_to_pin_enabled ?? null, + unlockedAttachPin: status?.unlocked_by_attach_to_pin ?? null, + backupRequired: status?.backup_required ?? null, + noBackup: null, + language: null, + bleEnabled: null, + }; +}; + +const normalizeV1Verify = ( + features?: Features, + protocolV1OneKeyFeatures?: OnekeyFeatures +): DeviceProfileVerify => ({ + firmwareBuildId: + protocolV1OneKeyFeatures?.onekey_firmware_build_id ?? features?.verify?.firmwareBuildId, + firmwareHash: protocolV1OneKeyFeatures?.onekey_firmware_hash ?? features?.verify?.firmwareHash, + bootloaderBuildId: + protocolV1OneKeyFeatures?.onekey_boot_build_id ?? features?.verify?.bootloaderBuildId, + bootloaderHash: protocolV1OneKeyFeatures?.onekey_boot_hash ?? features?.verify?.bootloaderHash, + boardBuildId: protocolV1OneKeyFeatures?.onekey_board_build_id ?? features?.verify?.boardBuildId, + boardHash: protocolV1OneKeyFeatures?.onekey_board_hash ?? features?.verify?.boardHash, + bleBuildId: protocolV1OneKeyFeatures?.onekey_ble_build_id ?? features?.verify?.bleBuildId, + bleHash: protocolV1OneKeyFeatures?.onekey_ble_hash ?? features?.verify?.bleHash, + se01BuildId: protocolV1OneKeyFeatures?.onekey_se01_build_id ?? features?.verify?.se01BuildId, + se01Hash: protocolV1OneKeyFeatures?.onekey_se01_hash ?? features?.verify?.se01Hash, + se02BuildId: protocolV1OneKeyFeatures?.onekey_se02_build_id ?? features?.verify?.se02BuildId, + se02Hash: protocolV1OneKeyFeatures?.onekey_se02_hash ?? features?.verify?.se02Hash, + se03BuildId: protocolV1OneKeyFeatures?.onekey_se03_build_id ?? features?.verify?.se03BuildId, + se03Hash: protocolV1OneKeyFeatures?.onekey_se03_hash ?? features?.verify?.se03Hash, + se04BuildId: protocolV1OneKeyFeatures?.onekey_se04_build_id ?? features?.verify?.se04BuildId, + se04Hash: protocolV1OneKeyFeatures?.onekey_se04_hash ?? features?.verify?.se04Hash, + se01BootBuildId: + protocolV1OneKeyFeatures?.onekey_se01_boot_build_id ?? features?.verify?.se01BootBuildId, + se01BootHash: protocolV1OneKeyFeatures?.onekey_se01_boot_hash ?? features?.verify?.se01BootHash, + se02BootBuildId: + protocolV1OneKeyFeatures?.onekey_se02_boot_build_id ?? features?.verify?.se02BootBuildId, + se02BootHash: protocolV1OneKeyFeatures?.onekey_se02_boot_hash ?? features?.verify?.se02BootHash, + se03BootBuildId: + protocolV1OneKeyFeatures?.onekey_se03_boot_build_id ?? features?.verify?.se03BootBuildId, + se03BootHash: protocolV1OneKeyFeatures?.onekey_se03_boot_hash ?? features?.verify?.se03BootHash, + se04BootBuildId: + protocolV1OneKeyFeatures?.onekey_se04_boot_build_id ?? features?.verify?.se04BootBuildId, + se04BootHash: protocolV1OneKeyFeatures?.onekey_se04_boot_hash ?? features?.verify?.se04BootHash, +}); + +const normalizeV2Verify = (deviceInfo?: ProtocolV2DeviceInfo): DeviceProfileVerify => { + const info = deviceInfo; + return { + firmwareBuildId: getImageBuildId(info?.fw?.application), + firmwareHash: getImageHash(info?.fw?.application), + bootloaderBuildId: getImageBuildId(info?.fw?.bootloader), + bootloaderHash: getImageHash(info?.fw?.bootloader), + boardBuildId: getImageBuildId(info?.fw?.romloader), + boardHash: getImageHash(info?.fw?.romloader), + bleBuildId: getImageBuildId(info?.coprocessor?.application), + bleHash: getImageHash(info?.coprocessor?.application), + se01BuildId: getImageBuildId(info?.se1?.application), + se01Hash: getImageHash(info?.se1?.application), + se02BuildId: getImageBuildId(info?.se2?.application), + se02Hash: getImageHash(info?.se2?.application), + se03BuildId: getImageBuildId(info?.se3?.application), + se03Hash: getImageHash(info?.se3?.application), + se04BuildId: getImageBuildId(info?.se4?.application), + se04Hash: getImageHash(info?.se4?.application), + se01BootBuildId: getImageBuildId(info?.se1?.bootloader), + se01BootHash: getImageHash(info?.se1?.bootloader), + se02BootBuildId: getImageBuildId(info?.se2?.bootloader), + se02BootHash: getImageHash(info?.se2?.bootloader), + se03BootBuildId: getImageBuildId(info?.se3?.bootloader), + se03BootHash: getImageHash(info?.se3?.bootloader), + se04BootBuildId: getImageBuildId(info?.se4?.bootloader), + se04BootHash: getImageHash(info?.se4?.bootloader), + }; +}; + +const normalizeRaw = ({ + features, + protocolV1OneKeyFeatures, + protocolV2DeviceInfo, +}: { + features?: Features; + protocolV1OneKeyFeatures?: OnekeyFeatures; + protocolV2DeviceInfo?: ProtocolV2DeviceInfo; +}): DeviceProfileRaw => ({ + ...(features ? { features } : {}), + ...(protocolV1OneKeyFeatures ? { protocolV1OneKeyFeatures } : {}), + ...(protocolV2DeviceInfo ? { protocolV2DeviceInfo } : {}), +}); + +export function buildProfileFromProtocolV1({ + protocol = 'V1', + features, + protocolV1OneKeyFeatures, + sources = ['features'], + scope = 'basic', + includeRaw = false, +}: BuildProtocolV1ProfileParams): DeviceProfile { + const sourceFeatures = features; + const verify = normalizeV1Verify(sourceFeatures, protocolV1OneKeyFeatures); + + return { + protocol, + sources, + deviceType: getDeviceType(sourceFeatures), + firmwareType: getFirmwareType(sourceFeatures), + deviceId: sourceFeatures?.deviceId || (sourceFeatures ? getDeviceUUID(sourceFeatures) : ''), + serialNo: sourceFeatures ? getDeviceUUID(sourceFeatures) : '', + label: getDeviceLabel(sourceFeatures), + bleName: getDeviceBleName(sourceFeatures), + status: normalizeV1Status(sourceFeatures), + versions: normalizeV1Versions(sourceFeatures, protocolV1OneKeyFeatures), + ...(shouldIncludeVerify(scope) ? { verify } : {}), + ...(includeRaw + ? { + raw: normalizeRaw({ + features, + protocolV1OneKeyFeatures, + }), + } + : {}), + }; +} + +export function buildProfileFromProtocolV2({ + deviceInfo, + sources = ['deviceInfo'], + scope = 'basic', + includeRaw = false, +}: BuildProtocolV2ProfileParams): DeviceProfile { + const info = deviceInfo; + const deviceId = info?.status?.device_id || ''; + const serialNo = deviceInfo?.hw?.serial_no || ''; + const label = null; + const bleName = info?.coprocessor?.bt_adv_name ?? null; + const verify = normalizeV2Verify(deviceInfo); + + return { + protocol: 'V2', + sources, + deviceType: EDeviceType.Pro2, + firmwareType: EFirmwareType.Universal, + deviceId, + serialNo, + label, + bleName, + status: normalizeV2Status(deviceInfo), + versions: normalizeV2Versions(deviceInfo), + ...(shouldIncludeVerify(scope) ? { verify } : {}), + ...(includeRaw + ? { + raw: normalizeRaw({ + protocolV2DeviceInfo: deviceInfo, + }), + } + : {}), + }; +} diff --git a/packages/core/src/deviceProfile/index.ts b/packages/core/src/deviceProfile/index.ts new file mode 100644 index 000000000..9ebd5285d --- /dev/null +++ b/packages/core/src/deviceProfile/index.ts @@ -0,0 +1,5 @@ +export { buildProfileFromProtocolV1, buildProfileFromProtocolV2 } from './buildDeviceProfile'; +export { + buildProtocolV1FeaturesPayload, + buildProtocolV2FeaturesPayload, +} from './buildDeviceFeatures'; diff --git a/packages/core/src/events/call.ts b/packages/core/src/events/call.ts index a38a2665d..a1a058984 100644 --- a/packages/core/src/events/call.ts +++ b/packages/core/src/events/call.ts @@ -53,6 +53,12 @@ export interface IFrameCancelMessage { payload: { connectId?: string }; } +export interface IFrameCancelOperationMessage { + event: typeof IFRAME.CANCEL_OPERATION; + type: typeof IFRAME.CANCEL_OPERATION; + payload: { operationId: string }; +} + export interface IFrameSwitchTransportMessage { event: typeof IFRAME.SWITCH_TRANSPORT; type: typeof IFRAME.SWITCH_TRANSPORT; diff --git a/packages/core/src/events/core.ts b/packages/core/src/events/core.ts index 250deae57..b6ba949f0 100644 --- a/packages/core/src/events/core.ts +++ b/packages/core/src/events/core.ts @@ -5,6 +5,7 @@ import type { IFrameCallMessage, IFrameCallbackMessage, IFrameCancelMessage, + IFrameCancelOperationMessage, IFrameSwitchTransportMessage, } from './call'; import type { DeviceEventMessage } from './device'; @@ -23,6 +24,7 @@ export type CoreMessage = { | IFrameEventMessage | IFrameCallMessage | IFrameCancelMessage + | IFrameCancelOperationMessage | IFrameSwitchTransportMessage | IFrameCallbackMessage | UiResponseMessage diff --git a/packages/core/src/events/iframe.ts b/packages/core/src/events/iframe.ts index d84372b38..cf09f84d1 100644 --- a/packages/core/src/events/iframe.ts +++ b/packages/core/src/events/iframe.ts @@ -8,6 +8,7 @@ export const IFRAME = { INIT_BRIDGE: 'iframe-init-bridge', CALL: 'iframe-call', CANCEL: 'iframe-cancel', + CANCEL_OPERATION: 'iframe-cancel-operation', SWITCH_TRANSPORT: 'iframe-switch-transport', CALLBACK: 'iframe-callback', } as const; diff --git a/packages/core/src/events/logBlockEvent.ts b/packages/core/src/events/logBlockEvent.ts index ccd98e158..2b0fe6608 100644 --- a/packages/core/src/events/logBlockEvent.ts +++ b/packages/core/src/events/logBlockEvent.ts @@ -4,3 +4,26 @@ export const LogBlockEvent: Set = new Set([ UI_RESPONSE.RECEIVE_PIN, UI_RESPONSE.RECEIVE_PASSPHRASE, ]); + +const LogBlockMethod: Set = new Set(['evmSignTypedData']); + +export function getLogBlockLabel(message: unknown): string | undefined { + if (!message || typeof message !== 'object') return undefined; + + const { type, payload, method } = message as { + type?: string; + payload?: { method?: string }; + method?: string; + }; + + if (type && LogBlockEvent.has(type)) { + return type; + } + + const methodName = method ?? payload?.method; + if (methodName && LogBlockMethod.has(methodName)) { + return methodName; + } + + return undefined; +} diff --git a/packages/core/src/events/ui-request.ts b/packages/core/src/events/ui-request.ts index 7e0699c8c..d44f966ca 100644 --- a/packages/core/src/events/ui-request.ts +++ b/packages/core/src/events/ui-request.ts @@ -129,6 +129,10 @@ export interface FirmwareProgress { device: Device; progress: number; progressType: IFirmwareUpdateProgressType; + transferredBytes?: number; + totalBytes?: number; + rateBytesPerSecond?: number; + elapsedMs?: number; }; } @@ -144,6 +148,10 @@ export interface DeviceProgress { type: typeof UI_REQUEST.DEVICE_PROGRESS; payload: { progress?: number; + transferredBytes?: number; + totalBytes?: number; + rateBytesPerSecond?: number; + elapsedMs?: number; }; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3299d52d6..1b3836891 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -27,6 +27,7 @@ const HardwareSdk = ({ eventEmitter, uiResponse, cancel, + cancelOperation, updateSettings, switchTransport, }: InjectApi): CoreApi => @@ -37,6 +38,7 @@ const HardwareSdk = ({ eventEmitter, uiResponse, cancel, + cancelOperation, updateSettings, switchTransport, }); @@ -49,6 +51,7 @@ const HardwareSDKLowLevel = ({ addHardwareGlobalEventListener, uiResponse, cancel, + cancelOperation, updateSettings, switchTransport, }: LowLevelInjectApi): LowLevelCoreApi => @@ -60,6 +63,7 @@ const HardwareSDKLowLevel = ({ addHardwareGlobalEventListener, uiResponse, cancel, + cancelOperation, updateSettings, switchTransport, }); diff --git a/packages/core/src/inject.ts b/packages/core/src/inject.ts index 0a1f6b7c8..7d6ad6347 100644 --- a/packages/core/src/inject.ts +++ b/packages/core/src/inject.ts @@ -36,12 +36,14 @@ export interface InjectApi { dispose: CoreApi['dispose']; uiResponse: CoreApi['uiResponse']; cancel: CoreApi['cancel']; + cancelOperation: CoreApi['cancelOperation']; switchTransport: CoreApi['switchTransport']; } export const inject = ({ call, cancel, + cancelOperation, dispose, eventEmitter, init, @@ -73,6 +75,7 @@ export const inject = ({ uiResponse, cancel, + cancelOperation, updateSettings, @@ -96,19 +99,22 @@ export const createCoreApi = ( | 'dispose' | 'uiResponse' | 'cancel' + | 'cancelOperation' | 'updateSettings' | 'switchTransport' > => ({ getLogs: () => call({ method: 'getLogs' }), + clearSessionCache: params => call({ ...params, method: 'clearSessionCache' }), /** * 搜索设备 */ - searchDevices: () => call({ method: 'searchDevices' }), + searchDevices: params => call({ ...params, method: 'searchDevices' }), /** * 获取设备信息 */ getFeatures: (connectId, params) => call({ ...params, connectId, method: 'getFeatures' }), + getDeviceInfo: (connectId, params) => call({ ...params, connectId, method: 'getDeviceInfo' }), getOnekeyFeatures: (connectId, params) => call({ ...params, connectId, method: 'getOnekeyFeatures' }), @@ -148,6 +154,59 @@ export const createCoreApi = ( deviceFlags: (connectId, params) => call({ ...params, connectId, method: 'deviceFlags' }), deviceRebootToBoardloader: connectId => call({ connectId, method: 'deviceRebootToBoardloader' }), deviceRebootToBootloader: connectId => call({ connectId, method: 'deviceRebootToBootloader' }), + + // File system & device control API (Protocol V2 only) + protocolInfoRequest: (connectId, params) => + call({ ...params, connectId, method: 'protocolInfoRequest' }), + ping: (connectId, params) => call({ ...params, connectId, method: 'ping' }), + deviceReboot: (connectId, params) => call({ ...params, connectId, method: 'deviceReboot' }), + deviceInfoGet: (connectId, params) => call({ ...params, connectId, method: 'deviceInfoGet' }), + deviceStatusGet: (connectId, params) => call({ ...params, connectId, method: 'deviceStatusGet' }), + deviceGetOnboardingStatus: (connectId, params) => + call({ ...params, connectId, method: 'deviceGetOnboardingStatus' }), + deviceSessionGet: (connectId, params) => + call({ ...params, connectId, method: 'deviceSessionGet' }), + deviceFirmwareUpdate: (connectId, params) => + call({ ...params, connectId, method: 'deviceFirmwareUpdate' }), + deviceGetFirmwareUpdateStatus: (connectId, params) => + call({ ...params, connectId, method: 'deviceGetFirmwareUpdateStatus' }), + deviceFactoryInfoSet: (connectId, params) => + call({ ...params, connectId, method: 'deviceFactoryInfoSet' }), + deviceFactoryInfoGet: (connectId, params) => + call({ ...params, connectId, method: 'deviceFactoryInfoGet' }), + deviceSettingsGet: (connectId, params) => + call({ ...params, connectId, method: 'deviceSettingsGet' }), + deviceSettingsSet: (connectId, params) => + call({ ...params, connectId, method: 'deviceSettingsSet' }), + deviceSettingsPageShow: (connectId, params) => + call({ ...params, connectId, method: 'deviceSettingsPageShow' }), + deviceUploadWallpaper: (connectId, params) => + call({ ...params, connectId, method: 'deviceUploadWallpaper' }), + filesystemPermissionFix: (connectId, params) => + call({ ...params, connectId, method: 'filesystemPermissionFix' }), + fileRead: (connectId, params) => call({ ...params, connectId, method: 'fileRead' }), + fileWrite: (connectId, params) => call({ ...params, connectId, method: 'fileWrite' }), + fileDelete: (connectId, params) => call({ ...params, connectId, method: 'fileDelete' }), + dirList: (connectId, params) => call({ ...params, connectId, method: 'dirList' }), + dirMake: (connectId, params) => call({ ...params, connectId, method: 'dirMake' }), + dirRemove: (connectId, params) => call({ ...params, connectId, method: 'dirRemove' }), + pathInfo: (connectId, params) => call({ ...params, connectId, method: 'pathInfo' }), + filesystemFileRead: (connectId, params) => + call({ ...params, connectId, method: 'filesystemFileRead' }), + filesystemFileWrite: (connectId, params) => + call({ ...params, connectId, method: 'filesystemFileWrite' }), + uploadPortfolio: (connectId, params) => call({ ...params, connectId, method: 'uploadPortfolio' }), + filesystemFileDelete: (connectId, params) => + call({ ...params, connectId, method: 'filesystemFileDelete' }), + filesystemDirList: (connectId, params) => + call({ ...params, connectId, method: 'filesystemDirList' }), + filesystemDirMake: (connectId, params) => + call({ ...params, connectId, method: 'filesystemDirMake' }), + filesystemDirRemove: (connectId, params) => + call({ ...params, connectId, method: 'filesystemDirRemove' }), + filesystemPathInfoQuery: (connectId, params) => + call({ ...params, connectId, method: 'filesystemPathInfoQuery' }), + filesystemFormat: connectId => call({ connectId, method: 'filesystemFormat' }), deviceRecovery: (connectId, params) => call({ ...params, connectId, method: 'deviceRecovery' }), deviceReset: (connectId, params) => call({ ...params, connectId, method: 'deviceReset' }), deviceSettings: (connectId, params) => call({ ...params, connectId, method: 'deviceSettings' }), @@ -260,6 +319,8 @@ export const createCoreApi = ( call({ ...params, connectId, method: 'firmwareUpdateV2' }), firmwareUpdateV3: (connectId, params) => call({ ...params, connectId, method: 'firmwareUpdateV3' }), + firmwareUpdateV4: (connectId, params) => + call({ ...params, connectId, method: 'firmwareUpdateV4' }), promptWebDeviceAccess: params => call({ ...params, method: 'promptWebDeviceAccess' }), tronGetAddress: (connectId, deviceId, params) => diff --git a/packages/core/src/lowLevelInject.ts b/packages/core/src/lowLevelInject.ts index b913f9a3f..ed86e469e 100644 --- a/packages/core/src/lowLevelInject.ts +++ b/packages/core/src/lowLevelInject.ts @@ -13,6 +13,7 @@ export interface LowLevelInjectApi { dispose: CoreApi['dispose']; uiResponse: CoreApi['uiResponse']; cancel: CoreApi['cancel']; + cancelOperation: CoreApi['cancelOperation']; updateSettings: CoreApi['updateSettings']; switchTransport: CoreApi['switchTransport']; addHardwareGlobalEventListener: (listener: IAddHardwareGlobalEventListener) => void; @@ -25,6 +26,7 @@ export type LowLevelCoreApi = Omit & { export const lowLevelInject = ({ call, cancel, + cancelOperation, dispose, eventEmitter, init, @@ -48,6 +50,7 @@ export const lowLevelInject = ({ uiResponse, cancel, + cancelOperation, updateSettings, diff --git a/packages/core/src/protocols/protocol-v2/features.ts b/packages/core/src/protocols/protocol-v2/features.ts new file mode 100644 index 000000000..07977b9c1 --- /dev/null +++ b/packages/core/src/protocols/protocol-v2/features.ts @@ -0,0 +1,159 @@ +import { DeviceSEState, DeviceSeType } from '@onekeyfe/hd-transport'; + +import type { DeviceInfoGet, DeviceSEInfo, ProtocolV2DeviceInfo } from '@onekeyfe/hd-transport'; +import type { DeviceCommands } from '../../device/DeviceCommands'; + +// 单源类型:直接使用 hd-transport 生成的 ProtocolV2DeviceInfo / DeviceSEInfo / +// DeviceFirmwareImageInfo(与 firmware-pro2 proto 一致),不再维护手写副本。 +export type { ProtocolV2DeviceInfo }; +export type { DeviceFirmwareImageInfo as ProtocolV2FirmwareImageInfo } from '@onekeyfe/hd-transport'; +export type { DeviceSEInfo as ProtocolV2SEInfo } from '@onekeyfe/hd-transport'; + +export type ProtocolV2SeStateLabel = 'BOOT' | 'APP_FACTORY' | 'APP'; + +/** + * 传输层沿用历史 decode 语义:单值 proto enum 输出为名称字符串。 + * 这里按 SDK decode 后的字符串语义处理,同时接受数字枚举,便于低层调用复用。 + */ +const normalizeEnumValue = >( + enumObject: T, + value: number | string | null | undefined +): string | null => { + if (value == null) return null; + if (typeof value === 'string') return value; + const label = enumObject[value]; + return typeof label === 'string' ? label : null; +}; + +/** + * DeviceSEInfo.state → 可读标签。SDK 内唯一的 SE 状态映射实现, + * DeviceProfile 与标准 Features 构建都从这里取。 + */ +export const getProtocolV2SeState = (se?: DeviceSEInfo): ProtocolV2SeStateLabel | null => { + const label = normalizeEnumValue(DeviceSEState, se?.state); + switch (label) { + case 'BOOT': + return 'BOOT'; + case 'APP_FACTORY': + return 'APP_FACTORY'; + case 'APP': + return 'APP'; + default: + return null; + } +}; + +/** + * DeviceSEInfo.type → 可读标签(如 'THD89')。DeviceProfile / Features + * 的 SE 类型归一化从这里取。 + */ +export const getProtocolV2SeType = (se?: DeviceSEInfo): string | null => + normalizeEnumValue(DeviceSeType, se?.type); + +/** + * 兼容尚未提供显式 runtime mode 的 Protocol V2 固件。 + * + * 当前 romloader 只上报 hw、fw.romloader 和 fw.bootloader;bootloader + * 还会上报 application/application_data、coprocessor 或 SE 信息。 + */ +export const isProtocolV2RomloaderDeviceInfo = (deviceInfo?: ProtocolV2DeviceInfo | null) => + !!deviceInfo && + deviceInfo.status == null && + deviceInfo.fw?.romloader != null && + deviceInfo.fw?.application == null && + deviceInfo.fw?.application_data == null && + deviceInfo.coprocessor == null && + deviceInfo.se1 == null && + deviceInfo.se2 == null && + deviceInfo.se3 == null && + deviceInfo.se4 == null; + +export const isProtocolV2BootloaderDeviceInfo = (deviceInfo?: ProtocolV2DeviceInfo | null) => + !!deviceInfo && deviceInfo.status == null && !isProtocolV2RomloaderDeviceInfo(deviceInfo); + +export const PROTOCOL_V2_FEATURES_DEVICE_INFO_REQUEST = { + targets: { + hw: true, + fw: true, + coprocessor: true, + status: true, + }, + types: { + version: true, + specific: true, + }, +}; + +/** + * 轻量状态刷新请求(每次 run 前使用)。 + * + * status 提供 init_states / passphrase_enabled 等会在设备端变化的字段; + * hw / coprocessor 提供 serialNo / bleName 等身份字段;不含 fw/SE targets,单帧请求开销很小。 + */ +export const PROTOCOL_V2_STATUS_DEVICE_INFO_REQUEST = { + targets: { + hw: true, + coprocessor: true, + status: true, + }, + types: { + version: true, + specific: true, + }, +}; + +export const PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST = { + targets: { + hw: true, + fw: true, + coprocessor: true, + se1: true, + se2: true, + se3: true, + se4: true, + status: true, + }, + types: { + version: true, + specific: true, + }, +}; + +export const PROTOCOL_V2_FULL_DEVICE_INFO_REQUEST = { + targets: { + hw: true, + fw: true, + coprocessor: true, + se1: true, + se2: true, + se3: true, + se4: true, + status: true, + }, + types: { + version: true, + build_id: true, + hash: true, + specific: true, + }, +}; + +export const PROTOCOL_V2_DEVICE_INFO_REQUEST = PROTOCOL_V2_FULL_DEVICE_INFO_REQUEST; +export const PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS = 10 * 1000; + +export async function requestProtocolV2DeviceInfo({ + commands, + timeoutMs = PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS, + request = PROTOCOL_V2_FEATURES_DEVICE_INFO_REQUEST, +}: { + commands: DeviceCommands; + timeoutMs?: number; + request?: DeviceInfoGet; +}): Promise { + const { message } = await commands.typedCall('DeviceInfoGet', 'DeviceInfo', request, { + timeoutMs, + }); + // 'DeviceInfo' 在生成类型里是 V1 DeviceInfo | ProtocolV2DeviceInfo 的合并; + // DeviceInfoGet 是 V2-only 消息,这里收窄到 V2 形态。 + return message as ProtocolV2DeviceInfo; +} diff --git a/packages/core/src/protocols/protocol-v2/firmware.ts b/packages/core/src/protocols/protocol-v2/firmware.ts new file mode 100644 index 000000000..f8d43244f --- /dev/null +++ b/packages/core/src/protocols/protocol-v2/firmware.ts @@ -0,0 +1,19 @@ +/** + * 当前 firmware-pro2 子模块的 DeviceFirmwareTargetType。 + */ +export const ProtocolV2FirmwareTargetType = { + FW_MGMT_TARGET_INVALID: 0, + FW_MGMT_TARGET_CRATE: 1, + FW_MGMT_TARGET_ROMLOADER: 2, + FW_MGMT_TARGET_BOOTLOADER: 3, + FW_MGMT_TARGET_APPLICATION_P1: 4, + FW_MGMT_TARGET_APPLICATION_P2: 5, + FW_MGMT_TARGET_COPROCESSOR: 6, + FW_MGMT_TARGET_SE01: 7, + FW_MGMT_TARGET_SE02: 8, + FW_MGMT_TARGET_SE03: 9, + FW_MGMT_TARGET_SE04: 10, +} as const; + +export type ProtocolV2FirmwareTargetType = + (typeof ProtocolV2FirmwareTargetType)[keyof typeof ProtocolV2FirmwareTargetType]; diff --git a/packages/core/src/protocols/protocol-v2/index.ts b/packages/core/src/protocols/protocol-v2/index.ts new file mode 100644 index 000000000..53ebc7ffe --- /dev/null +++ b/packages/core/src/protocols/protocol-v2/index.ts @@ -0,0 +1,19 @@ +export { + PROTOCOL_V2_DEVICE_INFO_REQUEST, + PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS, + PROTOCOL_V2_FEATURES_DEVICE_INFO_REQUEST, + PROTOCOL_V2_STATUS_DEVICE_INFO_REQUEST, + PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST, + isProtocolV2BootloaderDeviceInfo, + isProtocolV2RomloaderDeviceInfo, + getProtocolV2SeState, + getProtocolV2SeType, +} from './features'; +export type { + ProtocolV2DeviceInfo, + ProtocolV2FirmwareImageInfo, + ProtocolV2SEInfo, + ProtocolV2SeStateLabel, +} from './features'; +export * from './firmware'; +export * from './walletSession'; diff --git a/packages/core/src/protocols/protocol-v2/unlockRetry.ts b/packages/core/src/protocols/protocol-v2/unlockRetry.ts new file mode 100644 index 000000000..891274779 --- /dev/null +++ b/packages/core/src/protocols/protocol-v2/unlockRetry.ts @@ -0,0 +1,46 @@ +import { HardwareErrorCode } from '@onekeyfe/hd-shared'; + +import { LoggerNames, getLogger } from '../../utils'; + +import type { BaseMethod } from '../../api/BaseMethod'; +import type { Device } from '../../device/Device'; + +const Log = getLogger(LoggerNames.Core); + +type RunnableMethod = Pick & { name?: string }; +type UnlockableDevice = Pick; + +function isDeviceLockedError(error: unknown): boolean { + return ( + typeof error === 'object' && + error !== null && + 'errorCode' in error && + error.errorCode === HardwareErrorCode.DeviceLocked + ); +} + +export async function runMethodWithUnlockRetry(method: RunnableMethod, device: UnlockableDevice) { + try { + return await method.run(); + } catch (error) { + if ( + !device.isProtocolV2() || + method.unlockPolicy !== 'retry-on-locked' || + !isDeviceLockedError(error) + ) { + throw error; + } + + Log.debug('Protocol V2 unlock retry triggered', { method: method.name }); + await device.unlockDevice(); + Log.debug('Protocol V2 unlock completed', { method: method.name }); + try { + const response = await method.run(); + Log.debug('Protocol V2 method retry completed', { method: method.name, success: true }); + return response; + } catch (retryError) { + Log.debug('Protocol V2 method retry completed', { method: method.name, success: false }); + throw retryError; + } + } +} diff --git a/packages/core/src/protocols/protocol-v2/walletSession.ts b/packages/core/src/protocols/protocol-v2/walletSession.ts new file mode 100644 index 000000000..8aa85ff93 --- /dev/null +++ b/packages/core/src/protocols/protocol-v2/walletSession.ts @@ -0,0 +1,92 @@ +import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared'; + +import type { Device } from '../../device/Device'; + +const getErrorText = (error: unknown) => { + if (error instanceof Error) return `${error.name} ${error.message}`; + if (typeof error === 'string') return error; + if (error && typeof error === 'object') { + const record = error as Record; + return [record.code, record.errorCode, record.message, record.reason] + .filter(value => value !== undefined && value !== null) + .join(' '); + } + return String(error ?? ''); +}; + +export const isProtocolV2InvalidSessionError = (error: unknown) => + getErrorText(error).toLowerCase().includes('failure_invalidsession'); + +export async function requestProtocolV2DeviceStatus(device: Device) { + const { message } = await device.commands.typedCall('DeviceStatusGet', 'DeviceStatus', {}); + return message; +} + +export async function refreshProtocolV2DeviceStatus(device: Device) { + const status = await requestProtocolV2DeviceStatus(device); + return device.updateProtocolV2Status(status); +} + +export async function getProtocolV2WalletSession( + device: Device, + options?: { initSession?: boolean; expectedPassphraseState?: string } +) { + if (device.features?.unlocked === false) { + throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Device is locked'); + } + + if (options?.initSession) { + device.clearInternalState(); + } + + const cachedSessionId = + typeof device.getInternalState === 'function' ? device.getInternalState() : undefined; + + try { + const requestDeviceSession = (sessionId?: string) => + device.commands.typedCall( + 'DeviceSessionGet', + 'DeviceSession', + sessionId ? { session_id: sessionId } : {} + ); + + const { message } = await requestDeviceSession(cachedSessionId).catch(async error => { + if (!cachedSessionId || !isProtocolV2InvalidSessionError(error)) { + throw error; + } + device.clearInternalState(); + return requestDeviceSession(); + }); + + if ( + options?.expectedPassphraseState && + options.expectedPassphraseState !== message.btc_test_address + ) { + device.clearInternalState(); + throw ERRORS.TypedError(HardwareErrorCode.DeviceCheckPassphraseStateError); + } + + if (message.btc_test_address && device.getCurrentPassphraseProtection() !== true) { + await refreshProtocolV2DeviceStatus(device); + } + + device.updateInternalState( + (device.getCurrentPassphraseProtection() ?? false) || Boolean(message.btc_test_address), + message.btc_test_address, + device.getCurrentDeviceId(), + message.session_id, + options?.initSession ? null : device.features?.sessionId + ); + + return { + passphraseState: message.btc_test_address, + newSession: message.session_id, + unlockedAttachPin: device.features?.unlockedAttachPin ?? undefined, + }; + } catch (error) { + if (isProtocolV2InvalidSessionError(error)) { + device.clearInternalState(); + } + throw error; + } +} diff --git a/packages/core/src/topLevelInject.ts b/packages/core/src/topLevelInject.ts index 7f7c12185..4267c2c26 100644 --- a/packages/core/src/topLevelInject.ts +++ b/packages/core/src/topLevelInject.ts @@ -52,6 +52,8 @@ export const topLevelInject = () => { cancel: (connectId?: string) => lowLevelApi?.cancel(connectId), + cancelOperation: (operationId: string) => lowLevelApi?.cancelOperation(operationId), + updateSettings: settings => lowLevelApi?.updateSettings(settings) ?? Promise.resolve(false), switchTransport: (env: ConnectSettings['env']) => diff --git a/packages/core/src/types/api/export.ts b/packages/core/src/types/api/export.ts index e1fa8f0dc..4b9c5a8f9 100644 --- a/packages/core/src/types/api/export.ts +++ b/packages/core/src/types/api/export.ts @@ -28,6 +28,7 @@ export type { FirmwareUpdateParams, FirmwareUpdateBinaryParams, FirmwareUpdateV3Params, + FirmwareUpdateV4Params, } from './firmwareUpdate'; export type { AllFirmwareRelease } from './checkAllFirmwareRelease'; diff --git a/packages/core/src/types/api/firmwareUpdate.ts b/packages/core/src/types/api/firmwareUpdate.ts index 02e2691b6..151f0ffd4 100644 --- a/packages/core/src/types/api/firmwareUpdate.ts +++ b/packages/core/src/types/api/firmwareUpdate.ts @@ -41,6 +41,7 @@ export declare function firmwareUpdateV2( export interface FirmwareUpdateV3Params { bleVersion?: number[]; bleBinary?: ArrayBuffer; + chunkSize?: number; firmwareVersion?: number[]; firmwareBinary?: ArrayBuffer; @@ -56,6 +57,53 @@ export interface FirmwareUpdateV3Params { platform: IPlatform; } +/** + * firmwareUpdateV4(Protocol V2)按 DeviceFirmwareTargetType 拆分的目标二进制。 + * 除 romloader 外,每个字段对应一个 bootloader 可接受的固件升级 target。 + */ +export type FirmwareUpdateV4Target = + | 'boot' + | 'app_v1' + | 'app_v2' + | 'coprocessor' + | 'resource' + | 'se01' + | 'se02' + | 'se03' + | 'se04'; + +export interface FirmwareUpdateV4Params { + platform: IPlatform; + chunkSize?: number; + firmwareType?: EFirmwareType; + targetsToUpdate?: FirmwareUpdateV4Target[]; + + /** FW_MGMT_TARGET_ROMLOADER = 2;当前 Pro2 bootloader 不接受通过 firmwareUpdateV4 安装 */ + romloaderBinary?: ArrayBuffer; + /** FW_MGMT_TARGET_BOOTLOADER = 3 */ + bootloaderBinary?: ArrayBuffer; + /** FW_MGMT_TARGET_APPLICATION_P1 = 4 */ + applicationP1Binary?: ArrayBuffer; + /** FW_MGMT_TARGET_APPLICATION_P2 = 5 */ + applicationP2Binary?: ArrayBuffer; + /** FW_MGMT_TARGET_COPROCESSOR = 6 */ + coprocessorBinary?: ArrayBuffer; + /** FW_MGMT_TARGET_SE01-04 = 7-10 */ + se01Binary?: ArrayBuffer; + se02Binary?: ArrayBuffer; + se03Binary?: ArrayBuffer; + se04Binary?: ArrayBuffer; + forcedUpdateRes?: boolean; + /** + * RESC bundle okpkg 列表,通过 FilesystemFileWrite 直写到 devicePath(vol0:/bundles/...)。 + * 手动传入模式:SDK 直接 FileWrite 安装,不做版本比对。 + */ + resourceBundleFiles?: Array<{ + binary: ArrayBuffer; + devicePath: string; + }>; +} + export declare function firmwareUpdateV3( connectId: string | undefined, params: Params @@ -64,3 +112,12 @@ export declare function firmwareUpdateV3( firmwareVersion: string; bootloaderVersion: string; }>; + +export declare function firmwareUpdateV4( + connectId: string | undefined, + params: Params +): Response<{ + bleVersion: string; + firmwareVersion: string; + bootloaderVersion: string; +}>; diff --git a/packages/core/src/types/api/getDeviceInfo.ts b/packages/core/src/types/api/getDeviceInfo.ts new file mode 100644 index 000000000..cb73f057d --- /dev/null +++ b/packages/core/src/types/api/getDeviceInfo.ts @@ -0,0 +1,101 @@ +import type { CommonParams, Response } from '../params'; +import type { Features, IDeviceType, OnekeyFeatures } from '../device'; +import type { EFirmwareType } from '@onekeyfe/hd-shared'; +import type { ProtocolType, ProtocolV2DeviceInfo } from '@onekeyfe/hd-transport'; + +// 协议类型单源:从 hd-transport 的 ProtocolType 派生,只额外允许 'unknown'。 +export type DeviceInfoProtocol = ProtocolType | 'unknown'; + +export type DeviceInfoSource = 'features' | 'protocolV1OneKeyFeatures' | 'deviceInfo'; + +export type DeviceInfoScope = 'basic' | 'versions' | 'verify' | 'full'; + +export type GetDeviceInfoParams = { + scope?: DeviceInfoScope; + refresh?: boolean; + includeRaw?: boolean; +}; + +export type DeviceInfoMode = 'normal' | 'bootloader' | 'romloader' | 'notInitialized' | 'unknown'; + +export type DeviceInfoStatus = { + mode: DeviceInfoMode; + initialized: boolean | null; + bootloaderMode: boolean | null; + unlocked: boolean | null; + passphraseProtection: boolean | null; + attachToPinEnabled?: boolean | null; + unlockedAttachPin?: boolean | null; + backupRequired: boolean | null; + noBackup: boolean | null; + language: string | null; + bleEnabled: boolean | null; +}; + +export type DeviceProfileVersions = { + firmware: string | null; + bootloader: string | null; + board: string | null; + ble: string | null; + se01?: string | null; + se02?: string | null; + se03?: string | null; + se04?: string | null; + se01Boot?: string | null; + se02Boot?: string | null; + se03Boot?: string | null; + se04Boot?: string | null; +}; + +export type DeviceProfileVerify = { + firmwareBuildId?: string; + firmwareHash?: string; + bootloaderBuildId?: string; + bootloaderHash?: string; + boardBuildId?: string; + boardHash?: string; + bleBuildId?: string; + bleHash?: string; + se01BuildId?: string; + se01Hash?: string; + se02BuildId?: string; + se02Hash?: string; + se03BuildId?: string; + se03Hash?: string; + se04BuildId?: string; + se04Hash?: string; + se01BootBuildId?: string; + se01BootHash?: string; + se02BootBuildId?: string; + se02BootHash?: string; + se03BootBuildId?: string; + se03BootHash?: string; + se04BootBuildId?: string; + se04BootHash?: string; +}; + +export type DeviceProfileRaw = { + features?: Features; + protocolV1OneKeyFeatures?: OnekeyFeatures; + protocolV2DeviceInfo?: ProtocolV2DeviceInfo; +}; + +export type DeviceProfile = { + protocol: DeviceInfoProtocol; + sources: DeviceInfoSource[]; + deviceType: IDeviceType; + firmwareType: EFirmwareType; + deviceId: string; + serialNo: string; + label: string | null; + bleName: string | null; + status: DeviceInfoStatus; + versions: DeviceProfileVersions; + verify?: DeviceProfileVerify; + raw?: DeviceProfileRaw; +}; + +export declare function getDeviceInfo( + connectId?: string, + params?: CommonParams & GetDeviceInfoParams +): Response; diff --git a/packages/core/src/types/api/getPassphraseState.ts b/packages/core/src/types/api/getPassphraseState.ts index 5d2a37b8d..be841a368 100644 --- a/packages/core/src/types/api/getPassphraseState.ts +++ b/packages/core/src/types/api/getPassphraseState.ts @@ -1,6 +1,8 @@ import type { CommonParams, Response } from '../params'; +export type GetPassphraseStateParams = CommonParams; + export declare function getPassphraseState( connectId?: string, - params?: CommonParams -): Response; + params?: GetPassphraseStateParams +): Response; diff --git a/packages/core/src/types/api/index.ts b/packages/core/src/types/api/index.ts index 2c31ae10e..1e778af21 100644 --- a/packages/core/src/types/api/index.ts +++ b/packages/core/src/types/api/index.ts @@ -1,9 +1,44 @@ +import type { + deviceFactoryInfoGet, + deviceFactoryInfoSet, + deviceFirmwareUpdate, + deviceGetFirmwareUpdateStatus, + deviceGetOnboardingStatus, + deviceInfoGet, + deviceReboot, + deviceSessionGet, + deviceSettingsGet, + deviceSettingsPageShow, + deviceSettingsSet, + deviceStatusGet, + deviceUploadWallpaper, + dirList, + dirMake, + dirRemove, + fileDelete, + fileRead, + fileWrite, + filesystemDirList, + filesystemDirMake, + filesystemDirRemove, + filesystemFileDelete, + filesystemFileRead, + filesystemFileWrite, + filesystemFormat, + filesystemPathInfoQuery, + filesystemPermissionFix, + pathInfo, + ping, + protocolInfoRequest, + uploadPortfolio, +} from './protocolV2'; import type { off, on, removeAllListeners } from './event'; import type { uiResponse } from './uiResponse'; import type { init, updateSettings } from './init'; import type { testInitializeDeviceDuration } from './testInitializeDeviceDuration'; import type { preInitialize } from './preInitialize'; import type { getLogs } from './getLogs'; +import type { clearSessionCache } from './sessionCache'; import type { checkBridgeStatus } from './checkBridgeStatus'; import type { checkBridgeRelease } from './checkBridgeRelease'; import type { checkBootloaderRelease } from './checkBootloaderRelease'; @@ -11,11 +46,17 @@ import type { checkAllFirmwareRelease } from './checkAllFirmwareRelease'; import type { checkFirmwareTypeAvailable } from './checkFirmwareTypeAvailable'; import type { searchDevices } from './searchDevices'; import type { getFeatures } from './getFeatures'; +import type { getDeviceInfo } from './getDeviceInfo'; import type { getOnekeyFeatures } from './getOnekeyFeatures'; import type { getPassphraseState } from './getPassphraseState'; import type { checkFirmwareRelease } from './checkFirmwareRelease'; import type { checkBLEFirmwareRelease } from './checkBLEFirmwareRelease'; -import type { firmwareUpdate, firmwareUpdateV2, firmwareUpdateV3 } from './firmwareUpdate'; +import type { + firmwareUpdate, + firmwareUpdateV2, + firmwareUpdateV3, + firmwareUpdateV4, +} from './firmwareUpdate'; import type { promptWebDeviceAccess } from './promptWebDeviceAccess'; import type { deviceReset } from './deviceReset'; import type { deviceRecovery } from './deviceRecovery'; @@ -131,6 +172,20 @@ import type { neoSignTransaction } from './neoSignTransaction'; import type { ConnectSettings } from '../settings'; export * from './export'; +export type { + DeviceInfoMode, + DeviceInfoProtocol, + DeviceInfoScope, + DeviceInfoSource, + DeviceInfoStatus, + DeviceProfile, + GetDeviceInfoParams, + DeviceProfileRaw, + DeviceProfileVerify, + DeviceProfileVersions, +} from './getDeviceInfo'; +export type { GetPassphraseStateParams } from './getPassphraseState'; +export type { ClearSessionCacheParams, ClearSessionCachePayload } from './sessionCache'; export type CoreApi = { /** @@ -145,9 +200,11 @@ export type CoreApi = { call: (params: any) => Promise; uiResponse: typeof uiResponse; cancel: (connectId?: string) => void; + cancelOperation: (operationId: string) => void; updateSettings: typeof updateSettings; switchTransport: (env: ConnectSettings['env']) => Promise<{ success: boolean }>; getLogs: typeof getLogs; + clearSessionCache: typeof clearSessionCache; /** * Test function @@ -170,6 +227,7 @@ export type CoreApi = { searchDevices: typeof searchDevices; promptWebDeviceAccess: typeof promptWebDeviceAccess; getFeatures: typeof getFeatures; + getDeviceInfo: typeof getDeviceInfo; getOnekeyFeatures: typeof getOnekeyFeatures; getPassphraseState: typeof getPassphraseState; deviceBackup: typeof deviceBackup; @@ -197,8 +255,45 @@ export type CoreApi = { firmwareUpdate: typeof firmwareUpdate; firmwareUpdateV2: typeof firmwareUpdateV2; firmwareUpdateV3: typeof firmwareUpdateV3; + firmwareUpdateV4: typeof firmwareUpdateV4; cipherKeyValue: typeof cipherKeyValue; + /** + * File system & device control API (Protocol V2 only) + */ + protocolInfoRequest: typeof protocolInfoRequest; + ping: typeof ping; + deviceReboot: typeof deviceReboot; + deviceInfoGet: typeof deviceInfoGet; + deviceStatusGet: typeof deviceStatusGet; + deviceGetOnboardingStatus: typeof deviceGetOnboardingStatus; + deviceSessionGet: typeof deviceSessionGet; + deviceFirmwareUpdate: typeof deviceFirmwareUpdate; + deviceGetFirmwareUpdateStatus: typeof deviceGetFirmwareUpdateStatus; + deviceFactoryInfoSet: typeof deviceFactoryInfoSet; + deviceFactoryInfoGet: typeof deviceFactoryInfoGet; + deviceSettingsGet: typeof deviceSettingsGet; + deviceSettingsSet: typeof deviceSettingsSet; + deviceSettingsPageShow: typeof deviceSettingsPageShow; + deviceUploadWallpaper: typeof deviceUploadWallpaper; + filesystemPermissionFix: typeof filesystemPermissionFix; + fileRead: typeof fileRead; + fileWrite: typeof fileWrite; + fileDelete: typeof fileDelete; + dirList: typeof dirList; + dirMake: typeof dirMake; + dirRemove: typeof dirRemove; + pathInfo: typeof pathInfo; + filesystemFileRead: typeof filesystemFileRead; + filesystemFileWrite: typeof filesystemFileWrite; + uploadPortfolio: typeof uploadPortfolio; + filesystemFileDelete: typeof filesystemFileDelete; + filesystemDirList: typeof filesystemDirList; + filesystemDirMake: typeof filesystemDirMake; + filesystemDirRemove: typeof filesystemDirRemove; + filesystemPathInfoQuery: typeof filesystemPathInfoQuery; + filesystemFormat: typeof filesystemFormat; + /** * All network function */ diff --git a/packages/core/src/types/api/protocolV2.ts b/packages/core/src/types/api/protocolV2.ts new file mode 100644 index 000000000..cf7b481c9 --- /dev/null +++ b/packages/core/src/types/api/protocolV2.ts @@ -0,0 +1,239 @@ +import type { CommonParams, Response } from '../params'; +import type { + DeviceFactoryInfo, + DeviceFirmwareUpdateStatus, + DevOnboardingStatus, + DeviceSession, + DeviceSettings, + DeviceStatus, + ProtocolInfo, + ProtocolV2DeviceInfo, + Success, +} from '@onekeyfe/hd-transport'; +import type { + DeviceFactoryInfoSetParams, + DeviceFirmwareUpdateParams, + DeviceFirmwareUpdateStatusGetParams, + DeviceRebootParams, +} from '../../api/protocol-v2/helpers'; +import type { DeviceInfoGetParams } from '../../api/protocol-v2/DeviceInfoGet'; +import type { DeviceSettingsSetParams } from '../../api/protocol-v2/DeviceSettingsSet'; +import type { DeviceSettingsPageShowParams } from '../../api/protocol-v2/DeviceSettingsPageShow'; +import type { + DeviceUploadWallpaperParams, + DeviceUploadWallpaperResponse, +} from '../../api/protocol-v2/DeviceUploadWallpaper'; + +// 参数类型单源:以 api/protocol-v2 的实现为准(type-only re-export,无运行时依赖) +export type { + DeviceFirmwareTargetInput, + DeviceFirmwareUpdateParams, + DeviceFirmwareUpdateStatusGetParams, + DeviceRebootParams, + DeviceFactoryInfoSetParams, + RebootTypeInput, +} from '../../api/protocol-v2/helpers'; +export type { + DeviceInfoGetParams, + DeviceInfoGetTargets, + DeviceInfoGetTypes, +} from '../../api/protocol-v2/DeviceInfoGet'; +export type { DeviceSettingsSetParams } from '../../api/protocol-v2/DeviceSettingsSet'; +export type { + DeviceSettingsPageShowParams, + SupportedDeviceSettingsPage, +} from '../../api/protocol-v2/DeviceSettingsPageShow'; +export type { + DeviceUploadWallpaperParams, + DeviceUploadWallpaperResponse, +} from '../../api/protocol-v2/DeviceUploadWallpaper'; + +// ── Shared response shapes (Protocol V2 file system) ──────────────────── + +export type FileOpSuccess = { message?: string }; + +export type FileInfo = { + path: string; + offset: number; + total_size: number; + data?: Uint8Array; + data_hash?: number; + processed_byte?: number; + chunks?: number; +}; + +export type DirInfo = { + path: string; + child_dirs?: string; + child_files?: string; +}; + +// proto 中 FilesystemPathInfo 的全部字段均为 required,类型与之保持一致 +export type PathInfoResult = { + exist: boolean; + size: number; + year: number; + month: number; + day: number; + hour: number; + minute: number; + second: number; + readonly: boolean; + hidden: boolean; + system: boolean; + archive: boolean; + directory: boolean; +}; + +// ── Method signatures ───────────────────────────────────────────────────── + +export declare function protocolInfoRequest( + connectId: string, + params?: CommonParams +): Response; + +export declare function ping( + connectId: string, + params?: CommonParams & { message?: string } +): Response; + +export declare function deviceReboot( + connectId: string, + params: CommonParams & DeviceRebootParams +): Response; + +export declare function deviceInfoGet( + connectId: string, + params?: CommonParams & DeviceInfoGetParams +): Response; + +export declare function deviceStatusGet( + connectId: string, + params?: CommonParams +): Response; + +export declare function deviceGetOnboardingStatus( + connectId: string, + params?: CommonParams +): Response; + +export declare function deviceSessionGet( + connectId: string, + params?: CommonParams +): Response; + +export declare function deviceFirmwareUpdate( + connectId: string, + params: CommonParams & DeviceFirmwareUpdateParams +): Response; + +export declare function deviceGetFirmwareUpdateStatus( + connectId: string, + params?: CommonParams & DeviceFirmwareUpdateStatusGetParams +): Response; + +export declare function deviceFactoryInfoSet( + connectId: string, + params: CommonParams & DeviceFactoryInfoSetParams +): Response; + +export declare function deviceFactoryInfoGet( + connectId: string, + params?: CommonParams +): Response; + +export declare function deviceSettingsGet( + connectId: string, + params?: CommonParams +): Response; + +export declare function deviceSettingsSet( + connectId: string, + params: CommonParams & DeviceSettingsSetParams +): Response; + +export declare function deviceSettingsPageShow( + connectId: string, + params: CommonParams & DeviceSettingsPageShowParams +): Response; + +export declare function deviceUploadWallpaper( + connectId: string, + params: CommonParams & DeviceUploadWallpaperParams +): Response; + +export declare function filesystemPermissionFix( + connectId: string, + params?: CommonParams +): Response; + +export declare function fileRead( + connectId: string, + params: { + path: string; + offset?: number; + totalSize?: number; + chunkLen?: number; + uiPercentage?: number; + } +): Response; + +export declare function fileWrite( + connectId: string, + params: { + path: string; + offset?: number; + totalSize?: number; + chunkSize?: number; + chunkLen?: number; + data: ArrayBuffer | Uint8Array | Blob | string; + overwrite?: boolean; + append?: boolean; + uiPercentage?: number; + timeoutMs?: number | string; + } +): Response; + +export declare function fileDelete( + connectId: string, + params: { path: string } +): Response; + +export declare function dirList( + connectId: string, + params: { path: string; depth?: number } +): Response; + +export declare function dirMake( + connectId: string, + params: { path: string } +): Response; + +export declare function dirRemove( + connectId: string, + params: { path: string } +): Response; + +export declare function pathInfo( + connectId: string, + params: { path: string; timeoutMs?: number | string } +): Response; + +export declare const filesystemFileRead: typeof fileRead; +export declare const filesystemFileWrite: typeof fileWrite; + +export declare function uploadPortfolio( + connectId: string, + params: { + operationId?: string; + packageBytes: ArrayBuffer | Uint8Array | Blob; + timeoutMs?: number | string; + } +): Response; +export declare const filesystemFileDelete: typeof fileDelete; +export declare const filesystemDirList: typeof dirList; +export declare const filesystemDirMake: typeof dirMake; +export declare const filesystemDirRemove: typeof dirRemove; +export declare const filesystemPathInfoQuery: typeof pathInfo; + +export declare function filesystemFormat(connectId: string): Response; diff --git a/packages/core/src/types/api/searchDevices.ts b/packages/core/src/types/api/searchDevices.ts index aeb127e4a..bbb7f5693 100644 --- a/packages/core/src/types/api/searchDevices.ts +++ b/packages/core/src/types/api/searchDevices.ts @@ -1,4 +1,4 @@ import type { SearchDevice } from '../device'; -import type { Response } from '../params'; +import type { CommonParams, Response } from '../params'; -export declare function searchDevices(): Response; +export declare function searchDevices(params?: CommonParams): Response; diff --git a/packages/core/src/types/api/sessionCache.ts b/packages/core/src/types/api/sessionCache.ts new file mode 100644 index 000000000..53ab25699 --- /dev/null +++ b/packages/core/src/types/api/sessionCache.ts @@ -0,0 +1,14 @@ +import type { Response } from '../params'; + +export type ClearSessionCacheParams = { + deviceId?: string; + passphraseState?: string; +}; + +export type ClearSessionCachePayload = { + cleared: true; +}; + +export declare function clearSessionCache( + params?: ClearSessionCacheParams +): Response; diff --git a/packages/core/src/types/device.ts b/packages/core/src/types/device.ts index 0d44f0da8..65aa15c19 100644 --- a/packages/core/src/types/device.ts +++ b/packages/core/src/types/device.ts @@ -1,8 +1,8 @@ -import { EDeviceType } from '@onekeyfe/hd-shared'; +import { EDeviceType, type EFirmwareType } from '@onekeyfe/hd-shared'; import type { IVersionArray } from './settings'; import type { PROTO } from '../constants'; -import type { OneKeyDeviceCommType } from '@onekeyfe/hd-transport'; +import type { OneKeyDeviceCommType, ProtocolV2DeviceInfo } from '@onekeyfe/hd-transport'; export type DeviceStatus = 'available' | 'occupied' | 'used'; @@ -33,7 +33,8 @@ export type KnownDevice = { name: string; error?: typeof undefined; mode: EOneKeyDeviceMode; - features: PROTO.Features; + features?: Features; + sessionId?: string | null; unavailableCapabilities: UnavailableCapabilities; bleFirmwareVersion: IVersionArray | null; firmwareVersion: IVersionArray | null; @@ -85,7 +86,102 @@ export type SearchDevice = { export type Device = KnownDevice; -export type Features = PROTO.Features; +export type DeviceFeaturesProtocol = 'V1' | 'V2' | 'unknown'; + +export type DeviceFeaturesMode = + | 'normal' + | 'bootloader' + | 'romloader' + | 'notInitialized' + | 'backupMode' + | 'unknown'; + +export type DeviceFeaturesVerify = { + firmwareBuildId?: string; + firmwareHash?: string; + bootloaderBuildId?: string; + bootloaderHash?: string; + boardBuildId?: string; + boardHash?: string; + bleBuildId?: string; + bleHash?: string; + se01BuildId?: string; + se01Hash?: string; + se02BuildId?: string; + se02Hash?: string; + se03BuildId?: string; + se03Hash?: string; + se04BuildId?: string; + se04Hash?: string; + se01BootBuildId?: string; + se01BootHash?: string; + se02BootBuildId?: string; + se02BootHash?: string; + se03BootBuildId?: string; + se03BootHash?: string; + se04BootBuildId?: string; + se04BootHash?: string; +}; + +export type DeviceFeaturesRaw = { + protocolV1Features?: PROTO.Features; + protocolV1OneKeyFeatures?: OnekeyFeatures; + protocolV2DeviceInfo?: ProtocolV2DeviceInfo; +}; + +export type Features = { + protocol: DeviceFeaturesProtocol; + protocolVersion?: number | null; + deviceType: IDeviceType; + firmwareType: EFirmwareType; + model: string | null; + vendor: string | null; + deviceId: string | null; + serialNo: string; + label: string | null; + bleName: string | null; + capabilities: Array; + mode: DeviceFeaturesMode; + initialized: boolean | null; + bootloaderMode: boolean | null; + unlocked: boolean | null; + firmwarePresent: boolean | null; + passphraseProtection: boolean | null; + pinProtection: boolean | null; + backupRequired: boolean | null; + noBackup: boolean | null; + unfinishedBackup: boolean | null; + recoveryMode: boolean | null; + language: string | null; + bleEnabled: boolean | null; + sdCardPresent: boolean | null; + sdProtection: boolean | null; + wipeCodeProtection: boolean | null; + passphraseAlwaysOnDevice: boolean | null; + attachToPinEnabled?: boolean | null; + safetyChecks: string | null; + autoLockDelayMs: number | null; + displayRotation: number | null; + experimentalFeatures: boolean | null; + firmwareVersion: string | null; + bootloaderVersion: string | null; + boardVersion: string | null; + bleVersion: string | null; + se01Version?: string | null; + se02Version?: string | null; + se03Version?: string | null; + se04Version?: string | null; + se01BootVersion?: string | null; + se02BootVersion?: string | null; + se03BootVersion?: string | null; + se04BootVersion?: string | null; + seVersion?: string | null; + verify?: DeviceFeaturesVerify; + sessionId: string | null; + passphraseState?: string; + unlockedAttachPin?: boolean; + raw?: DeviceFeaturesRaw; +}; export type OnekeyFeatures = PROTO.OnekeyFeatures; @@ -96,7 +192,8 @@ export type IDeviceType = | EDeviceType.ClassicPure | EDeviceType.Mini | EDeviceType.Touch - | EDeviceType.Pro; + | EDeviceType.Pro + | EDeviceType.Pro2; /** * model_classic: 'classic' | 'classic1s' | 'classicpure' @@ -124,6 +221,7 @@ export const DeviceTypeToModels: { [deviceType in IDeviceType]: IDeviceModel[] } [EDeviceType.Mini]: ['model_mini'], [EDeviceType.Touch]: ['model_touch'], [EDeviceType.Pro]: ['model_touch'], + [EDeviceType.Pro2]: [], [EDeviceType.Unknown]: [], }; @@ -136,6 +234,7 @@ export type ITransportStatus = 'valid' | 'outdated'; export type IVersionRange = { min: string; max?: string; + unsupported?: boolean; }; export type DeviceFirmwareRange = { diff --git a/packages/core/src/types/params.ts b/packages/core/src/types/params.ts index b2cb4e935..6725527ae 100644 --- a/packages/core/src/types/params.ts +++ b/packages/core/src/types/params.ts @@ -1,3 +1,5 @@ +import type { HardwareConnectProtocol } from '@onekeyfe/hd-shared'; + export interface CommonParams { keepSession?: boolean; /** @@ -12,6 +14,10 @@ export interface CommonParams { * Timeout time for single polling */ timeout?: number; + /** + * Protocol V2 初始化阶段 DeviceInfoGet 超时时间 + */ + protocolV2DeviceInfoTimeoutMs?: number; /** * passphrase state */ @@ -52,6 +58,11 @@ export interface CommonParams { * Use pre-initialized device state (BLE only) */ usePreInitialize?: boolean; + + /** + * Expected transport protocol. If omitted, SDK probes Protocol V1 then Protocol V2. + */ + connectProtocol?: HardwareConnectProtocol; } export type Params = CommonParams & T & { bundle?: undefined }; diff --git a/packages/core/src/types/settings.ts b/packages/core/src/types/settings.ts index fd8cf3065..8274b5f46 100644 --- a/packages/core/src/types/settings.ts +++ b/packages/core/src/types/settings.ts @@ -40,6 +40,40 @@ export type IVersionArray = [number, number, number]; export type ILocale = 'zh-CN' | 'en-US'; +export type IProtocolV2FirmwareComponentTarget = + | 'ROMLOADER' + | 'BOOTLOADER' + | 'APPLICATION_P1' + | 'APPLICATION_P2' + | 'COPROCESSOR' + | 'SE01' + | 'SE02' + | 'SE03' + | 'SE04'; + +export type IProtocolV2FirmwareComponent = { + target: IProtocolV2FirmwareComponentTarget; + url: string; + fingerprint?: string; + version?: IVersionArray; +}; + +/** Pro2 RESC bundle okpkg 描述,用于 FileWrite 直写方式增量同步资源 */ +export type IProtocolV2ResourceBundle = { + /** bundle 名称,如 images / animation / translations / fonts_roobert */ + name: string; + /** 下载 URL */ + url: string; + /** 设备上的目标路径,如 vol0:/bundles/images/images.okpkg */ + devicePath: string; + /** okpkg container 的 payload_version(用于 FileRead 比对跳过) */ + version?: IVersionArray; + /** okpkg container 的 payload_hash(SHA3-512,用于 FileRead 比对) */ + payloadHash?: string; + /** okpkg container 的 header_hash(SHA3-512,用于 FileRead 比对) */ + headerHash?: string; +}; + /** STM32 firmware config */ export type IFirmwareReleaseInfo = { required: boolean; @@ -58,6 +92,11 @@ export type IFirmwareReleaseInfo = { bootloaderVersion?: IVersionArray; displayBootloaderVersion?: IVersionArray; bootloaderRelatedFirmwareVersion?: IVersionArray; + upgradeType?: 'payload-package-set' | string; + components?: Record; + installOrder?: string[]; + /** Pro2 RESC bundle 列表(FileWrite 直写增量同步模式) */ + resourceBundles?: IProtocolV2ResourceBundle[]; bootloaderChangelog?: { [k in ILocale]: string; }; @@ -85,41 +124,14 @@ export type IBLEFirmwareReleaseInfo = { type IKnownDevice = Exclude; -/** - * Device firmware configuration map - * - * IMPORTANT: This type is used for firmware update logic. - * - DO NOT remove existing firmware fields - * - Only ADD new optional firmware fields for new versions - * - 'firmware' field is required for backward compatibility - * - 'ble' field is required for BLE firmware updates - * - * @example - * // When adding firmware-v8: - * // { - * // firmware: IFirmwareReleaseInfo[]; - * // 'firmware-v2'?: IFirmwareReleaseInfo[]; - * // 'firmware-v8'?: IFirmwareReleaseInfo[]; - * // 'firmware-v8'?: IFirmwareReleaseInfo[]; // New - * // 'firmware-btc-v8'?: IFirmwareReleaseInfo[]; - * // 'firmware-btc-v8'?: IFirmwareReleaseInfo[]; // New - * // ble: IBLEFirmwareReleaseInfo[]; - * // } - */ export type DeviceTypeMap = { [k in IKnownDevice]: { - /** Base firmware field (required for backward compatibility) */ firmware: IFirmwareReleaseInfo[]; - /** Firmware v2 (Touch/Pro specific) */ + /** Pro2 Protocol V2 payload package set */ + 'firmware-v1'?: IFirmwareReleaseInfo[]; 'firmware-v2'?: IFirmwareReleaseInfo[]; - /** Universal firmware v7 */ 'firmware-v8'?: IFirmwareReleaseInfo[]; - /** Bitcoin-only firmware v7 */ 'firmware-btc-v8'?: IFirmwareReleaseInfo[]; - // Future firmware versions should be added here as optional fields: - // 'firmware-v8'?: IFirmwareReleaseInfo[]; - // 'firmware-btc-v8'?: IFirmwareReleaseInfo[]; - /** BLE firmware (required) */ ble: IBLEFirmwareReleaseInfo[]; }; }; diff --git a/packages/core/src/utils/capabilitieUtils.ts b/packages/core/src/utils/capabilitieUtils.ts index ffebe0d50..f3b3fa528 100644 --- a/packages/core/src/utils/capabilitieUtils.ts +++ b/packages/core/src/utils/capabilitieUtils.ts @@ -2,8 +2,7 @@ import type { Enum_Capability } from '@onekeyfe/hd-transport'; import type { Features } from '../types/device'; export const existCapability = (features?: Features, capability?: Enum_Capability) => - // @ts-expect-error - features?.capabilities?.includes(capability); + capability !== undefined && features?.capabilities?.includes(capability); export const requireCapability = (features: Features, capability: Enum_Capability) => { if (!existCapability(features, capability)) { diff --git a/packages/core/src/utils/deviceFeaturesUtils.ts b/packages/core/src/utils/deviceFeaturesUtils.ts index 595365bdb..2bd565a7c 100644 --- a/packages/core/src/utils/deviceFeaturesUtils.ts +++ b/packages/core/src/utils/deviceFeaturesUtils.ts @@ -1,27 +1,30 @@ import semver from 'semver'; import { isNaN } from 'lodash'; import { EDeviceType, type EFirmwareType, ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared'; -import { Enum_Capability } from '@onekeyfe/hd-transport'; +import { Enum_Capability, type GetPassphraseState } from '@onekeyfe/hd-transport'; import { toHardened } from '../api/helpers/pathUtils'; import { DeviceModelToTypes, DeviceTypeToModels } from '../types'; -import DataManager, { type IFirmwareField, type MessageVersion } from '../data-manager/DataManager'; +import DataManager, { + type IFirmwareField, + type ProtocolV1MessageSchema, +} from '../data-manager/DataManager'; import { PROTOBUF_MESSAGE_CONFIG } from '../data-manager/MessagesConfig'; import { getDeviceType } from './deviceInfoUtils'; import { getDeviceFirmwareVersion } from './deviceVersionUtils'; import { existCapability } from './capabilitieUtils'; +import { getProtocolV2WalletSession } from '../protocols/protocol-v2/walletSession'; import type { Device } from '../device/Device'; -import type { DeviceCommands } from '../device/DeviceCommands'; -import type { Features, SupportFeatureType } from '../types'; +import type { Features, IDeviceType, SupportFeatureType } from '../types'; -export const getSupportMessageVersion = ( +export const getSupportProtocolV1MessageSchema = ( features: Features | undefined -): { messages: JSON; messageVersion: MessageVersion } => { +): { messages: JSON; protocolV1MessageSchema: ProtocolV1MessageSchema } => { if (!features) return { - messages: DataManager.messages.latest, - messageVersion: 'latest', + messages: DataManager.messages.v1CurrentSchema, + protocolV1MessageSchema: 'v1CurrentSchema', }; const currentDeviceVersion = getDeviceFirmwareVersion(features).join('.'); @@ -37,18 +40,18 @@ export const getSupportMessageVersion = ( const sortedDeviceVersionConfigs = deviceVersionConfigs?.sort((a, b) => semver.compare(b.minVersion, a.minVersion)) ?? []; - for (const { minVersion, messageVersion } of sortedDeviceVersionConfigs) { + for (const { minVersion, protocolV1MessageSchema } of sortedDeviceVersionConfigs) { if (semver.gte(currentDeviceVersion, minVersion)) { return { - messages: DataManager.messages[messageVersion], - messageVersion, + messages: DataManager.messages[protocolV1MessageSchema], + protocolV1MessageSchema, }; } } return { - messages: DataManager.messages.latest, - messageVersion: 'latest', + messages: DataManager.messages.v1CurrentSchema, + protocolV1MessageSchema: 'v1CurrentSchema', }; }; @@ -68,7 +71,11 @@ export const supportNewPassphrase = (features?: Features): SupportFeatureType => if (!features) return { support: false }; const deviceType = getDeviceType(features); - if (deviceType === EDeviceType.Touch || deviceType === EDeviceType.Pro) { + if ( + deviceType === EDeviceType.Touch || + deviceType === EDeviceType.Pro || + deviceType === EDeviceType.Pro2 + ) { return { support: true }; } @@ -82,25 +89,45 @@ export const getPassphraseStateWithRefreshDeviceInfo = async ( options?: { expectPassphraseState?: string; onlyMainPin?: boolean; + initSession?: boolean; } ) => { - const { features, commands } = device; + if (device.isProtocolV2()) { + if (!device.features) { + return { + passphraseState: undefined, + newSession: undefined, + unlockedAttachPin: undefined, + }; + } + + return getProtocolV2WalletSession(device, { + initSession: options?.initSession, + expectedPassphraseState: options?.expectPassphraseState, + }); + } + + const { features } = device; const locked = features?.unlocked === false; + const deviceType = device.getCurrentDeviceType(); - const { passphraseState, newSession, unlockedAttachPin } = await getPassphraseState( - features, - commands, - { - ...options, - } - ); + if (options?.initSession) { + device.clearInternalState(); + } + + const { passphraseState, newSession, unlockedAttachPin } = await getPassphraseState(device, { + ...options, + }); const isModeT = - getDeviceType(features) === EDeviceType.Touch || getDeviceType(features) === EDeviceType.Pro; + deviceType === EDeviceType.Touch || + deviceType === EDeviceType.Pro || + deviceType === EDeviceType.Pro2; // 如果可以获取到 passphraseState,但是设备 features 显示设备未开启 passphrase,需要刷新设备状态 // if passphraseState can be obtained, but the device features show that the device has not enabled passphrase, the device status needs to be refreshed - const needRefreshWithPassphrase = passphraseState && features?.passphrase_protection !== true; + const needRefreshWithPassphrase = + passphraseState && device.getCurrentPassphraseProtection() !== true; // 如果 Touch/Pro 在之前是锁定状态,刷新设备状态 // if Touch/Pro was locked before, refresh the device state const needRefreshWithLocked = isModeT && locked; @@ -111,49 +138,71 @@ export const getPassphraseStateWithRefreshDeviceInfo = async ( } // Attach to pin try to fix internal state - if (features?.device_id) { - device.updateInternalState( - device.features?.passphrase_protection ?? false, - passphraseState, - device.features?.device_id ?? '', - newSession, - device.features?.session_id - ); - } + const deviceId = device.getCurrentDeviceId(); + device.updateInternalState( + device.getCurrentPassphraseProtection() ?? false, + passphraseState, + deviceId, + newSession, + options?.initSession ? null : device.features?.sessionId + ); - return { passphraseState, newSession, unlockedAttachPin }; + return { + passphraseState, + newSession, + unlockedAttachPin: unlockedAttachPin ?? device.features?.unlockedAttachPin, + }; }; +// 仅适用于 Protocol V1 的 Pro:Pro2 走独立版本线,不能套用 4.15.0 门槛 +const supportProSeriesAttachPinPassphrase = (deviceType: IDeviceType, firmwareVersion: string) => + deviceType === EDeviceType.Pro && semver.gte(firmwareVersion, '4.15.0'); + export const getPassphraseState = async ( - features: Features | undefined, - commands: DeviceCommands, + device: Device, options?: { expectPassphraseState?: string; onlyMainPin?: boolean; + initSession?: boolean; } ): Promise<{ passphraseState: string | undefined; newSession: string | undefined; unlockedAttachPin: boolean | undefined; }> => { + const { features, commands } = device; + + // 设备尚未建立任何状态时无法判定,保持旧的空返回语义 if (!features) return { passphraseState: undefined, newSession: undefined, unlockedAttachPin: undefined }; - const firmwareVersion = getDeviceFirmwareVersion(features); - const deviceType = getDeviceType(features); + const firmwareVersion = device.getCurrentFirmwareVersionString() ?? '0.0.0'; + const deviceType = device.getCurrentDeviceType(); + + if (device.isProtocolV2()) { + return getProtocolV2WalletSession(device, { + initSession: options?.initSession, + expectedPassphraseState: options?.expectPassphraseState, + }); + } const supportAttachPinCapability = existCapability( features, Enum_Capability.Capability_AttachToPin ); const supportGetPassphraseState = - supportAttachPinCapability || - (deviceType === EDeviceType.Pro && semver.gte(firmwareVersion.join('.'), '4.15.0')); + supportAttachPinCapability || supportProSeriesAttachPinPassphrase(deviceType, firmwareVersion); if (supportGetPassphraseState) { - const { message, type } = await commands.typedCall('GetPassphraseState', 'PassphraseState', { + const payload: GetPassphraseState = { passphrase_state: options?.onlyMainPin ? undefined : options?.expectPassphraseState, - }); + }; + + const { message, type } = await commands.typedCall( + 'GetPassphraseState', + 'PassphraseState', + payload + ); // @ts-expect-error if (type === 'CallMethodError') { @@ -186,40 +235,8 @@ export const getPassphraseState = async ( }; }; -export const supportBatchPublicKey = ( - features?: Features, - options?: { - includeNode?: boolean; - } -): boolean => { - if (!features) return false; - const currentVersion = getDeviceFirmwareVersion(features).join('.'); - - const deviceType = getDeviceType(features); - // btc batch get public key - if (!!options?.includeNode && deviceType === EDeviceType.Pro) { - return semver.gte(currentVersion, '4.14.0'); - } - if (!!options?.includeNode && deviceType === EDeviceType.Touch) { - return semver.gte(currentVersion, '4.11.0'); - } - if (!!options?.includeNode && DeviceModelToTypes.model_classic1s.includes(deviceType)) { - return semver.gte(currentVersion, '3.12.0'); - } - if (!!options?.includeNode && DeviceModelToTypes.model_mini.includes(deviceType)) { - return semver.gte(currentVersion, '3.10.0'); - } - if (options?.includeNode) { - return false; - } - - // support batch get public key - if (deviceType === EDeviceType.Touch || deviceType === EDeviceType.Pro) { - return semver.gte(currentVersion, '3.1.0'); - } - - return semver.gte(currentVersion, '2.6.0'); -}; +// supportBatchPublicKey 已迁移为 device-aware 版本: +// 见 api/helpers/batchGetPublickeys.ts 的 supportBatchPublicKeyByDevice export const supportModifyHomescreen = (features?: Features): SupportFeatureType => { if (!features) return { support: false }; @@ -278,6 +295,9 @@ export const getFirmwareUpdateField = ({ if (deviceType === EDeviceType.Pro) { return latestFirmwareField; } + if (deviceType === EDeviceType.Pro2) { + return 'firmware-v1'; + } return 'firmware'; }; /** @@ -321,6 +341,10 @@ export const getFirmwareUpdateFieldArray = ( return ['firmware-v8']; } + if (deviceType === 'pro2') { + return ['firmware-v1']; + } + return ['firmware']; }; @@ -340,12 +364,8 @@ export const fixFeaturesFirmwareVersion = (features: Features): Features => { // fix Touch、Pro device when bootloader version is lower than 2.5.2, the features returned do not have firmware_version error const tempFeatures = { ...features }; - if (tempFeatures.onekey_firmware_version && !semver.valid(tempFeatures.onekey_firmware_version)) { - tempFeatures.onekey_firmware_version = fixVersion(tempFeatures.onekey_firmware_version); - } - - if (tempFeatures.onekey_version && !semver.valid(tempFeatures.onekey_version)) { - tempFeatures.onekey_version = fixVersion(tempFeatures.onekey_version); + if (tempFeatures.firmwareVersion && !semver.valid(tempFeatures.firmwareVersion)) { + tempFeatures.firmwareVersion = fixVersion(tempFeatures.firmwareVersion); } return tempFeatures; diff --git a/packages/core/src/utils/deviceInfoUtils.ts b/packages/core/src/utils/deviceInfoUtils.ts index 9463224a7..b7538477a 100644 --- a/packages/core/src/utils/deviceInfoUtils.ts +++ b/packages/core/src/utils/deviceInfoUtils.ts @@ -14,27 +14,12 @@ export const getDeviceType = (features?: Features): IDeviceType => { if (!features || typeof features !== 'object') { return EDeviceType.Unknown; } + if (features.deviceType) { + return features.deviceType; + } - // classic1s 3.5.0 pro 4.6.0 - switch (features.onekey_device_type) { - case 'CLASSIC': - return EDeviceType.Classic; - case 'CLASSIC1S': - return EDeviceType.Classic1s; - case 'MINI': - return EDeviceType.Mini; - case 'TOUCH': - return EDeviceType.Touch; - case 'PRO': - return EDeviceType.Pro; - case 'PURE': - return EDeviceType.ClassicPure; - default: - // future And old device onekey_device_type is empty - if (!isEmpty(features.onekey_serial_no)) { - return EDeviceType.Unknown; - } - // old device type + if (features.model === EDeviceType.Pro2 || features.model === 'pro2') { + return EDeviceType.Pro2; } // low version hardware @@ -42,7 +27,7 @@ export const getDeviceType = (features?: Features): IDeviceType => { const serialNo = getDeviceUUID(features); // not exist serialNo, bootloader mode, model 1 is classic - if (isEmpty(serialNo) && features.bootloader_mode === true && features.model === '1') { + if (isEmpty(serialNo) && features.bootloaderMode === true && features.model === '1') { return EDeviceType.Classic; } @@ -56,6 +41,7 @@ export const getDeviceType = (features?: Features): IDeviceType => { if (miniFlag.toLowerCase() === 'mi') return EDeviceType.Mini; if (miniFlag.toLowerCase() === 'tc') return EDeviceType.Touch; if (miniFlag.toLowerCase() === 'pr') return EDeviceType.Pro; + if (miniFlag.toLowerCase() === 'p2') return EDeviceType.Pro2; // unknown device return EDeviceType.Unknown; @@ -68,13 +54,14 @@ export const getDeviceType = (features?: Features): IDeviceType => { export const getDeviceTypeByBleName = (name?: string): IDeviceType => { if (!name) return EDeviceType.Unknown; - if (name.startsWith('BixinKey')) return EDeviceType.Classic; - if (name.startsWith('K')) return EDeviceType.Classic; + if (/^BixinKey/i.test(name)) return EDeviceType.Classic; + if (/^K/i.test(name)) return EDeviceType.Classic; - if (name.startsWith('T')) return EDeviceType.Touch; - if (name.startsWith('Touch')) return EDeviceType.Touch; + if (/^T/i.test(name)) return EDeviceType.Touch; + if (/^Touch/i.test(name)) return EDeviceType.Touch; - if (name.startsWith('Pro')) return EDeviceType.Pro; + if (/\bPro\s*2\b/i.test(name) || /^Pro2/i.test(name)) return EDeviceType.Pro2; + if (/\bPro\b/i.test(name) || /^Pro/i.test(name)) return EDeviceType.Pro; return EDeviceType.Unknown; }; @@ -85,16 +72,13 @@ export const getDeviceTypeByBleName = (name?: string): IDeviceType => { */ export const getDeviceBleName = (features?: Features): string | null => { if (features == null) return null; - return features.onekey_ble_name || features.ble_name || null; + return features.bleName || null; }; /** * Get Connected Device UUID by features */ -export const getDeviceUUID = (features: Features) => { - const serialNo = features.onekey_serial_no || features.onekey_serial || features.serial_no; - return serialNo ?? ''; -}; +export const getDeviceUUID = (features: Features) => features.serialNo ?? ''; /** * Get Connected Device label by features @@ -128,8 +112,8 @@ export const getMethodVersionRange = ( getVersionRange: (deviceModel: IDeviceType | IDeviceModel) => IVersionRange | undefined ): IVersionRange | undefined => { const deviceType = getDeviceType(features); - let versionRange: IVersionRange | undefined = getVersionRange(deviceType); + const versionRange = getVersionRange(deviceType); if (versionRange) { return versionRange; } @@ -142,22 +126,25 @@ export const getMethodVersionRange = ( ]; for (const model of modelFallbacks) { if (DeviceModelToTypes[model].includes(deviceType)) { - versionRange = getVersionRange(model); + const versionRange = getVersionRange(model); if (versionRange) { return versionRange; } } } - return versionRange; + return undefined; }; +export const isMethodVersionRangeUnsupported = (versionRange?: IVersionRange): boolean => + versionRange?.unsupported === true; + export const getFirmwareType = (features: Features | undefined) => { if (!features) { return EFirmwareType.Universal; } - if (features.fw_vendor === 'OneKey Bitcoin-only') { - return EFirmwareType.BitcoinOnly; + if (features.firmwareType) { + return features.firmwareType; } // old firmware return features?.capabilities?.length > 0 && diff --git a/packages/core/src/utils/deviceVersionUtils.ts b/packages/core/src/utils/deviceVersionUtils.ts index 674b3a1ca..713b050f1 100644 --- a/packages/core/src/utils/deviceVersionUtils.ts +++ b/packages/core/src/utils/deviceVersionUtils.ts @@ -8,12 +8,8 @@ import type { Features, IVersionArray } from '../types'; export const getDeviceFirmwareVersion = (features: Features | undefined): IVersionArray => { if (!features) return [0, 0, 0]; - if (semver.valid(features.onekey_firmware_version)) { - return features.onekey_firmware_version?.split('.') as unknown as IVersionArray; - } - - if (semver.valid(features.onekey_version)) { - return features.onekey_version?.split('.') as unknown as IVersionArray; + if (features.firmwareVersion && semver.valid(features.firmwareVersion)) { + return features.firmwareVersion.split('.').map(Number) as IVersionArray; } return [0, 0, 0]; @@ -23,7 +19,7 @@ export const getDeviceFirmwareVersion = (features: Features | undefined): IVersi * Get Connected Device bluetooth firmware version by features */ export const getDeviceBLEFirmwareVersion = (features: Features): IVersionArray => { - const bleVer = features?.onekey_ble_version || features?.ble_ver; + const bleVer = features?.bleVersion; if (!bleVer) { return [0, 0, 0]; @@ -46,24 +42,10 @@ export const getDeviceBootloaderVersion = (features: Features | undefined): IVer if (!features) return [0, 0, 0]; // classic1s 3.5.0 pro 4.6.0 - if (semver.valid(features.onekey_boot_version)) { - return features.onekey_boot_version?.split('.') as unknown as IVersionArray; + if (features.bootloaderVersion && semver.valid(features.bootloaderVersion)) { + return features.bootloaderVersion.split('.').map(Number) as IVersionArray; } - // low version hardware - if (!features.bootloader_version) { - if (features.bootloader_mode) { - return [ - features?.major_version ?? 0, - features?.minor_version ?? 0, - features?.patch_version ?? 0, - ]; - } - return [0, 0, 0]; - } - if (semver.valid(features.bootloader_version)) { - return features.bootloader_version?.split('.') as unknown as IVersionArray; - } return [0, 0, 0]; }; @@ -71,8 +53,8 @@ export const getDeviceBootloaderVersion = (features: Features | undefined): IVer * Get Connected Device boardloader version by features */ export const getDeviceBoardloaderVersion = (features: Features): IVersionArray => { - if (semver.valid(features?.onekey_board_version)) { - return features?.onekey_board_version?.split('.') as unknown as IVersionArray; + if (features?.boardVersion && semver.valid(features.boardVersion)) { + return features.boardVersion.split('.').map(Number) as IVersionArray; } return [0, 0, 0]; diff --git a/packages/core/src/utils/findDefectiveBatchDevice.ts b/packages/core/src/utils/findDefectiveBatchDevice.ts index 2020d9b88..e0c833554 100644 --- a/packages/core/src/utils/findDefectiveBatchDevice.ts +++ b/packages/core/src/utils/findDefectiveBatchDevice.ts @@ -5,7 +5,7 @@ import type { Features } from '../types'; /** * 检测故障固件设备 * 检测规则: - * - 序列号范围:21032200001 到 21032201500 (从 onekey_serial 字段提取) + * - 序列号范围:21032200001 到 21032201500 * - SE版本为 1.1.0.2 * * 对齐之前版本的检测逻辑 @@ -13,13 +13,13 @@ import type { Features } from '../types'; export const findDefectiveBatchDevice = (features: Features) => { if (!features) return; - const { onekey_serial: onekeySerial, se_ver: seVer } = features; - if (!onekeySerial) return; + const { serialNo, seVersion } = features; + if (!serialNo) return; - const versionNum = +onekeySerial.slice(5); + const versionNum = +serialNo.slice(5); if (Number.isNaN(versionNum)) return; - return versionNum >= 21032200001 && versionNum <= 21032201500 && seVer === '1.1.0.2'; + return versionNum >= 21032200001 && versionNum <= 21032201500 && seVersion === '1.1.0.2'; }; /** @@ -29,7 +29,7 @@ export const getDefectiveDeviceInfo = (features: Features) => { if (!findDefectiveBatchDevice(features)) return null; const serialNo = getDeviceUUID(features); const deviceType = getDeviceType(features); - const seVersion = features.se_ver; + const { seVersion } = features; return { serialNo, diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 522cb3a08..7f4e6e697 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -11,6 +11,7 @@ export { getDeviceUUID, getDeviceLabel, getMethodVersionRange, + isMethodVersionRangeUnsupported, getFirmwareType, } from './deviceInfoUtils'; export { @@ -37,6 +38,8 @@ export { getHDPath, getScriptType, getOutputScriptType } from '../api/helpers/pa export const isBleConnect = (env: string) => env === 'react-native' || env === 'lowlevel'; export { getHomeScreenHex, getHomeScreenDefaultList, getHomeScreenSize } from './homescreen'; +export { encodePro2Wallpaper, PRO2_WALLPAPER_HEIGHT, PRO2_WALLPAPER_WIDTH } from './pro2Wallpaper'; +export type { Pro2WallpaperColorFormat } from './pro2Wallpaper'; export const wait = (ms: number) => new Promise(resolve => { diff --git a/packages/core/src/utils/pro2Wallpaper.ts b/packages/core/src/utils/pro2Wallpaper.ts new file mode 100644 index 000000000..ce5f704de --- /dev/null +++ b/packages/core/src/utils/pro2Wallpaper.ts @@ -0,0 +1,107 @@ +/* eslint-disable no-bitwise */ +import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared'; + +export const PRO2_WALLPAPER_WIDTH = 604; +export const PRO2_WALLPAPER_HEIGHT = 1024; + +export type Pro2WallpaperColorFormat = 'RGB565' | 'RGB565A8'; + +const COLOR_FORMAT_RGB565 = 0x12; +const COLOR_FORMAT_RGB565A8 = 0x14; + +const RED_THRESHOLD = [ + 1, 7, 3, 5, 0, 8, 2, 6, 7, 1, 5, 3, 8, 0, 6, 2, 3, 5, 0, 8, 2, 6, 1, 7, 5, 3, 8, 0, 6, 2, 7, 1, 0, + 8, 2, 6, 1, 7, 3, 5, 8, 0, 6, 2, 7, 1, 5, 3, 2, 6, 1, 7, 3, 5, 0, 8, 6, 2, 7, 1, 5, 3, 8, 0, +]; +const GREEN_THRESHOLD = [ + 1, 3, 2, 2, 3, 1, 2, 2, 2, 2, 0, 4, 2, 2, 4, 0, 3, 1, 2, 2, 1, 3, 2, 2, 2, 2, 4, 0, 2, 2, 0, 4, 1, + 3, 2, 2, 3, 1, 2, 2, 2, 2, 0, 4, 2, 2, 4, 0, 3, 1, 2, 2, 1, 3, 2, 2, 2, 2, 4, 0, 2, 2, 0, 4, +]; +const BLUE_THRESHOLD = [ + 5, 3, 8, 0, 6, 2, 7, 1, 3, 5, 0, 8, 2, 6, 1, 7, 8, 0, 6, 2, 7, 1, 5, 3, 0, 8, 2, 6, 1, 7, 3, 5, 6, + 2, 7, 1, 5, 3, 8, 0, 2, 6, 1, 7, 3, 5, 0, 8, 7, 1, 5, 3, 8, 0, 6, 2, 1, 7, 3, 5, 0, 8, 2, 6, +]; + +function invalidParameter(message: string): Error { + return ERRORS.TypedError(HardwareErrorCode.CallMethodInvalidParameter, message); +} + +function asBytes(rgba: Uint8Array | ArrayBuffer): Uint8Array { + return rgba instanceof Uint8Array ? rgba : new Uint8Array(rgba); +} + +function align(value: number, boundary: number): number { + return Math.ceil(value / boundary) * boundary; +} + +export function encodePro2Wallpaper(options: { + width: number; + height: number; + rgba: Uint8Array | ArrayBuffer; +}): { data: Uint8Array; colorFormat: Pro2WallpaperColorFormat } { + const { width, height } = options; + if (!Number.isInteger(width) || width <= 0 || width > 0xffff) { + throw invalidParameter('Wallpaper width must be an integer between 1 and 65535.'); + } + if (!Number.isInteger(height) || height <= 0 || height > 0xffff) { + throw invalidParameter('Wallpaper height must be an integer between 1 and 65535.'); + } + + const rgba = asBytes(options.rgba); + const expectedLength = width * height * 4; + if (rgba.byteLength !== expectedLength) { + throw invalidParameter( + `Wallpaper RGBA data length must be ${expectedLength} bytes, received ${rgba.byteLength}.` + ); + } + + let hasTransparency = false; + for (let index = 3; index < rgba.length; index += 4) { + if (rgba[index] !== 0xff) { + hasTransparency = true; + break; + } + } + + const colorFormat: Pro2WallpaperColorFormat = hasTransparency ? 'RGB565A8' : 'RGB565'; + const stride = align(width * 2, 4); + const alphaStride = stride / 2; + const rgbSize = stride * height; + const alphaSize = hasTransparency ? alphaStride * height : 0; + const data = new Uint8Array(12 + rgbSize + alphaSize); + const view = new DataView(data.buffer); + + data[0] = 0x19; + data[1] = hasTransparency ? COLOR_FORMAT_RGB565A8 : COLOR_FORMAT_RGB565; + view.setUint16(2, 0, true); + view.setUint16(4, width, true); + view.setUint16(6, height, true); + view.setUint16(8, stride, true); + view.setUint16(10, 0, true); + + for (let y = 0; y < height; y += 1) { + for (let x = 0; x < width; x += 1) { + const sourceOffset = (y * width + x) * 4; + const thresholdIndex = ((y & 7) << 3) + (x & 7); + let red = Math.min(rgba[sourceOffset] + RED_THRESHOLD[thresholdIndex], 0xff) & 0xf8; + let green = Math.min(rgba[sourceOffset + 1] + GREEN_THRESHOLD[thresholdIndex], 0xff) & 0xfc; + let blue = Math.min(rgba[sourceOffset + 2] + BLUE_THRESHOLD[thresholdIndex], 0xff) & 0xf8; + if (!hasTransparency) { + const alpha = rgba[sourceOffset + 3]; + red = (red * alpha) >> 8; + green = (green * alpha) >> 8; + blue = (blue * alpha) >> 8; + } + const rgb565 = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3); + const rgbOffset = 12 + y * stride + x * 2; + data[rgbOffset] = rgb565 & 0xff; + data[rgbOffset + 1] = rgb565 >> 8; + + if (hasTransparency) { + data[12 + rgbSize + y * alphaStride + x] = rgba[sourceOffset + 3]; + } + } + } + + return { data, colorFormat }; +} diff --git a/packages/hd-ble-sdk/src/index.ts b/packages/hd-ble-sdk/src/index.ts index 015ffef72..7949d32fc 100644 --- a/packages/hd-ble-sdk/src/index.ts +++ b/packages/hd-ble-sdk/src/index.ts @@ -50,6 +50,10 @@ const cancel = (connectId?: string) => { _core.handleMessage({ event: IFRAME.CANCEL, type: IFRAME.CANCEL, payload: { connectId } }); }; +const cancelOperation = (operationId: string) => { + _core?.cancelOperation(operationId); +}; + function handleMessage(message: CoreMessage) { const { event, type } = message; if (!_core) { @@ -179,6 +183,7 @@ const HardwareBleSdk = HardwareSdk({ init, call, cancel, + cancelOperation, dispose, uiResponse, updateSettings, diff --git a/packages/hd-common-connect-sdk/src/index.ts b/packages/hd-common-connect-sdk/src/index.ts index f7ff42713..40f90483d 100644 --- a/packages/hd-common-connect-sdk/src/index.ts +++ b/packages/hd-common-connect-sdk/src/index.ts @@ -13,6 +13,7 @@ import HardwareSdk, { createUiMessage, enableLog, executeCallback, + getLogBlockLabel, getLogger, initCore, parseConnectSettings, @@ -38,7 +39,9 @@ const eventEmitter = new EventEmitter(); const Log = getLogger(LoggerNames.HdCommonConnectSdk); const getTransport = async (env: ConnectSettings['env']) => { - if (env === 'desktop-web-ble') return ElectronBleTransport; + if (env === 'desktop-web-ble') { + return ElectronBleTransport; + } if (env === 'webusb' || env === 'desktop-webusb') return WebUsbTransport; if (env === 'lowlevel') return LowlevelTransport; if (env === 'node-usb') { @@ -76,14 +79,19 @@ const cancel = (connectId?: string) => { _core.handleMessage({ event: IFRAME.CANCEL, type: IFRAME.CANCEL, payload: { connectId } }); }; +const cancelOperation = (operationId: string) => { + _core?.cancelOperation(operationId); +}; + function handleMessage(message: CoreMessage) { const { event } = message; if (!_core) { return; } + const blockLog = getLogBlockLabel(message); if (event !== LOG_EVENT) { - Log.debug('hd-common-connect-sdk handleMessage', message); + Log.debug('hd-common-connect-sdk handleMessage', blockLog ?? message); } switch (event) { case UI_EVENT: @@ -157,7 +165,8 @@ const init = async ( }; const call = async (params: any) => { - Log.debug('call: ', params); + const blockLog = getLogBlockLabel(params); + Log.debug('call: ', blockLog ?? params); try { const response = await postMessage({ event: IFRAME.CALL, type: IFRAME.CALL, payload: params }); @@ -194,6 +203,7 @@ const HardwareCommonConnectSdk = HardwareSdk({ init, call, cancel, + cancelOperation, dispose, uiResponse, updateSettings, diff --git a/packages/hd-transport-electron/src/ble-ops.ts b/packages/hd-transport-electron/src/ble-ops.ts index 3205d38f4..fc42100e7 100644 --- a/packages/hd-transport-electron/src/ble-ops.ts +++ b/packages/hd-transport-electron/src/ble-ops.ts @@ -7,15 +7,7 @@ export interface SoftRefreshParams { subscriptionOperations: Map; subscribedDevices: Map; pairedDevices: Set; - notificationCallbacks: Map void>; - processNotificationData: ( - deviceId: string, - data: Buffer - ) => { - isComplete: boolean; - completePacket?: string; - error?: string; - }; + onNotificationData: (deviceId: string, data: Buffer) => void; logger: Logger | null; } @@ -26,8 +18,7 @@ export async function softRefreshSubscription(params: SoftRefreshParams): Promis subscriptionOperations, subscribedDevices, pairedDevices, - notificationCallbacks, - processNotificationData, + onNotificationData, logger, } = params; @@ -60,15 +51,7 @@ export async function softRefreshSubscription(params: SoftRefreshParams): Promis logger?.info('[BLE-OPS] Device paired successfully', { deviceId }); } - const result = processNotificationData(deviceId, data); - if (result.error) { - logger?.error('[BLE-OPS] Packet processing error:', result.error); - return; - } - if (result.isComplete && result.completePacket) { - const appCb = notificationCallbacks.get(deviceId); - if (appCb) appCb(result.completePacket); - } + onNotificationData(deviceId, data); }); subscribedDevices.set(deviceId, true); diff --git a/packages/hd-transport-electron/src/noble-ble-handler.ts b/packages/hd-transport-electron/src/noble-ble-handler.ts index 37226e77b..154c81f67 100644 --- a/packages/hd-transport-electron/src/noble-ble-handler.ts +++ b/packages/hd-transport-electron/src/noble-ble-handler.ts @@ -9,14 +9,10 @@ import { EOneKeyBleMessageKeys, ERRORS, HardwareErrorCode, - ONEKEY_NOTIFY_CHARACTERISTIC_UUID, ONEKEY_SERVICE_UUID, - ONEKEY_WRITE_CHARACTERISTIC_UUID, - isHeaderChunk, isOnekeyDevice, wait, } from '@onekeyfe/hd-shared'; -import { COMMON_HEADER_SIZE } from '@onekeyfe/hd-transport'; import pRetry from 'p-retry'; import { safeLog } from './types/noble-extended'; @@ -55,15 +51,6 @@ const subscribedDevices = new Map(); // Track subscription stat // 🔒 Subscription operation state tracking to prevent race conditions const subscriptionOperations = new Map(); -// Packet reassembly state for each device -interface PacketAssemblyState { - bufferLength: number; - buffer: number[]; - packetCount: number; - messageId?: string; // Add message ID to track concurrent requests -} -const devicePacketStates = new Map(); - // Windows-only response watchdog state moved to utils/windows-ble-recovery // Pairing-related state removed @@ -72,6 +59,7 @@ const devicePacketStates = new Map(); // Service UUIDs to scan for - using constants from hd-shared const ONEKEY_SERVICE_UUIDS = [ONEKEY_SERVICE_UUID]; +const PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS = new Set(['fffd']); // Pre-normalized characteristic identifiers for fast comparison const NORMALIZED_WRITE_UUID = '0002'; @@ -79,10 +67,10 @@ const NORMALIZED_NOTIFY_UUID = '0003'; // Timeout and interval constants const BLUETOOTH_INIT_TIMEOUT = 10000; // 10 seconds for Bluetooth initialization -const DEVICE_SCAN_TIMEOUT = 5000; // 5 seconds for device scanning -const FAST_SCAN_TIMEOUT = 1500; // 1.5 seconds for fast targeted scanning +const DEVICE_SCAN_TIMEOUT = 8000; // 8 seconds for device scanning (Pro2 has longer advertising interval) +const FAST_SCAN_TIMEOUT = 8000; // 8 seconds for targeted scanning (Pro2 has longer advertising interval) const DEVICE_CHECK_INTERVAL = 500; // 500ms interval for periodic device checks -const CONNECTION_TIMEOUT = 3000; // 3 seconds for device connection +const CONNECTION_TIMEOUT = 8000; // 8 seconds for device connection (BLE reconnect after release can be slow) const SERVICE_DISCOVERY_TIMEOUT = 10000; // 10 seconds for service discovery // Write-related constants @@ -94,101 +82,43 @@ const ABORTABLE_WRITE_ERROR_PATTERNS = [ /status:\s*3/i, // Windows pairing cancelled / GATT write failed ]; -// Validation limits -const MIN_HEADER_LENGTH = 9; // Minimum header chunk length - -// Packet processing result types -interface PacketProcessResult { - isComplete: boolean; - completePacket?: string; - error?: string; +function getBleUuidKey(uuid?: string | null) { + const normalized = (uuid ?? '').replace(/-/g, '').toLowerCase(); + return normalized.length >= 8 ? normalized.substring(4, 8) : normalized; } -// Process incoming BLE notification data with proper packet reassembly -function processNotificationData(deviceId: string, data: Buffer): PacketProcessResult { - // notification telemetry - logger?.info('[NobleBLE] Notification', { - deviceId, - dataLength: data.length, - }); - - // Get or initialize packet state for this device - let packetState = devicePacketStates.get(deviceId); - if (!packetState) { - packetState = { bufferLength: 0, buffer: [], packetCount: 0 }; - devicePacketStates.set(deviceId, packetState); - logger?.info('[NobleBLE] Initialized new packet state for device:', deviceId); - } +const NORMALIZED_ONEKEY_SERVICE_UUIDS = new Set([ + ...ONEKEY_SERVICE_UUIDS.map(uuid => getBleUuidKey(uuid)), + '0001', +]); - try { - if (isHeaderChunk(data)) { - // Validate header chunk - if (data.length < MIN_HEADER_LENGTH) { - return { isComplete: false, error: 'Invalid header chunk: too short' }; - } - - // Generate message ID for this packet sequence - const messageId = `${deviceId}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; - - // Reset packet state for new message - packetState.bufferLength = data.readInt32BE(5); - packetState.buffer = [...data.subarray(3)]; - packetState.packetCount = 1; - packetState.messageId = messageId; - - // Only validate for negative lengths (which would be invalid) - if (packetState.bufferLength < 0) { - logger?.error('[NobleBLE] Invalid negative packet length detected:', { - length: packetState.bufferLength, - dataLength: data.length, - rawHeader: data.subarray(0, Math.min(16, data.length)).toString('hex'), - lengthBytes: data.subarray(5, 9).toString('hex'), - }); - resetPacketState(packetState); - return { isComplete: false, error: 'Invalid packet length in header' }; - } - } else { - // Validate we have an active packet session - if (packetState.bufferLength === 0) { - return { isComplete: false, error: 'Received data chunk without header' }; - } - - // Increment packet counter and append data - packetState.packetCount += 1; - packetState.buffer = packetState.buffer.concat([...data]); - } - - // Check if packet is complete - if (packetState.buffer.length - COMMON_HEADER_SIZE >= packetState.bufferLength) { - const completeBuffer = Buffer.from(packetState.buffer); - const hexString = completeBuffer.toString('hex'); - - logger?.info('[NobleBLE] Packet assembled', { - deviceId, - totalPackets: packetState.packetCount, - expectedLength: packetState.bufferLength, - actualLength: packetState.buffer.length - COMMON_HEADER_SIZE, - }); - - // Reset packet state for next message - resetPacketState(packetState); +function isGenericBleService(uuid?: string | null) { + return ['1800', '1801', '180a', '180f'].includes(getBleUuidKey(uuid)); +} - return { isComplete: true, completePacket: hexString }; - } +function hasOneKeyAdvertisementService(peripheral: Peripheral) { + const serviceUuids = peripheral.advertisement?.serviceUuids ?? []; + return serviceUuids.some(uuid => { + const uuidKey = getBleUuidKey(uuid); + return ( + NORMALIZED_ONEKEY_SERVICE_UUIDS.has(uuidKey) || + PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(uuidKey) + ); + }); +} - return { isComplete: false }; - } catch (error) { - resetPacketState(packetState); - return { isComplete: false, error: `Packet processing error: ${error}` }; - } +function isOneKeyPeripheral(peripheral: Peripheral) { + const deviceName = peripheral.advertisement?.localName || null; + return isOnekeyDevice(deviceName, peripheral.id) || hasOneKeyAdvertisementService(peripheral); } -// Reset packet state to clean state -function resetPacketState(packetState: PacketAssemblyState): void { - packetState.bufferLength = 0; - packetState.buffer = []; - packetState.packetCount = 0; - packetState.messageId = undefined; +/** + * Forward a single BLE notification chunk (not an assembled packet) to the + * renderer-side transport. Packet reassembly is handled by ElectronBleTransport. + */ +function emitRawNotification(deviceId: string, data: Buffer): void { + const appCb = notificationCallbacks.get(deviceId); + if (appCb) appCb(data.toString('hex')); } // Check Bluetooth availability - returns detailed state @@ -250,7 +180,6 @@ function setupPersistentStateListener(): void { deviceCharacteristics.clear(); subscribedDevices.clear(); notificationCallbacks.clear(); - devicePacketStates.clear(); subscriptionOperations.clear(); pairedDevices.clear(); @@ -428,7 +357,6 @@ function cleanupDevice( connectedDevices.delete(deviceId); deviceCharacteristics.delete(deviceId); notificationCallbacks.delete(deviceId); - devicePacketStates.delete(deviceId); subscribedDevices.delete(deviceId); subscriptionOperations.delete(deviceId); pairedDevices.delete(deviceId); @@ -590,8 +518,7 @@ async function attemptWindowsWriteUntilPaired( subscriptionOperations, subscribedDevices, pairedDevices, - notificationCallbacks, - processNotificationData, + onNotificationData: emitRawNotification, logger, }); logger?.info('[BLE-Write] Subscription refresh completed', { deviceId }); @@ -618,12 +545,6 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi } const toBuffer = Buffer.from(hexData, 'hex'); - logger?.info('[NobleBLE] Writing data:', { - deviceId, - dataLength: toBuffer.length, - firstBytes: toBuffer.subarray(0, 8).toString('hex'), - }); - const doGetWriteCharacteristic = () => deviceCharacteristics.get(deviceId)?.write; if (!IS_WINDOWS || pairedDevices.has(deviceId)) { @@ -685,15 +606,21 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi // Handle discovered device (for general enumeration only) function handleDeviceDiscovered(peripheral: Peripheral): void { - const deviceName = peripheral.advertisement?.localName || 'Unknown Device'; - - // Only process OneKey devices for general discovery - if (!isOnekeyDevice(deviceName)) { + // Only process OneKey candidates for general discovery. Avoid logging every + // ambient BLE peripheral; it makes Pro2 debugging hard to read. + if (!isOneKeyPeripheral(peripheral)) { return; } - logger?.info('[NobleBLE] Discovered OneKey device:', deviceName); + const isNewDevice = !discoveredDevices.has(peripheral.id); discoveredDevices.set(peripheral.id, peripheral); + if (isNewDevice) { + logger?.debug('[NobleBLE] OneKey BLE device discovered', { + deviceId: peripheral.id, + name: peripheral.advertisement?.localName || 'Unknown Device', + serviceUUIDs: peripheral.advertisement?.serviceUuids || [], + }); + } } // Ensure discover listener is properly set up @@ -753,8 +680,8 @@ async function performTargetedScan(targetDeviceId: string): Promise { + // Start scanning — no service UUID filter (Pro2 may use different service UUID) + nobleInstance.startScanning([], false, (error?: Error) => { if (error) { clearTimeout(timeoutId); nobleInstance.removeListener('discover', onDiscover); @@ -816,15 +743,19 @@ async function enumerateDevices(): Promise { }); }; - // Set timeout for scanning + // Set timeout for scanning — use longer timeout to catch slow-advertising devices like Pro2 const timeoutId = setTimeout(() => { + // Final collection before resolving — catches devices discovered near the deadline + checkDevices(); cleanup(); logger?.info('[NobleBLE] Scan completed, found devices:', devices.length); resolve(devices); }, DEVICE_SCAN_TIMEOUT); - // Start scanning for OneKey service UUIDs - nobleInstance.startScanning(ONEKEY_SERVICE_UUIDS, false, (error?: Error) => { + // Start scanning without a service UUID filter so Pro2 advertisements with + // short vendor UUIDs can be found, but only OneKey candidates are logged/returned. + logger?.info('[NobleBLE] Scanning for OneKey BLE devices'); + nobleInstance.startScanning([], false, (error?: Error) => { if (error) { cleanup(); logger?.error('[NobleBLE] Failed to start scanning:', error); @@ -952,9 +883,9 @@ async function discoverServicesAndCharacteristics( // Main discovery logic as async function const discoveryPromise = (async (): Promise => { - // Step 1: Discover services (promisified) + // Step 1: Discover ALL services (no filter — Pro2 may use different service UUID) const services = await new Promise((resolve, reject) => { - peripheral.discoverServices(ONEKEY_SERVICE_UUIDS, (error, svc) => { + peripheral.discoverServices([], (error, svc) => { if (error) { logger?.error('[NobleBLE] Service discovery failed:', error); reject(ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, error.message)); @@ -965,33 +896,44 @@ async function discoverServicesAndCharacteristics( }); if (!services || services.length === 0) { - throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No OneKey services found'); + throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No services found'); } - const service = services[0]; - logger?.info('[NobleBLE] Found service:', service.uuid); + logger?.debug('[NobleBLE] services discovered', { + deviceId: peripheral.id, + serviceUUIDs: services.map(service => service.uuid), + }); - // Step 2: Discover characteristics (promisified) + // Find OneKey service — Noble may expose 128-bit UUIDs as short UUID keys. + let service = services.find(s => NORMALIZED_ONEKEY_SERVICE_UUIDS.has(getBleUuidKey(s.uuid))); + if (!service) { + logger?.info('[NobleBLE] Known OneKey service UUID not found, trying first vendor service'); + service = + services.find(s => PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(getBleUuidKey(s.uuid))) || + services.find(s => !isGenericBleService(s.uuid)) || + services[0]; + } + if (!service) { + throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound); + } + const selectedService = service; + logger?.debug('[NobleBLE] service selected', { + deviceId: peripheral.id, + serviceUuid: selectedService.uuid, + }); + // Step 2: Discover ALL characteristics (no filter) const characteristics = await new Promise((resolve, reject) => { - service.discoverCharacteristics( - [ONEKEY_WRITE_CHARACTERISTIC_UUID, ONEKEY_NOTIFY_CHARACTERISTIC_UUID], - (error, chars) => { - if (error) { - logger?.error('[NobleBLE] Characteristic discovery failed:', error); - reject(ERRORS.TypedError(HardwareErrorCode.BleCharacteristicNotFound, error.message)); - } else { - resolve(chars); - } + selectedService.discoverCharacteristics([], (error, chars) => { + if (error) { + logger?.error('[NobleBLE] Characteristic discovery failed:', error); + reject(ERRORS.TypedError(HardwareErrorCode.BleCharacteristicNotFound, error.message)); + } else { + resolve(chars); } - ); + }); }); // Step 3: Find required characteristics - logger?.info('[NobleBLE] Discovered characteristics:', { - count: characteristics?.length || 0, - uuids: characteristics?.map(c => c.uuid) || [], - }); - let writeCharacteristic: Characteristic | null = null; let notifyCharacteristic: Characteristic | null = null; @@ -1006,11 +948,6 @@ async function discoverServicesAndCharacteristics( } } - logger?.info('[NobleBLE] Characteristic discovery result:', { - writeFound: !!writeCharacteristic, - notifyFound: !!notifyCharacteristic, - }); - if (!writeCharacteristic || !notifyCharacteristic) { logger?.error( '[NobleBLE] Missing characteristics - write:', @@ -1024,6 +961,13 @@ async function discoverServicesAndCharacteristics( ); } + logger?.debug('[NobleBLE] characteristics selected', { + deviceId: peripheral.id, + serviceUuid: selectedService.uuid, + writeUuid: writeCharacteristic.uuid, + notifyUuid: notifyCharacteristic.uuid, + }); + return { write: writeCharacteristic, notify: notifyCharacteristic }; })(); @@ -1320,7 +1264,6 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis existingCharacteristics.notify.removeAllListeners('data'); } notificationCallbacks.delete(deviceId); - devicePacketStates.delete(deviceId); subscribedDevices.delete(deviceId); // Continue to re-setup the connection properly } @@ -1431,7 +1374,6 @@ async function unsubscribeNotifications(deviceId: string): Promise { // Remove all listeners and clear subscription status notifyCharacteristic.removeAllListeners('data'); notificationCallbacks.delete(deviceId); - devicePacketStates.delete(deviceId); subscribedDevices.delete(deviceId); } finally { // 🔒 CRITICAL: Always clear operation state (even on error) @@ -1498,14 +1440,6 @@ async function subscribeNotifications( // Just update the callback without re-subscribing notificationCallbacks.set(deviceId, callback); - // Reset packet state for new session - devicePacketStates.set(deviceId, { - bufferLength: 0, - buffer: [], - packetCount: 0, - messageId: undefined, - }); - // 🔒 Clear operation state subscriptionOperations.set(deviceId, 'idle'); return Promise.resolve(); @@ -1522,14 +1456,6 @@ async function subscribeNotifications( // Store callback for this device notificationCallbacks.set(deviceId, callback); - // Reset packet state for new subscription session - devicePacketStates.set(deviceId, { - bufferLength: 0, - buffer: [], - packetCount: 0, - messageId: undefined, - }); - // Helper: rebuild a clean application-layer subscription async function rebuildAppSubscription( deviceId: string, @@ -1558,15 +1484,7 @@ async function subscribeNotifications( logger?.info('[NobleBLE] Device paired successfully', { deviceId }); } - const result = processNotificationData(deviceId, data); - if (result.error) { - logger?.error('[NobleBLE] Packet processing error:', result.error); - return; - } - if (result.isComplete && result.completePacket) { - const appCb = notificationCallbacks.get(deviceId); - if (appCb) appCb(result.completePacket); - } + emitRawNotification(deviceId, data); }); } @@ -1581,29 +1499,25 @@ async function subscribeNotifications( // Setup IPC handlers export function setupNobleBleHandlers(webContents: WebContents): void { - // Use console.log for initial logging as electron-log might not be available yet. - console.log('[NobleBLE] Attempting to set up Noble BLE handlers.'); try { - console.log('[NobleBLE] NOBLE_VERSION_771'); - // @ts-ignore – electron-log is only available at runtime // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require logger = require('electron-log') as Logger; - console.log('[NobleBLE] electron-log loaded successfully.'); // @ts-ignore – electron is only available at runtime // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require const { ipcMain } = require('electron'); - console.log('[NobleBLE] electron.ipcMain loaded successfully.'); safeLog(logger, 'info', 'Setting up Noble BLE IPC handlers'); // Handle enumerate request - console.log(`[NobleBLE] Registering handler for: ${EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE}`); ipcMain.handle(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE, async () => { try { const devices = await enumerateDevices(); - safeLog(logger, 'info', 'Enumeration completed, devices:', devices); + safeLog(logger, 'debug', 'Enumeration completed', { + count: devices.length, + devices: devices.map(device => ({ id: device.id, name: device.name })), + }); return devices; } catch (error) { safeLog(logger, 'error', 'Enumeration failed:', error); @@ -1648,7 +1562,6 @@ export function setupNobleBleHandlers(webContents: WebContents): void { ipcMain.handle( EOneKeyBleMessageKeys.NOBLE_BLE_WRITE, async (_event: IpcMainInvokeEvent, deviceId: string, hexData: string) => { - logger?.info('[NobleBLE] IPC WRITE', { deviceId, len: hexData.length }); await transmitHexDataToDevice(deviceId, hexData); } ); diff --git a/packages/hd-transport-emulator/src/http.ts b/packages/hd-transport-emulator/src/http.ts index 612359dfb..8443881b1 100644 --- a/packages/hd-transport-emulator/src/http.ts +++ b/packages/hd-transport-emulator/src/http.ts @@ -75,7 +75,6 @@ axios.interceptors.request.use((config: InternalAxiosRequestConfig) => { // node environment if (config.url?.startsWith('http://localhost:21333')) { if (!config.headers.get('Origin')) { - console.log('set node request origin'); // add Origin field for request headers config.headers.set('Origin', 'https://jssdk.onekey.so'); } diff --git a/packages/hd-transport-emulator/src/index.ts b/packages/hd-transport-emulator/src/index.ts index cda7073d2..85cc3efd9 100644 --- a/packages/hd-transport-emulator/src/index.ts +++ b/packages/hd-transport-emulator/src/index.ts @@ -1,12 +1,16 @@ -import transport, { LogBlockCommand } from '@onekeyfe/hd-transport'; +import transport from '@onekeyfe/hd-transport'; import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared'; import { request as http } from './http'; import { DEFAULT_URL } from './constants'; -import type { AcquireInput, OneKeyDeviceInfoWithSession } from '@onekeyfe/hd-transport'; +import type { + AcquireInput, + OneKeyDeviceInfoWithSession, + ProtocolType, +} from '@onekeyfe/hd-transport'; -const { check, buildOne, receiveOne, parseConfigure } = transport; +const { check, ProtocolV1, parseConfigure } = transport; type IncompleteRequestOptions = { body?: Array | Record | string; @@ -27,6 +31,11 @@ export default class EmulatorTransport { isOutdated = false; + // EmulatorTransport speaks Protocol V1 only. + getProtocolType(_path: string): ProtocolType { + return 'V1'; + } + url: string; Log?: any; @@ -115,13 +124,9 @@ export default class EmulatorTransport { throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); } const messages = this._messages; - if (LogBlockCommand.has(name)) { - this.Log.debug('call-', ' name: ', name); - } else { - this.Log.debug('call-', ' name: ', name, ' data: ', data); - } + this.Log.debug('transport call', { name, protocol: 'V1' }); - const o = buildOne(messages, name, data); + const o = ProtocolV1.encodeEnvelope(messages, name, data); const outData = o.toString('hex'); const resData = await this._post({ url: `/call/${session}`, @@ -131,7 +136,7 @@ export default class EmulatorTransport { if (typeof resData !== 'string') { throw ERRORS.TypedError(HardwareErrorCode.NetworkError, 'Returning data is not string.'); } - const jsonData = receiveOne(messages, resData); + const jsonData = ProtocolV1.decodeMessage(messages, resData); return check.call(jsonData); } @@ -140,7 +145,7 @@ export default class EmulatorTransport { throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); } const messages = this._messages; - const outData = buildOne(messages, name, data).toString('hex'); + const outData = ProtocolV1.encodeEnvelope(messages, name, data).toString('hex'); await this._post({ url: `/post/${session}`, body: outData, @@ -158,7 +163,7 @@ export default class EmulatorTransport { if (typeof resData !== 'string') { throw ERRORS.TypedError(HardwareErrorCode.NetworkError, 'Returning data is not string.'); } - const jsonData = receiveOne(messages, resData); + const jsonData = ProtocolV1.decodeMessage(messages, resData); return check.call(jsonData); } diff --git a/packages/hd-transport-http/src/index.ts b/packages/hd-transport-http/src/index.ts index c51caba7f..c95003843 100644 --- a/packages/hd-transport-http/src/index.ts +++ b/packages/hd-transport-http/src/index.ts @@ -1,12 +1,16 @@ -import transport, { LogBlockCommand } from '@onekeyfe/hd-transport'; +import transport from '@onekeyfe/hd-transport'; import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared'; import { request as http } from './http'; import { DEFAULT_URL } from './constants'; -import type { AcquireInput, OneKeyDeviceInfoWithSession } from '@onekeyfe/hd-transport'; +import type { + AcquireInput, + OneKeyDeviceInfoWithSession, + ProtocolType, +} from '@onekeyfe/hd-transport'; -const { check, buildOne, receiveOne, parseConfigure } = transport; +const { check, ProtocolV1, parseConfigure } = transport; type IncompleteRequestOptions = { body?: Array | Record | string; @@ -27,6 +31,11 @@ export default class HttpTransport { Log?: any; + // HttpTransport (Bridge) speaks Protocol V1 only. + getProtocolType(_path: string): ProtocolType { + return 'V1'; + } + constructor(url?: string) { this.url = url == null ? DEFAULT_URL : url; } @@ -111,13 +120,9 @@ export default class HttpTransport { throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); } const messages = this._messages; - if (LogBlockCommand.has(name)) { - this.Log.debug('call-', ' name: ', name); - } else { - this.Log.debug('call-', ' name: ', name, ' data: ', data); - } + this.Log.debug('transport call', { name, protocol: 'V1' }); - const o = buildOne(messages, name, data); + const o = ProtocolV1.encodeEnvelope(messages, name, data); const outData = o.toString('hex'); const resData = await this._post({ url: `/call/${session}`, @@ -127,7 +132,7 @@ export default class HttpTransport { if (typeof resData !== 'string') { throw ERRORS.TypedError(HardwareErrorCode.NetworkError, 'Returning data is not string.'); } - const jsonData = receiveOne(messages, resData); + const jsonData = ProtocolV1.decodeMessage(messages, resData); return check.call(jsonData); } @@ -136,7 +141,7 @@ export default class HttpTransport { throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); } const messages = this._messages; - const outData = buildOne(messages, name, data).toString('hex'); + const outData = ProtocolV1.encodeEnvelope(messages, name, data).toString('hex'); await this._post({ url: `/post/${session}`, body: outData, @@ -154,7 +159,7 @@ export default class HttpTransport { if (typeof resData !== 'string') { throw ERRORS.TypedError(HardwareErrorCode.NetworkError, 'Returning data is not string.'); } - const jsonData = receiveOne(messages, resData); + const jsonData = ProtocolV1.decodeMessage(messages, resData); return check.call(jsonData); } diff --git a/packages/hd-transport-lowlevel/.eslintignore b/packages/hd-transport-lowlevel/.eslintignore new file mode 100644 index 000000000..00d1a98c8 --- /dev/null +++ b/packages/hd-transport-lowlevel/.eslintignore @@ -0,0 +1,4 @@ +coverage/ +dist/ +__tests__/ +jest.config.js diff --git a/packages/hd-transport-lowlevel/__tests__/protocol-v2.test.js b/packages/hd-transport-lowlevel/__tests__/protocol-v2.test.js new file mode 100644 index 000000000..d23c99ff3 --- /dev/null +++ b/packages/hd-transport-lowlevel/__tests__/protocol-v2.test.js @@ -0,0 +1,388 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const LowlevelTransport = require('../src').default; +const { parseConfigure } = require('../../hd-transport/src/serialization/protobuf/messages'); +const { ProtocolV1, ProtocolV2 } = require('../../hd-transport/src/protocols'); +const { bytesToHex } = require('../../hd-transport/src/protocols/v2/session'); +const { PROTOCOL_V2_CHANNEL_BLE_UART } = require('../../hd-transport/src/constants'); + +const protocolV1Schema = { + nested: { + Initialize: { + fields: {}, + }, + Success: { + fields: { + message: { + type: 'string', + id: 1, + }, + }, + }, + MessageType: { + values: { + MessageType_Initialize: 1, + MessageType_Success: 2, + }, + }, + }, +}; + +const protocolV2Schema = { + nested: { + ProtocolInfoRequest: { + fields: {}, + }, + ProtocolInfo: { + fields: { + version: { + type: 'uint32', + id: 1, + }, + supported_messages: { + rule: 'repeated', + type: 'uint32', + id: 2, + options: { + packed: false, + }, + }, + protobuf_definition: { + type: 'string', + id: 3, + }, + }, + }, + Ping: { + fields: { + message: { + type: 'string', + id: 1, + }, + }, + }, + Success: { + fields: { + message: { + type: 'string', + id: 1, + }, + }, + }, + MessageType: { + values: { + MessageType_ProtocolInfoRequest: 60200, + MessageType_ProtocolInfo: 60201, + MessageType_Ping: 60206, + MessageType_Success: 60207, + }, + }, + }, +}; + +const schemas = { + protocolV1: parseConfigure(protocolV1Schema), + protocolV2: parseConfigure(protocolV2Schema), +}; + +const createLogger = () => ({ + debug: jest.fn(), + error: jest.fn(), +}); + +const createPlugin = ({ devices, responses }) => ({ + enumerate: jest.fn(() => Promise.resolve(devices)), + connect: jest.fn(() => Promise.resolve()), + disconnect: jest.fn(() => Promise.resolve()), + init: jest.fn(() => Promise.resolve()), + send: jest.fn(() => Promise.resolve()), + receive: jest.fn(() => { + const next = responses.shift(); + if (next instanceof Error) { + return Promise.reject(next); + } + if (!next) { + return Promise.reject(new Error('No queued response')); + } + return Promise.resolve(next); + }), + version: 'test-plugin', +}); + +const configureTransport = plugin => { + const lowlevel = new LowlevelTransport(); + lowlevel.init(createLogger(), undefined, plugin); + lowlevel.configure(protocolV1Schema); + lowlevel.configureProtocolV2(protocolV2Schema); + return lowlevel; +}; + +const splitFrame = (frame, index) => [ + bytesToHex(frame.slice(0, index)), + bytesToHex(frame.slice(index)), +]; + +describe('LowlevelTransport protocol framing', () => { + test('keeps Protocol V1 raw notification chunks compatible', async () => { + const responseChunks = ProtocolV1.encodeTransportPackets(schemas.protocolV1, 'Success', { + message: 'ok', + }).map(chunk => chunk.toString('hex')); + const plugin = createPlugin({ + devices: [{ id: 'classic-id', name: 'OneKey Classic', commType: 'ble' }], + responses: [...responseChunks, ...responseChunks], + }); + const lowlevel = configureTransport(plugin); + + await expect(lowlevel.acquire({ uuid: 'classic-id' })).resolves.toEqual({ + uuid: 'classic-id', + protocolType: 'V1', + }); + await expect(lowlevel.call('classic-id', 'Initialize', {})).resolves.toEqual({ + type: 'Success', + message: { message: 'ok' }, + }); + }); + + test('rejects calls before protocol detection', async () => { + const responseChunks = ProtocolV1.encodeTransportPackets(schemas.protocolV1, 'Success', { + message: 'ok', + }).map(chunk => chunk.toString('hex')); + const plugin = createPlugin({ + devices: [{ id: 'classic-id', name: 'OneKey Classic', commType: 'ble' }], + responses: responseChunks, + }); + const lowlevel = configureTransport(plugin); + + await expect(lowlevel.call('classic-id', 'Initialize', {})).rejects.toThrow( + 'Device protocol has not been detected' + ); + }); + + test('detects Protocol V2 devices and reassembles split Protocol V2 notifications', async () => { + const probeResponse = ProtocolV2.encodeFrame( + schemas, + 'Success', + { message: 'ok' }, + { router: PROTOCOL_V2_CHANNEL_BLE_UART } + ); + const callResponse = ProtocolV2.encodeFrame( + schemas, + 'ProtocolInfo', + { + version: 1, + supported_messages: [60200, 60201, 60206, 60207], + protobuf_definition: 'onekey-protocol-v2', + }, + { router: PROTOCOL_V2_CHANNEL_BLE_UART } + ); + const plugin = createPlugin({ + devices: [{ id: 'pro2-id', name: 'OneKey Pro 2', commType: 'ble' }], + responses: [...splitFrame(probeResponse, 4), ...splitFrame(callResponse, 5)], + }); + const lowlevel = configureTransport(plugin); + + await expect(lowlevel.enumerate()).resolves.toEqual([ + { id: 'pro2-id', name: 'OneKey Pro 2', commType: 'ble' }, + ]); + await expect(lowlevel.acquire({ uuid: 'pro2-id' })).resolves.toEqual({ + uuid: 'pro2-id', + protocolType: 'V2', + }); + await expect(lowlevel.call('pro2-id', 'ProtocolInfoRequest', {})).resolves.toEqual({ + type: 'ProtocolInfo', + message: { + version: 1, + supported_messages: [60200, 60201, 60206, 60207], + protobuf_definition: 'onekey-protocol-v2', + }, + }); + expect(plugin.send).toHaveBeenCalled(); + const sentSeqs = plugin.send.mock.calls.map(([, hex]) => + Number.parseInt(hex.slice(12, 14), 16) + ); + expect(sentSeqs).toEqual([1, 2]); + }); + + test('falls back to Protocol V2 probe for unnamed Protocol V2 devices', async () => { + const probeResponse = ProtocolV2.encodeFrame( + schemas, + 'Success', + { message: 'ok' }, + { router: PROTOCOL_V2_CHANNEL_BLE_UART } + ); + const plugin = createPlugin({ + devices: [{ id: 'unknown-pro2-id', name: 'Unknown BLE Device', commType: 'ble' }], + responses: [new Error('Protocol V1 probe timed out'), bytesToHex(probeResponse)], + }); + const lowlevel = configureTransport(plugin); + + await expect(lowlevel.acquire({ uuid: 'unknown-pro2-id' })).resolves.toEqual({ + uuid: 'unknown-pro2-id', + protocolType: 'V2', + }); + expect(lowlevel.getProtocolType('unknown-pro2-id')).toBe('V2'); + }); + + test('retains the Protocol V2 hint and sequence cursor across release and reacquire', async () => { + const probeResponse = ProtocolV2.encodeFrame( + schemas, + 'Success', + { message: 'ok' }, + { router: PROTOCOL_V2_CHANNEL_BLE_UART } + ); + const plugin = createPlugin({ + devices: [{ id: 'reconnect-pro2-id', name: 'OneKey Pro 2', commType: 'ble' }], + responses: [bytesToHex(probeResponse), bytesToHex(probeResponse)], + }); + const lowlevel = configureTransport(plugin); + + await lowlevel.enumerate(); + await expect(lowlevel.acquire({ uuid: 'reconnect-pro2-id' })).resolves.toEqual({ + uuid: 'reconnect-pro2-id', + protocolType: 'V2', + }); + await lowlevel.release('reconnect-pro2-id'); + await expect(lowlevel.acquire({ uuid: 'reconnect-pro2-id' })).resolves.toEqual({ + uuid: 'reconnect-pro2-id', + protocolType: 'V2', + }); + + const sentSeqs = plugin.send.mock.calls.map(([, hex]) => + Number.parseInt(hex.slice(12, 14), 16) + ); + expect(sentSeqs).toEqual([1, 2]); + }); + + test('reuses the active generation when Core acquires the same BLE connection again', async () => { + const probeResponse = ProtocolV2.encodeFrame( + schemas, + 'Success', + { message: 'ok' }, + { router: PROTOCOL_V2_CHANNEL_BLE_UART } + ); + const plugin = createPlugin({ + devices: [{ id: 'repeated-acquire-id', name: 'OneKey Pro 2', commType: 'ble' }], + responses: [bytesToHex(probeResponse), bytesToHex(probeResponse)], + }); + const lowlevel = configureTransport(plugin); + + await expect( + lowlevel.acquire({ uuid: 'repeated-acquire-id', expectedProtocol: 'V2' }) + ).resolves.toEqual({ + uuid: 'repeated-acquire-id', + protocolType: 'V2', + }); + await lowlevel.call('repeated-acquire-id', 'Ping', { message: 'first-acquire' }); + await expect( + lowlevel.acquire({ uuid: 'repeated-acquire-id', expectedProtocol: 'V2' }) + ).resolves.toEqual({ + uuid: 'repeated-acquire-id', + protocolType: 'V2', + }); + await lowlevel.call('repeated-acquire-id', 'Ping', { message: 'second-acquire' }); + + const sentSeqs = plugin.send.mock.calls.map(([, hex]) => + Number.parseInt(hex.slice(12, 14), 16) + ); + expect(sentSeqs).toEqual([1, 2]); + }); + + test('trusts explicit Protocol V2 during bootloader reconnect without probing Ping', async () => { + const plugin = createPlugin({ + devices: [{ id: 'bootloader-v2-id', name: 'OneKey Pro 2', commType: 'ble' }], + responses: [], + }); + const lowlevel = configureTransport(plugin); + + await expect( + lowlevel.acquire({ uuid: 'bootloader-v2-id', expectedProtocol: 'V2' }) + ).resolves.toEqual({ + uuid: 'bootloader-v2-id', + protocolType: 'V2', + }); + expect(plugin.send).not.toHaveBeenCalled(); + expect(plugin.receive).not.toHaveBeenCalled(); + }); + + test('resets the lowlevel connection before probing Protocol V2 after a V1 timeout', async () => { + const probeResponse = ProtocolV2.encodeFrame( + schemas, + 'Success', + { message: 'ok' }, + { router: PROTOCOL_V2_CHANNEL_BLE_UART } + ); + let staleReceivePending = false; + let resetAfterTimeout = false; + const plugin = createPlugin({ + devices: [{ id: 'slow-v2-id', name: 'Unknown BLE Device', commType: 'ble' }], + responses: [], + }); + plugin.disconnect.mockImplementation(() => { + staleReceivePending = false; + resetAfterTimeout = true; + return Promise.resolve(); + }); + plugin.receive.mockImplementation(() => { + if (!staleReceivePending && !resetAfterTimeout) { + staleReceivePending = true; + return new Promise(() => {}); + } + if (staleReceivePending) { + return Promise.reject(new Error('stale receive still pending')); + } + return Promise.resolve(bytesToHex(probeResponse)); + }); + const lowlevel = configureTransport(plugin); + + await expect(lowlevel.acquire({ uuid: 'slow-v2-id' })).resolves.toEqual({ + uuid: 'slow-v2-id', + protocolType: 'V2', + }); + expect(plugin.disconnect).toHaveBeenCalledWith('slow-v2-id'); + expect(plugin.connect).toHaveBeenCalledTimes(2); + }); + + test('disconnects a tainted Protocol V2 link after a response timeout', async () => { + const plugin = createPlugin({ + devices: [{ id: 'timeout-v2-id', name: 'OneKey Pro 2', commType: 'ble' }], + responses: [], + }); + plugin.receive.mockImplementation(() => new Promise(() => {})); + const lowlevel = configureTransport(plugin); + + await lowlevel.acquire({ uuid: 'timeout-v2-id', expectedProtocol: 'V2' }); + plugin.disconnect.mockClear(); + await expect( + lowlevel.call('timeout-v2-id', 'Ping', { message: 'timeout' }, { timeoutMs: 10 }) + ).rejects.toThrow('Lowlevel response timeout after 10ms for Ping'); + + expect(plugin.disconnect).toHaveBeenCalledWith('timeout-v2-id'); + }); + + test('verifies expected Protocol V1 instead of trusting the requested protocol', async () => { + const plugin = createPlugin({ + devices: [{ id: 'v2-id', name: 'Unknown BLE Device', commType: 'ble' }], + responses: [new Error('Protocol V1 probe timed out')], + }); + const lowlevel = configureTransport(plugin); + + await expect(lowlevel.acquire({ uuid: 'v2-id', expectedProtocol: 'V1' })).rejects.toThrow( + 'Device protocol mismatch: expected V1' + ); + }); + + test('rejects automatic detection instead of caching V1 when both protocol probes fail', async () => { + const plugin = createPlugin({ + devices: [{ id: 'flaky-pro2-id', name: 'Unknown BLE Device', commType: 'ble' }], + responses: [ + new Error('Protocol V1 probe timed out'), + new Error('Protocol V2 probe timed out'), + ], + }); + const lowlevel = configureTransport(plugin); + + await expect(lowlevel.acquire({ uuid: 'flaky-pro2-id' })).rejects.toThrow( + 'Unable to detect BLE protocol' + ); + expect(lowlevel.getProtocolType('flaky-pro2-id')).toBeUndefined(); + }); +}); diff --git a/packages/hd-transport-lowlevel/jest.config.js b/packages/hd-transport-lowlevel/jest.config.js new file mode 100644 index 000000000..4f08d66df --- /dev/null +++ b/packages/hd-transport-lowlevel/jest.config.js @@ -0,0 +1,6 @@ +module.exports = { + preset: '../../jest.config.js', + testEnvironment: 'node', + modulePathIgnorePatterns: ['node_modules', '/lib', '/libDev'], + collectCoverage: true, +}; diff --git a/packages/hd-transport-lowlevel/src/index.ts b/packages/hd-transport-lowlevel/src/index.ts index 6588cadc8..9b4fa47e7 100644 --- a/packages/hd-transport-lowlevel/src/index.ts +++ b/packages/hd-transport-lowlevel/src/index.ts @@ -1,15 +1,50 @@ import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared'; -import transport, { LogBlockCommand } from '@onekeyfe/hd-transport'; +import transport, { + PROTOCOL_V1_MESSAGE_HEADER_SIZE, + PROTOCOL_V2_BLE_FRAME_MAX_BYTES, + PROTOCOL_V2_CHANNEL_BLE_UART, + ProtocolV2FrameAssembler, + ProtocolV2LinkManager, + bytesToHex, + concatUint8Arrays, + hexToBytes, + probeProtocolV2 as probeProtocolV2Helper, + withProtocolTimeout, +} from '@onekeyfe/hd-transport'; import type EventEmitter from 'events'; -import type { LowlevelTransportSharedPlugin } from '@onekeyfe/hd-transport'; +import type { + LowLevelDevice, + LowlevelTransportSharedPlugin, + ProtocolType, + TransportCallOptions, +} from '@onekeyfe/hd-transport'; import type { LowLevelAcquireInput } from './types'; -const { check, buildBuffers, receiveOne, parseConfigure } = transport; +const { check, ProtocolV1, parseConfigure } = transport; + +const PROTOCOL_PROBE_TIMEOUT_MS = 1000; +const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000; +const LOWLEVEL_PROTOCOL_TIMEOUT_MS = 30_000; +const LOWLEVEL_PROTOCOL_V2_PACKET_LENGTH = 64; + +function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined { + return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined; +} + +function isProtocolV1TransportChunk(data: Uint8Array) { + return data.length >= 9 && data[0] === 0x3f && data[1] === 0x23 && data[2] === 0x23; +} + +function readProtocolV1PayloadLength(data: Uint8Array) { + return data[5] * 0x1000000 + data[6] * 0x10000 + data[7] * 0x100 + data[8]; +} export default class LowlevelTransport { _messages: ReturnType | undefined; + _messagesV2: ReturnType | undefined; + configured = false; Log?: any; @@ -18,6 +53,51 @@ export default class LowlevelTransport { plugin: LowlevelTransportSharedPlugin = {} as LowlevelTransportSharedPlugin; + private deviceProtocol: Map = new Map(); + + private deviceProtocolHints: Map = new Map(); + + private protocolV2Assemblers: Map = new Map(); + + private protocolV2Generations: Map = new Map(); + + private connectedDevices: Set = new Set(); + + private protocolV2Links = new ProtocolV2LinkManager({ + getSchemas: () => { + if (!this._messages || !this._messagesV2) { + throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); + } + return { + protocolV1: this._messages, + protocolV2: this._messagesV2, + }; + }, + classifyError: () => 'link-fatal', + onLinkInvalidated: async (uuid, reason) => { + this.protocolV2Assemblers.get(uuid)?.reset(); + this.Log?.debug(`[LowlevelTransport] Protocol V2 link invalidated: ${uuid}`, reason); + if (reason.startsWith('Protocol V2 link-fatal error:')) { + this.deviceProtocol.delete(uuid); + this.advanceProtocolV2Generation(uuid); + try { + await this.plugin.disconnect(uuid); + } catch (error) { + this.Log?.debug( + `[LowlevelTransport] disconnect tainted Protocol V2 link failed: ${uuid}`, + error + ); + } finally { + this.connectedDevices.delete(uuid); + } + } + }, + }); + + getProtocolType(path: string): ProtocolType | undefined { + return this.deviceProtocol.get(path); + } + init(logger: any, emitter: EventEmitter, plugin: LowlevelTransportSharedPlugin) { this.Log = logger; this.emitter = emitter; @@ -31,30 +111,66 @@ export default class LowlevelTransport { this._messages = messages; } + configureProtocolV2(signedData: any) { + this._messagesV2 = parseConfigure(signedData); + this.protocolV2Links + .invalidateAllLinks('Protocol V2 schema reconfigured') + .catch(error => this.Log?.debug('Protocol V2 schema link cleanup failed:', error)); + } + listen() { // empty } - enumerate() { - return this.plugin.enumerate(); + async enumerate() { + const devices = await this.plugin.enumerate(); + return devices.map((device: LowLevelDevice) => { + const protocolHint = inferProtocolHintFromDeviceName(device.name); + if (protocolHint) { + this.deviceProtocolHints.set(device.id, protocolHint); + } + return device; + }); } async acquire(input: LowLevelAcquireInput) { + const alreadyConnected = this.connectedDevices.has(input.uuid); try { await this.plugin.connect(input.uuid); - return { uuid: input.uuid }; + if (!alreadyConnected) { + this.connectedDevices.add(input.uuid); + this.advanceProtocolV2Generation(input.uuid); + } } catch (error) { + this.connectedDevices.delete(input.uuid); this.Log.debug('lowlelvel transport connect error: ', error); throw ERRORS.TypedError( HardwareErrorCode.LowlevelTrasnportConnectError, error.message ?? error ); } + + this.protocolV2Assemblers.set(input.uuid, new ProtocolV2FrameAssembler()); + const protocolHint = input.expectedProtocol + ? undefined + : this.deviceProtocolHints.get(input.uuid); + const protocolType = await this.detectProtocol( + input.uuid, + input.expectedProtocol, + protocolHint + ); + return { uuid: input.uuid, protocolType }; } async release(uuid: string) { try { + await this.protocolV2Links.invalidateLink(uuid, 'Lowlevel transport released'); await this.plugin.disconnect(uuid); + this.connectedDevices.delete(uuid); + this.deviceProtocol.delete(uuid); + // 设备名称推断出的协议提示不依赖当前连接,保留它可以让快速重连 + // 直接执行 Protocol V2 探测,避免先发送一次无意义的 V1 Initialize。 + this.protocolV2Assemblers.delete(uuid); return true; } catch (error) { this.Log.debug('lowlelvel transport disconnect error: ', error); @@ -62,23 +178,46 @@ export default class LowlevelTransport { } } - async call(uuid: string, name: string, data: Record) { + async call( + uuid: string, + name: string, + data: Record, + options?: TransportCallOptions + ) { if (this._messages === null || !this._messages) { throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); } - const messages = this._messages; - if (LogBlockCommand.has(name)) { - this.Log.debug('lowlevel-transport', 'call-', ' name: ', name); - } else { - this.Log.debug('lowlevel-transport', 'call-', ' name: ', name, ' data: ', data); + const protocol = this.getProtocolType(uuid); + if (!protocol) { + throw ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + `Device protocol has not been detected for ${uuid}` + ); } + this.Log.debug('transport call', { name, protocol }); - const buffers = buildBuffers(messages, name, data); + if (protocol === 'V2') { + return this.callProtocolV2(uuid, name, data, options); + } + + return this.callProtocolV1(uuid, name, data, options); + } + + private async callProtocolV1( + uuid: string, + name: string, + data: Record, + options?: TransportCallOptions + ) { + if (!this._messages) { + throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); + } + + const messages = this._messages; + const buffers = ProtocolV1.encodeTransportPackets(messages, name, data); for (const o of buffers) { const outData = o.toString('hex'); - // Upload resources on low-end phones may OOM - this.Log.debug('send hex strting: ', outData); try { await this.plugin.send(uuid, outData); } catch (e) { @@ -88,19 +227,304 @@ export default class LowlevelTransport { } try { - const response = await this.plugin.receive(); - if (typeof response !== 'string') { - throw new Error('Returning data is not string'); - } - this.Log.debug('receive data: ', response); - const jsonData = receiveOne(messages, response); + const response = await this.readProtocolV1Message(uuid, options?.timeoutMs); + const jsonData = ProtocolV1.decodeMessage(messages, response); return check.call(jsonData); } catch (e) { - this.Log.error('lowlevel call error: ', e); + if (name === 'Initialize' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) { + this.Log.debug('[LowlevelTransport] Protocol V1 Initialize probe call failed:', e); + } else { + this.Log.error('lowlevel call error: ', e); + } throw e; } } + private createProtocolTimeoutError(name: string, timeout: number) { + return ERRORS.TypedError( + HardwareErrorCode.BleTimeoutError, + `Lowlevel response timeout after ${timeout}ms for ${name}` + ); + } + + private createProtocolMismatchError(expected: ProtocolType) { + return ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol` + ); + } + + private createProtocolDetectionError() { + return ERRORS.TypedError( + HardwareErrorCode.BleTimeoutError, + 'Unable to detect BLE protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping' + ); + } + + private clearProbeProtocol(uuid: string, protocol: ProtocolType) { + if (this.deviceProtocol.get(uuid) === protocol) { + this.deviceProtocol.delete(uuid); + } + } + + private async detectProtocol( + uuid: string, + expectedProtocol?: ProtocolType, + protocolHint?: ProtocolType + ): Promise { + if (expectedProtocol === 'V2') { + // 固件升级重启后的 bootloader 不保证响应 Ping。上层显式传入 V2 + // 表示协议已经在重启前确认,这里与 RN/Electron BLE 保持一致, + // 直接建立 V2 Link,首个业务命令负责验证链路是否可用。 + this.deviceProtocol.set(uuid, 'V2'); + this.Log?.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V2 (expected)`); + return 'V2'; + } + + if (expectedProtocol === 'V1') { + if (await this.probeProtocolV1(uuid)) { + this.deviceProtocol.set(uuid, 'V1'); + this.Log?.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V1 (expected)`); + return 'V1'; + } + throw this.createProtocolMismatchError(expectedProtocol); + } + + if (protocolHint === 'V2' && (await this.probeProtocolV2(uuid))) { + this.deviceProtocol.set(uuid, 'V2'); + this.Log?.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V2 (hint)`); + return 'V2'; + } + + const cachedProtocol = this.deviceProtocol.get(uuid); + if (cachedProtocol === 'V2' && (await this.probeProtocolV2(uuid))) { + this.deviceProtocol.set(uuid, 'V2'); + this.Log?.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V2 (cached)`); + return 'V2'; + } + + const protocolV1Detected = await this.probeProtocolV1(uuid); + if (protocolV1Detected) { + this.deviceProtocol.set(uuid, 'V1'); + this.Log?.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V1`); + return 'V1'; + } + + await this.resetConnectionAfterProbe(uuid, 'V1'); + if (await this.probeProtocolV2(uuid)) { + this.deviceProtocol.set(uuid, 'V2'); + this.Log?.debug(`[LowlevelTransport] detectProtocol: uuid=${uuid} -> V2`); + return 'V2'; + } + + this.deviceProtocol.delete(uuid); + throw this.createProtocolDetectionError(); + } + + private async resetConnectionAfterProbe(uuid: string, protocol: ProtocolType) { + await this.protocolV2Links.invalidateLink( + uuid, + `Reset connection after Protocol ${protocol} probe` + ); + this.protocolV2Assemblers.get(uuid)?.reset(); + + try { + this.connectedDevices.delete(uuid); + await this.plugin.disconnect(uuid); + } catch (error) { + this.Log?.debug( + `[LowlevelTransport] disconnect after Protocol ${protocol} probe failed:`, + error + ); + } + + try { + await this.plugin.connect(uuid); + this.connectedDevices.add(uuid); + this.advanceProtocolV2Generation(uuid); + } catch (error) { + this.Log?.debug( + `[LowlevelTransport] reconnect after Protocol ${protocol} probe failed:`, + error + ); + throw ERRORS.TypedError( + HardwareErrorCode.LowlevelTrasnportConnectError, + error.message ?? error + ); + } + } + + private async probeProtocolV1(uuid: string) { + if (!this._messages) { + return false; + } + + try { + this.deviceProtocol.set(uuid, 'V1'); + await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS }); + return true; + } catch (error) { + this.clearProbeProtocol(uuid, 'V1'); + this.Log?.debug('[LowlevelTransport] Protocol V1 Initialize probe failed:', error); + return false; + } + } + + private async probeProtocolV2(uuid: string) { + if (!this._messages || !this._messagesV2) { + return false; + } + + this.deviceProtocol.set(uuid, 'V2'); + this.protocolV2Assemblers.get(uuid)?.reset(); + try { + const detected = await probeProtocolV2Helper({ + call: (name: string, data: Record, options?: TransportCallOptions) => + this.callProtocolV2(uuid, name, data, options), + timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT_MS, + logger: this.Log, + logPrefix: 'ProtocolV2 Lowlevel-BLE', + onProbeFailed: async () => { + this.protocolV2Assemblers.get(uuid)?.reset(); + await this.resetConnectionAfterProbe(uuid, 'V2'); + }, + }); + if (!detected) { + this.clearProbeProtocol(uuid, 'V2'); + } + return detected; + } catch (error) { + this.clearProbeProtocol(uuid, 'V2'); + throw error; + } + } + + private async receiveHex(uuid: string, timeoutMs: number | undefined, commandName: string) { + const response = await withProtocolTimeout(this.plugin.receive(uuid), timeoutMs, () => + this.createProtocolTimeoutError(commandName, timeoutMs ?? 0) + ); + if (typeof response !== 'string') { + throw new Error('Returning data is not string'); + } + return response; + } + + private async readProtocolV1Message(uuid: string, timeoutMs?: number) { + const first = await this.receiveHex(uuid, timeoutMs, 'ProtocolV1'); + const firstData = hexToBytes(first); + if (!isProtocolV1TransportChunk(firstData)) { + return first; + } + + const payloadLength = readProtocolV1PayloadLength(firstData); + let buffer = firstData.slice(3); + const expectedLength = PROTOCOL_V1_MESSAGE_HEADER_SIZE + payloadLength; + + while (buffer.length < expectedLength) { + const next = await this.receiveHex(uuid, timeoutMs, 'ProtocolV1'); + buffer = concatUint8Arrays([buffer, hexToBytes(next)]); + } + + return bytesToHex(buffer.slice(0, expectedLength)); + } + + private async readProtocolV2Frame(uuid: string, timeoutMs?: number, commandName = 'ProtocolV2') { + let assembler = this.protocolV2Assemblers.get(uuid); + if (!assembler) { + assembler = new ProtocolV2FrameAssembler(); + this.protocolV2Assemblers.set(uuid, assembler); + } + + const queuedFrame = assembler.push(new Uint8Array(0)); + if (queuedFrame) return queuedFrame; + + let frame: Uint8Array | undefined; + while (!frame) { + const response = await this.receiveHex(uuid, timeoutMs, commandName); + const chunk = hexToBytes(response); + if (chunk.length > 0) { + frame = assembler.push(chunk); + } + } + return frame; + } + + private async writeProtocolV2Frame(uuid: string, frame: Uint8Array) { + for (let offset = 0; offset < frame.length; offset += LOWLEVEL_PROTOCOL_V2_PACKET_LENGTH) { + const chunk = frame.slice(offset, offset + LOWLEVEL_PROTOCOL_V2_PACKET_LENGTH); + await this.plugin.send(uuid, bytesToHex(chunk)); + } + } + + private async callProtocolV2( + uuid: string, + name: string, + data: Record, + options?: TransportCallOptions + ) { + if (!this._messages || !this._messagesV2) { + throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); + } + + const timeoutMs = options?.timeoutMs ?? LOWLEVEL_PROTOCOL_TIMEOUT_MS; + + try { + return await this.protocolV2Links.call( + uuid, + () => this.createProtocolV2Adapter(uuid), + name, + data, + { + ...options, + timeoutMs, + } + ); + } catch (e) { + this.Log.error('lowlevel Protocol V2 call error: ', e); + throw e; + } + } + + private createProtocolV2Adapter(uuid: string) { + const generation = this.protocolV2Generations.get(uuid) ?? 0; + const assertCurrentGeneration = () => { + if (this.protocolV2Generations.get(uuid) !== generation) { + throw new Error(`Protocol V2 connection generation changed for ${uuid}`); + } + }; + + return { + router: PROTOCOL_V2_CHANNEL_BLE_UART, + maxFrameBytes: PROTOCOL_V2_BLE_FRAME_MAX_BYTES, + generation, + prepareCall: () => { + assertCurrentGeneration(); + this.protocolV2Assemblers.get(uuid)?.reset(); + }, + writeFrame: (frame: Uint8Array) => { + assertCurrentGeneration(); + return this.writeProtocolV2Frame(uuid, frame); + }, + readFrame: (context: { messageName: string; timeoutMs?: number }) => { + assertCurrentGeneration(); + return this.readProtocolV2Frame(uuid, context.timeoutMs, context.messageName); + }, + reset: () => { + this.protocolV2Assemblers.get(uuid)?.reset(); + }, + logger: this.Log, + logPrefix: 'ProtocolV2 Lowlevel-BLE', + createTimeoutError: (messageName: string, timeout: number) => + this.createProtocolTimeoutError(messageName, timeout), + }; + } + + private advanceProtocolV2Generation(uuid: string) { + const nextGeneration = (this.protocolV2Generations.get(uuid) ?? 0) + 1; + this.protocolV2Generations.set(uuid, nextGeneration); + return nextGeneration; + } + cancel() { this.Log.debug('lowlevel-transport', 'cancel'); } diff --git a/packages/hd-transport-lowlevel/src/types.ts b/packages/hd-transport-lowlevel/src/types.ts index 8a1e8ed00..07b6ee3e4 100644 --- a/packages/hd-transport-lowlevel/src/types.ts +++ b/packages/hd-transport-lowlevel/src/types.ts @@ -1,3 +1,6 @@ +import type { ProtocolType } from '@onekeyfe/hd-transport'; + export type LowLevelAcquireInput = { uuid: string; + expectedProtocol?: ProtocolType; }; diff --git a/packages/hd-transport-react-native/src/BleManager.ts b/packages/hd-transport-react-native/src/BleManager.ts index 171a6f39c..0c20da1b6 100644 --- a/packages/hd-transport-react-native/src/BleManager.ts +++ b/packages/hd-transport-react-native/src/BleManager.ts @@ -1,10 +1,11 @@ import BleUtils from '@onekeyfe/react-native-ble-utils'; import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared'; -import { LoggerNames, getLogger } from '@onekeyfe/hd-core'; + +import { bleLogger } from './logger'; import type { Peripheral } from '@onekeyfe/react-native-ble-utils'; -const Logger = getLogger(LoggerNames.HdBleTransport); +const Logger = bleLogger; /** * get the device basic info of connected devices @@ -20,36 +21,32 @@ export const pairDevice = (macAddress: string) => BleUtils.pairDevice(macAddress export const onDeviceBondState = (bleMacAddress: string): Promise => new Promise((resolve, reject) => { - let timeout: any | undefined; - - const cleanup = (cleanupListener: (() => void) | undefined) => { + const cleanup = () => { if (timeout) { clearTimeout(timeout); } if (cleanupListener) cleanupListener(); }; + const timeout = setTimeout(() => { + cleanup(); + reject(ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded, 'device is not bonded')); + }, 60 * 1000); + const cleanupListener = BleUtils.onDeviceBondState(peripheral => { if (peripheral.id?.toLowerCase() !== bleMacAddress.toLowerCase()) { return; } const { bondState } = peripheral; - if (bondState.preState === 'BOND_NONE' && bondState.state === 'BOND_BONDING') { - timeout = setTimeout(() => { - cleanup(cleanupListener); - reject(ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded, 'device is not bonded')); - }, 60 * 1000); - } - const hasBonded = bondState.preState === 'BOND_BONDING' && bondState.state === 'BOND_BONDED'; const hasCanceled = bondState.preState === 'BOND_BONDING' && bondState.state === 'BOND_NONE'; Logger.debug('onDeviceBondState bondState:', bondState); if (hasBonded) { - cleanup(cleanupListener); + cleanup(); resolve(peripheral); } else if (hasCanceled) { - cleanup(cleanupListener); + cleanup(); reject(ERRORS.TypedError(HardwareErrorCode.BleDeviceBondedCanceled, 'bonding canceled')); } }); diff --git a/packages/hd-transport-react-native/src/BleTransport.ts b/packages/hd-transport-react-native/src/BleTransport.ts index 25084bcd0..a59ca5c81 100644 --- a/packages/hd-transport-react-native/src/BleTransport.ts +++ b/packages/hd-transport-react-native/src/BleTransport.ts @@ -1,10 +1,11 @@ import { BleErrorCode } from 'react-native-ble-plx'; -import { LoggerNames, getLogger, wait } from '@onekeyfe/hd-core'; +import { wait } from '@onekeyfe/hd-shared'; + +import { bleLogger } from './logger'; import type { Characteristic, Device, Subscription } from 'react-native-ble-plx'; -// import { wait } from '@onekeyfe/hd-core/src/utils'; -const Log = getLogger(LoggerNames.HdBleTransport); +const Log = bleLogger; export default class BleTransport { id: string; @@ -13,7 +14,7 @@ export default class BleTransport { device: Device; - mtuSize = 20; + mtuSize = 23; writeCharacteristic: Characteristic; @@ -23,6 +24,10 @@ export default class BleTransport { disconnectSubscription?: Subscription; + notifyTransactionId?: string; + + monitorToken?: number; + static MAX_RETRIES = 5; static RETRY_DELAY = 2000; @@ -36,7 +41,6 @@ export default class BleTransport { this.device = device; this.writeCharacteristic = writeCharacteristic; this.notifyCharacteristic = notifyCharacteristic; - console.log(`BleTransport(${String(this.id)}) new instance`); } /** diff --git a/packages/hd-transport-react-native/src/__tests__/bleStrategy.test.ts b/packages/hd-transport-react-native/src/__tests__/bleStrategy.test.ts new file mode 100644 index 000000000..16ba273d9 --- /dev/null +++ b/packages/hd-transport-react-native/src/__tests__/bleStrategy.test.ts @@ -0,0 +1,42 @@ +import { hasWritableCapability, resolveProtocolV2PacketCapacity } from '../bleStrategy'; + +describe('React Native BLE strategy', () => { + test('accepts writeWithoutResponse-only characteristics', () => { + const characteristic = { + isWritableWithResponse: false, + isWritableWithoutResponse: true, + }; + + expect(hasWritableCapability(characteristic)).toBe(true); + }); + + test('falls back to Android default ATT payload when MTU is unavailable', () => { + expect( + resolveProtocolV2PacketCapacity({ + platform: 'android', + androidPacketLength: 192, + mtu: null, + }) + ).toBe(20); + }); + + test('caps Android packet length by negotiated MTU payload', () => { + expect( + resolveProtocolV2PacketCapacity({ + platform: 'android', + androidPacketLength: 192, + mtu: 100, + }) + ).toBe(97); + }); + + test('keeps iOS packet length controlled by tuning profile', () => { + expect( + resolveProtocolV2PacketCapacity({ + platform: 'ios', + iosPacketLength: 244, + mtu: 256, + }) + ).toBe(244); + }); +}); diff --git a/packages/hd-transport-react-native/src/__tests__/protocolV2Link.test.ts b/packages/hd-transport-react-native/src/__tests__/protocolV2Link.test.ts new file mode 100644 index 000000000..f9838d876 --- /dev/null +++ b/packages/hd-transport-react-native/src/__tests__/protocolV2Link.test.ts @@ -0,0 +1,243 @@ +import { EventEmitter } from 'events'; +import transportPackage, { + PROTOCOL_V2_CHANNEL_BLE_UART, + ProtocolV2, + bytesToHex, +} from '@onekeyfe/hd-transport'; +import { HardwareErrorCode } from '@onekeyfe/hd-shared'; + +import ReactNativeBleTransport from '../index'; + +jest.mock( + 'react-native', + () => ({ + PermissionsAndroid: {}, + Platform: { OS: 'ios' }, + }), + { virtual: true } +); + +jest.mock('react-native-ble-plx', () => ({ + BleATTErrorCode: { UnlikelyError: 14 }, + BleError: class BleError extends Error {}, + BleErrorCode: { + DeviceAlreadyConnected: 203, + DeviceDisconnected: 205, + DeviceMTUChangeFailed: 206, + OperationCancelled: 2, + CharacteristicNotFound: 404, + }, + BleManager: jest.fn(), + ScanMode: { LowLatency: 2 }, +})); + +jest.mock('../BleManager', () => ({ + getConnectedDeviceIds: jest.fn(() => Promise.resolve([])), + onDeviceBondState: jest.fn(() => Promise.resolve()), + pairDevice: jest.fn(() => Promise.resolve({ bonded: true, bonding: false })), +})); + +jest.mock('../subscribeBleOn', () => ({ + subscribeBleOn: jest.fn(() => Promise.resolve()), +})); + +const { parseConfigure } = transportPackage; + +const protocolV1Schema = { + nested: { + Initialize: { fields: {} }, + Success: { + fields: { + message: { type: 'string', id: 1 }, + }, + }, + MessageType: { + values: { + MessageType_Initialize: 1, + MessageType_Success: 2, + }, + }, + }, +}; + +const protocolV2Schema = { + nested: { + Ping: { + fields: { + message: { type: 'string', id: 1 }, + }, + }, + FileWrite: { fields: {} }, + Success: { + fields: { + message: { type: 'string', id: 1 }, + }, + }, + MessageType: { + values: { + MessageType_Ping: 60206, + MessageType_Success: 60207, + MessageType_FileWrite: 60805, + }, + }, + }, +}; + +const schemas = { + protocolV1: parseConfigure(protocolV1Schema), + protocolV2: parseConfigure(protocolV2Schema), +}; + +const createHarness = () => { + const uuid = 'rn-pro2-id'; + const sentSeqs: number[] = []; + let shouldRespond = true; + let notifyCallback: + | (( + error: (Error & { reason?: string }) | null, + characteristic: { value: string } | null + ) => void) + | undefined; + const notifyCharacteristic = { + uuid: '0003', + deviceID: uuid, + isNotifiable: true, + monitor: jest.fn(callback => { + notifyCallback = callback; + return { remove: jest.fn() }; + }), + }; + const handleWrite = (base64: string) => { + const frame = Buffer.from(base64, 'base64'); + sentSeqs.push(frame[6]); + if (shouldRespond) { + const response = ProtocolV2.encodeFrame( + schemas, + 'Success', + { message: 'ok' }, + { router: PROTOCOL_V2_CHANNEL_BLE_UART } + ); + notifyCallback?.(null, { value: Buffer.from(response).toString('base64') }); + } + return Promise.resolve(); + }; + const writeCharacteristic = { + uuid: '0002', + deviceID: uuid, + isWritableWithResponse: true, + isWritableWithoutResponse: true, + writeWithResponse: jest.fn(handleWrite), + writeWithoutResponse: jest.fn(handleWrite), + }; + const device = { + id: uuid, + name: 'OneKey Pro 2', + localName: 'OneKey Pro 2', + serviceUUIDs: ['fffd'], + isConnected: jest.fn(() => Promise.resolve(true)), + onDisconnected: jest.fn(() => ({ remove: jest.fn() })), + }; + const bleManager = { + devices: jest.fn(() => Promise.resolve([device])), + connectedDevices: jest.fn(() => Promise.resolve([])), + cancelTransaction: jest.fn(() => Promise.resolve()), + }; + const transport = new ReactNativeBleTransport({ scanTimeout: 1 }); + transport.blePlxManager = bleManager; + transport.resolveCharacteristics = jest.fn(() => + Promise.resolve({ writeCharacteristic, notifyCharacteristic }) + ); + transport.init({ debug: jest.fn(), error: jest.fn() }, new EventEmitter()); + transport.configure(protocolV1Schema); + transport.configureProtocolV2(protocolV2Schema); + + return { + transport, + uuid, + sentSeqs, + writeCharacteristic, + setShouldRespond(value: boolean) { + shouldRespond = value; + }, + emitMonitorError(error: Error & { reason?: string }) { + notifyCallback?.(error, null); + }, + }; +}; + +describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => { + test('keeps the Protocol V2 sequence across probe and the next call', async () => { + const { transport, uuid, sentSeqs } = createHarness(); + + await transport.acquire({ uuid }); + await transport.call(uuid, 'Ping', { message: 'after-probe' }); + + expect(sentSeqs).toEqual([1, 2]); + expect(bytesToHex(new Uint8Array([sentSeqs[0], sentSeqs[1]]))).toBe('0102'); + await transport.release(uuid, true); + }); + + test('rejects the active Protocol V2 reader when the current monitor errors', async () => { + const harness = createHarness(); + const { transport, uuid, sentSeqs } = harness; + await transport.acquire({ uuid }); + harness.setShouldRespond(false); + + const call = transport.call(uuid, 'Ping', { message: 'wait-for-monitor' }, { timeoutMs: 50 }); + while (sentSeqs.length < 2) { + await Promise.resolve(); + } + await new Promise(resolve => { + setTimeout(resolve, 0); + }); + harness.emitMonitorError(Object.assign(new Error('monitor failed'), { reason: 'link lost' })); + + await expect(call).rejects.toMatchObject({ + errorCode: HardwareErrorCode.BleCharacteristicNotifyError, + }); + }); + + test('retains the sequence cursor when a new monitor generation is acquired', async () => { + const { transport, uuid, sentSeqs } = createHarness(); + + await transport.acquire({ uuid }); + await transport.release(uuid, true); + await transport.acquire({ uuid }); + + expect(sentSeqs).toEqual([1, 2]); + await transport.release(uuid, true); + }); + + test('uses withoutResponse for normal and high-volume calls', async () => { + const { transport, uuid, writeCharacteristic } = createHarness(); + + await transport.acquire({ uuid }); + expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(1); + expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled(); + + await transport.call(uuid, 'Ping', { message: 'normal' }); + expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(2); + expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled(); + + await transport.call(uuid, 'FileWrite', {}); + expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(3); + expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled(); + await transport.release(uuid, true); + }); + + test('rejects an active Protocol V2 reader when disconnect resets the link', async () => { + const harness = createHarness(); + const { transport, uuid, sentSeqs } = harness; + await transport.acquire({ uuid }); + harness.setShouldRespond(false); + + const call = transport.call(uuid, 'Ping', { message: 'disconnect' }, { timeoutMs: 50 }); + while (sentSeqs.length < 2) { + await Promise.resolve(); + } + + const rejection = expect(call).rejects.toThrow('React Native BLE transport disconnected'); + await transport.disconnect(uuid); + await rejection; + }); +}); diff --git a/packages/hd-transport-react-native/src/bleStrategy.ts b/packages/hd-transport-react-native/src/bleStrategy.ts new file mode 100644 index 000000000..6a06dc364 --- /dev/null +++ b/packages/hd-transport-react-native/src/bleStrategy.ts @@ -0,0 +1,35 @@ +import { ANDROID_DEFAULT_MTU, ANDROID_PACKET_LENGTH, IOS_PACKET_LENGTH } from './constants'; + +export type BlePlatform = 'ios' | 'android' | string; + +export type BleWriteCapability = { + isWritableWithResponse?: boolean | null; + isWritableWithoutResponse?: boolean | null; +}; + +export function hasWritableCapability(characteristic: BleWriteCapability) { + return !!(characteristic.isWritableWithResponse || characteristic.isWritableWithoutResponse); +} + +export function resolveProtocolV2PacketCapacity({ + platform, + iosPacketLength = IOS_PACKET_LENGTH, + androidPacketLength = ANDROID_PACKET_LENGTH, + mtu, +}: { + platform: BlePlatform; + iosPacketLength?: number; + androidPacketLength?: number; + mtu?: number | null; +}) { + if (platform === 'ios') { + return iosPacketLength; + } + + if (platform === 'android') { + const payloadLength = Math.max((mtu ?? ANDROID_DEFAULT_MTU) - 3, 1); + return Math.min(androidPacketLength, payloadLength); + } + + return androidPacketLength; +} diff --git a/packages/hd-transport-react-native/src/constants.ts b/packages/hd-transport-react-native/src/constants.ts index 9161bdee3..fc97f059c 100644 --- a/packages/hd-transport-react-native/src/constants.ts +++ b/packages/hd-transport-react-native/src/constants.ts @@ -1,5 +1,6 @@ export const IOS_PACKET_LENGTH = 128; export const ANDROID_PACKET_LENGTH = 192; +export const ANDROID_DEFAULT_MTU = 23; type BluetoothServices = Record< string, @@ -35,9 +36,32 @@ export const getInfosForServiceUuid = (serviceUuid: string, deviceType: 'classic if (!services) { return null; } - const service = services[serviceUuid]; + const normalizedServiceUuid = normalizeBleUuid(serviceUuid); + const service = + services[serviceUuid] ?? + Object.values(services).find( + item => normalizeBleUuid(item.serviceUuid) === normalizedServiceUuid + ); if (!service) { return null; } return service; }; + +export const normalizeBleUuid = (uuid?: string | null) => + (uuid ?? '').replace(/-/g, '').toLowerCase(); + +export const getBleUuidKey = (uuid?: string | null) => { + const normalized = normalizeBleUuid(uuid); + return normalized.length >= 8 ? normalized.substring(4, 8) : normalized; +}; + +export const isSameBleUuid = (left?: string | null, right?: string | null) => { + const normalizedLeft = normalizeBleUuid(left); + const normalizedRight = normalizeBleUuid(right); + + return ( + normalizedLeft === normalizedRight || + (getBleUuidKey(left) !== '' && getBleUuidKey(left) === getBleUuidKey(right)) + ); +}; diff --git a/packages/hd-transport-react-native/src/index.ts b/packages/hd-transport-react-native/src/index.ts index b8ed9a359..1edba16d4 100644 --- a/packages/hd-transport-react-native/src/index.ts +++ b/packages/hd-transport-react-native/src/index.ts @@ -9,33 +9,44 @@ import { } from 'react-native-ble-plx'; import ByteBuffer from 'bytebuffer'; import transport, { - COMMON_HEADER_SIZE, LogBlockCommand, type OneKeyDeviceInfoBase, + PROTOCOL_V1_MESSAGE_HEADER_SIZE, + PROTOCOL_V2_BLE_FRAME_MAX_BYTES, + PROTOCOL_V2_CHANNEL_BLE_UART, + type ProtocolType, + ProtocolV2FrameAssembler, + ProtocolV2LinkManager, + type TransportCallOptions, + probeProtocolV2 as probeProtocolV2Helper, } from '@onekeyfe/hd-transport'; import { ERRORS, HardwareErrorCode, createDeferred, isOnekeyDevice } from '@onekeyfe/hd-shared'; -import { LoggerNames, getLogger } from '@onekeyfe/hd-core'; import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager'; +import { hasWritableCapability, resolveProtocolV2PacketCapacity } from './bleStrategy'; import { subscribeBleOn } from './subscribeBleOn'; import { ANDROID_PACKET_LENGTH, IOS_PACKET_LENGTH, + getBleUuidKey, getBluetoothServiceUuids, getInfosForServiceUuid, + isSameBleUuid, } from './constants'; import { isHeaderChunk } from './utils/validateNotify'; import BleTransport from './BleTransport'; import timer from './utils/timer'; +import { bleLogger, setBleLogger } from './logger'; +import { createTransportCallLog } from './transportLog'; import type { Deferred } from '@onekeyfe/hd-shared'; import type { Characteristic, Device, Subscription } from 'react-native-ble-plx'; import type EventEmitter from 'events'; import type { BleAcquireInput, TransportOptions } from './types'; -const { check, buildBuffers, receiveOne, parseConfigure } = transport; +const { check, ProtocolV1, parseConfigure } = transport; -const Log = getLogger(LoggerNames.HdBleTransport); +const Log = bleLogger; const transportCache: Record = {}; const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5; @@ -54,11 +65,6 @@ type ResolvedBleCharacteristics = { notifyCharacteristic: Characteristic; }; -const getBleIdentityName = (device?: { name?: string | null } | null): string | null => { - const localName = (device as { localName?: string | null } | undefined)?.localName; - return device?.name ?? localName ?? null; -}; - const delay = (ms: number) => new Promise(resolve => { setTimeout(resolve, ms); @@ -97,9 +103,77 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) => Math.min(baseDelayMs * 2 ** attempt, maxDelayMs); +const BLE_RESPONSE_TIMEOUT_MS = 30_000; +const PROTOCOL_PROBE_TIMEOUT_MS = 1000; +const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000; +const DEVICE_SCAN_TIMEOUT_MS = 8000; +const IOS_NOTIFY_READY_DELAY_MS = 150; +const ANDROID_NOTIFY_READY_DELAY_MS = 300; +export type ProtocolV2BleTuning = { + iosPacketLength?: number; + androidPacketLength?: number; +}; + +type ResolvedProtocolV2BleTuning = Required; + +const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = { + iosPacketLength: IOS_PACKET_LENGTH, + androidPacketLength: ANDROID_PACKET_LENGTH, +}; + +let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING }; + +const normalizePositiveInteger = (value: unknown, fallback: number) => { + const normalized = Number(value); + if (!Number.isFinite(normalized) || normalized <= 0) return fallback; + return Math.floor(normalized); +}; + +export function configureProtocolV2BleTuning(tuning: ProtocolV2BleTuning = {}) { + protocolV2BleTuning = { + iosPacketLength: normalizePositiveInteger( + tuning.iosPacketLength, + protocolV2BleTuning.iosPacketLength + ), + androidPacketLength: normalizePositiveInteger( + tuning.androidPacketLength, + protocolV2BleTuning.androidPacketLength + ), + }; + Log?.debug('[ReactNativeBleTransport] BLE tuning configured', protocolV2BleTuning); +} + +export function resetProtocolV2BleTuning() { + protocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING }; + Log?.debug('[ReactNativeBleTransport] BLE tuning reset', protocolV2BleTuning); +} + +export function getProtocolV2BleTuning() { + return { ...protocolV2BleTuning }; +} + +function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined { + return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined; +} + +function getDeviceDisplayName(device?: Device | null) { + return device?.name || device?.localName || null; +} + +function isGenericBleService(uuid?: string | null) { + return ['1800', '1801', '180a'].includes(getBleUuidKey(uuid)); +} -let connectOptions: Record = { - requestMTU: 256, +function hasKnownOneKeyService(device?: Device | null) { + return (device?.serviceUUIDs ?? []).some(serviceUuid => + getInfosForServiceUuid(serviceUuid, 'classic') + ); +} + +const ANDROID_REQUEST_MTU = 256; + +const connectOptions: Record = { + requestMTU: ANDROID_REQUEST_MTU, timeout: 3000, refreshGatt: 'OnConnected', }; @@ -108,12 +182,30 @@ export type IOneKeyDevice = OneKeyDeviceInfoBase & Device; const tryToGetConfiguration = (device: Device) => { if (!device || !device.serviceUUIDs) return null; - const [serviceUUID] = device.serviceUUIDs; + const serviceUUID = device.serviceUUIDs.find(uuid => getInfosForServiceUuid(uuid, 'classic')); + if (!serviceUUID) return null; const infos = getInfosForServiceUuid(serviceUUID, 'classic'); if (!infos) return null; return infos; }; +const requestAndroidMtu = async (device: Device) => { + if (Platform.OS !== 'android') return device; + + try { + const mtuDevice = await device.requestMTU(ANDROID_REQUEST_MTU); + Log?.debug('[ReactNativeBleTransport] MTU configured', { + deviceId: device.id, + requested: ANDROID_REQUEST_MTU, + actual: mtuDevice.mtu, + }); + return mtuDevice; + } catch (error) { + Log?.debug('[ReactNativeBleTransport] Android MTU request failed:', error); + return device; + } +}; + type IOBleErrorRemap = Error | BleError | null | undefined; function remapError(error: IOBleErrorRemap) { @@ -151,13 +243,15 @@ export default class ReactNativeBleTransport { _messages: ReturnType | undefined; + _messagesV2: ReturnType | undefined; + name = 'ReactNativeBleTransport'; configured = false; stopped = false; - scanTimeout = 3000; + scanTimeout = DEVICE_SCAN_TIMEOUT_MS; runPromise: Deferred | null = null; @@ -165,11 +259,48 @@ export default class ReactNativeBleTransport { firmwareUploadWriteRecoveryIds = new Set(); + /** Per-device protocol type detected by active wire-level probe after connect. */ + private deviceProtocol: Map = new Map(); + + private deviceProtocolHints: Map = new Map(); + + private protocolV2Assemblers: Map = new Map(); + + private protocolV2FrameQueues: Map = new Map(); + + private protocolV2FramePromises: Map> = new Map(); + + private protocolV2Links = new ProtocolV2LinkManager({ + getSchemas: () => { + if (!this._messages || !this._messagesV2) { + throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); + } + return { + protocolV1: this._messages, + protocolV2: this._messagesV2, + }; + }, + classifyError: () => 'link-fatal', + onLinkInvalidated: async (uuid, reason) => { + this.protocolV2Assemblers.get(uuid)?.reset(); + this.rejectProtocolV2Frames(uuid, new Error(reason)); + Log?.debug('[ReactNativeBleTransport] Protocol V2 link invalidated:', uuid, reason); + if (reason.startsWith('Protocol V2 link-fatal error:')) { + await this.release(uuid, true); + } + }, + }); + + private monitorTokens: Map = new Map(); + + private nextMonitorToken = 1; + constructor(options: TransportOptions) { - this.scanTimeout = options.scanTimeout ?? 3000; + this.scanTimeout = options.scanTimeout ?? DEVICE_SCAN_TIMEOUT_MS; } - init(_logger: any, emitter: EventEmitter) { + init(logger: any, emitter: EventEmitter) { + setBleLogger(logger); this.emitter = emitter; } @@ -179,6 +310,13 @@ export default class ReactNativeBleTransport { this._messages = messages; } + configureProtocolV2(signedData: any) { + this._messagesV2 = parseConfigure(signedData); + this.protocolV2Links + .invalidateAllLinks('Protocol V2 schema reconfigured') + .catch(error => Log?.debug('Protocol V2 schema link cleanup failed:', error)); + } + listen() { // empty } @@ -206,7 +344,29 @@ export default class ReactNativeBleTransport { } } + let fallbackServiceUuid: string | undefined; + if (!infos) { + const services = await device.services(); + Log?.debug( + '[ReactNativeBleTransport] Known OneKey service UUID not found, discovered services:', + services?.map(service => service.uuid) + ); + + const knownService = services.find(service => + getInfosForServiceUuid(service.uuid, 'classic') + ); + const fallbackService = + knownService ?? services.find(service => !isGenericBleService(service.uuid)) ?? services[0]; + + if (fallbackService) { + fallbackServiceUuid = fallbackService.uuid; + characteristics = await device.characteristicsForService(fallbackService.uuid); + Log?.debug('[ReactNativeBleTransport] Using fallback BLE service:', fallbackService.uuid); + } + } + + if (!infos && !fallbackServiceUuid) { try { Log?.debug('cancel connection when service not found'); await device.cancelConnection(); @@ -216,7 +376,13 @@ export default class ReactNativeBleTransport { throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound); } - const { serviceUuid, writeUuid, notifyUuid } = infos; + const serviceUuid = infos?.serviceUuid ?? fallbackServiceUuid; + const writeUuid = infos?.writeUuid ?? '00000002-0000-1000-8000-00805f9b34fb'; + const notifyUuid = infos?.notifyUuid ?? '00000003-0000-1000-8000-00805f9b34fb'; + + if (!serviceUuid) { + throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound); + } if (!characteristics) { characteristics = await device.characteristicsForService(serviceUuid); @@ -229,9 +395,9 @@ export default class ReactNativeBleTransport { let writeCharacteristic; let notifyCharacteristic; for (const c of characteristics) { - if (c.uuid === writeUuid) { + if (isSameBleUuid(c.uuid, writeUuid)) { writeCharacteristic = c; - } else if (c.uuid === notifyUuid) { + } else if (isSameBleUuid(c.uuid, notifyUuid)) { notifyCharacteristic = c; } } @@ -244,7 +410,7 @@ export default class ReactNativeBleTransport { throw ERRORS.TypedError('BLECharacteristicNotFound: notify characteristic not found'); } - if (!writeCharacteristic.isWritableWithResponse) { + if (!hasWritableCapability(writeCharacteristic)) { throw ERRORS.TypedError('BLECharacteristicNotWritable: write characteristic not writable'); } @@ -267,6 +433,10 @@ export default class ReactNativeBleTransport { Log?.debug('device disconnect ignored during FirmwareUpload write recovery: ', uuid); return; } + if (transportCache[uuid] !== transport) { + Log?.debug('device disconnect ignored for stale transport: ', device?.id); + return; + } try { Log?.debug('device disconnect: ', device?.id); @@ -276,12 +446,14 @@ export default class ReactNativeBleTransport { connectId: device?.id, }); if (this.runPromise) { - this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError)); + const error = ERRORS.TypedError(HardwareErrorCode.BleConnectedError); + this.runPromise.reject(error); + this.rejectAllProtocolV2Frames(error); } } catch (e) { Log?.debug('device disconnect error: ', e); } finally { - this.release(uuid); + this.release(uuid, true); } }); } @@ -304,7 +476,6 @@ export default class ReactNativeBleTransport { e.errorCode === BleErrorCode.DeviceMTUChangeFailed || e.errorCode === BleErrorCode.OperationCancelled ) { - connectOptions = {}; device = await device.connect(); } else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) { throw e; @@ -319,7 +490,18 @@ export default class ReactNativeBleTransport { transport.device = device; transport.writeCharacteristic = writeCharacteristic; transport.notifyCharacteristic = notifyCharacteristic; - transport.notifySubscription = this._monitorCharacteristic(notifyCharacteristic, uuid); + const monitorToken = this.nextMonitorToken; + this.nextMonitorToken += 1; + const notifyTransactionId = `${uuid}:notify:${monitorToken}`; + transport.monitorToken = monitorToken; + transport.notifyTransactionId = notifyTransactionId; + this.monitorTokens.set(uuid, monitorToken); + transport.notifySubscription = this._monitorCharacteristic( + notifyCharacteristic, + uuid, + monitorToken, + notifyTransactionId + ); this.attachDisconnectSubscription(transport, device, uuid); } finally { this.firmwareUploadWriteRecoveryIds.delete(uuid); @@ -365,11 +547,11 @@ export default class ReactNativeBleTransport { blePlxManager.startDeviceScan( getBluetoothServiceUuids(), { + allowDuplicates: true, scanMode: ScanMode.LowLatency, }, (error, device) => { if (error) { - Log?.debug('ble scan manager: ', blePlxManager); Log?.debug('ble scan error: ', error); if ( [BleErrorCode.BluetoothPoweredOff, BleErrorCode.BluetoothInUnknownState].includes( @@ -392,14 +574,20 @@ export default class ReactNativeBleTransport { return; } - if (isOnekeyDevice(getBleIdentityName(device), device?.id)) { - Log?.debug('search device start ======================'); - const { name, localName, id } = device ?? {}; - Log?.debug( - `device name: ${name ?? ''}\nlocalName: ${localName ?? ''}\nid: ${id ?? ''}` - ); + const displayName = getDeviceDisplayName(device); + const isOneKey = + isOnekeyDevice(device?.name ?? null, device?.id) || + isOnekeyDevice(device?.localName ?? null, device?.id) || + hasKnownOneKeyService(device); + if (isOneKey) { addDevice(device as unknown as Device); - Log?.debug('search device end ======================\n'); + } else if (displayName && /\bpro\s*2\b/i.test(displayName)) { + Log?.debug('[ReactNativeBleTransport] Pro2-like BLE device was not accepted:', { + name: device?.name, + localName: device?.localName, + id: device?.id, + serviceUUIDs: device?.serviceUUIDs, + }); } } ); @@ -411,7 +599,6 @@ export default class ReactNativeBleTransport { const hasCachedServiceUuid = Boolean(serviceUUIDs?.length); const keepDevice = Platform.OS === 'ios' || hasCachedServiceUuid; if (keepDevice) { - Log?.debug('search connected peripheral: ', device.id); addDevice(device as unknown as Device); } } @@ -420,7 +607,22 @@ export default class ReactNativeBleTransport { const addDevice = (device: Device) => { if (deviceList.every(d => d.id !== device.id)) { - deviceList.push({ ...device, commType: 'ble' } as IOneKeyDevice); + const displayName = getDeviceDisplayName(device) ?? 'Unknown BLE Device'; + const protocolHint = inferProtocolHintFromDeviceName(displayName); + if (protocolHint) { + this.deviceProtocolHints.set(device.id, protocolHint); + } + deviceList.push({ + ...device, + name: displayName, + commType: 'ble', + } as IOneKeyDevice); + Log?.debug('[ReactNativeBleTransport] OneKey BLE device discovered', { + deviceId: device.id, + name: displayName, + serviceUUIDs: device.serviceUUIDs, + protocolHint, + }); } }; @@ -432,25 +634,40 @@ export default class ReactNativeBleTransport { } async acquire(input: BleAcquireInput) { - const { uuid, forceCleanRunPromise } = input; + const { uuid, forceCleanRunPromise, expectedProtocol } = input; if (!uuid) { throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID); } - let device: Device | null = null; + const cachedTransport = transportCache[uuid]; + if (cachedTransport) { + const cachedProtocol = this.deviceProtocol.get(uuid); + const isCachedDeviceConnected = await cachedTransport.device.isConnected().catch(() => false); + if ( + isCachedDeviceConnected && + cachedProtocol && + (!expectedProtocol || cachedProtocol === expectedProtocol) + ) { + Log?.debug('[ReactNativeBleTransport] reuse cached BLE transport:', uuid, cachedProtocol); + return { uuid, protocolType: cachedProtocol }; + } - if (transportCache[uuid]) { /** - * If the transport is not released due to an exception operation - * it will be handled again here + * If the transport is not reusable due to a protocol mismatch or stale + * connection, clean it up before creating a new transport instance. */ - Log?.debug('transport not be released, will release: ', uuid); - await this.release(uuid); + Log?.debug('transport not reusable, will release: ', uuid); + await this.release(uuid, true); } + let device: Device | null = null; + if (forceCleanRunPromise && this.runPromise) { - this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise)); + const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise); + this.runPromise.reject(error); + this.rejectAllProtocolV2Frames(error); + this.runPromise = null; Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise); } @@ -462,11 +679,12 @@ export default class ReactNativeBleTransport { throw error; } - // check device is bonded if (Platform.OS === 'android') { const bondState = await pairDevice(uuid); if (bondState.bonding) { await onDeviceBondState(uuid); + } else if (!bondState.bonded) { + throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded, 'device is not bonded'); } } @@ -492,7 +710,6 @@ export default class ReactNativeBleTransport { e.errorCode === BleErrorCode.DeviceMTUChangeFailed || e.errorCode === BleErrorCode.OperationCancelled ) { - connectOptions = {}; Log?.debug('first try to reconnect without params'); device = await blePlxManager.connectToDevice(uuid); } else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) { @@ -512,17 +729,16 @@ export default class ReactNativeBleTransport { Log?.debug('not connected, try to connect to device: ', uuid); try { - await device.connect(connectOptions); + device = await device.connect(connectOptions); } catch (e) { Log?.debug('not connected, try to connect to device has error: ', e); if ( e.errorCode === BleErrorCode.DeviceMTUChangeFailed || e.errorCode === BleErrorCode.OperationCancelled ) { - connectOptions = {}; Log?.debug('second try to reconnect without params'); try { - await device.connect(); + device = await device.connect(); } catch (e) { Log?.debug('last try to reconnect error: ', e); // last try to reconnect device if this issue exists @@ -530,7 +746,7 @@ export default class ReactNativeBleTransport { if (e.errorCode === BleErrorCode.OperationCancelled) { Log?.debug('last try to reconnect'); await device.cancelConnection(); - await device.connect(); + device = await device.connect(); } } } else { @@ -539,18 +755,50 @@ export default class ReactNativeBleTransport { } } + device = await requestAndroidMtu(device); const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(device); + const protocolHint = expectedProtocol + ? undefined + : this.deviceProtocolHints.get(uuid) ?? + inferProtocolHintFromDeviceName(getDeviceDisplayName(device)); + // release transport before new transport instance - await this.release(uuid); + await this.release(uuid, true); + if (protocolHint) { + this.deviceProtocolHints.set(uuid, protocolHint); + } const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic); + if (Platform.OS === 'android') { + transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize; + } + const monitorToken = this.nextMonitorToken; + this.nextMonitorToken += 1; + const notifyTransactionId = `${uuid}:notify:${monitorToken}`; + transport.monitorToken = monitorToken; + transport.notifyTransactionId = notifyTransactionId; + this.monitorTokens.set(uuid, monitorToken); transport.notifySubscription = this._monitorCharacteristic( transport.notifyCharacteristic, - uuid + uuid, + monitorToken, + notifyTransactionId ); transportCache[uuid] = transport; + this.protocolV2Assemblers.set(uuid, new ProtocolV2FrameAssembler()); + + if (Platform.OS === 'ios') { + await new Promise(resolve => { + setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS); + }); + } else if (Platform.OS === 'android') { + await delay(ANDROID_NOTIFY_READY_DELAY_MS); + } + + const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint); + this.emitter?.emit('device-connect', { name: device.name, id: device.id, @@ -559,13 +807,19 @@ export default class ReactNativeBleTransport { this.attachDisconnectSubscription(transport, device, uuid); - return { uuid }; + return { uuid, protocolType }; } - _monitorCharacteristic(characteristic: Characteristic, uuid: string): Subscription { + _monitorCharacteristic( + characteristic: Characteristic, + uuid: string, + monitorToken: number, + notifyTransactionId: string + ): Subscription { let bufferLength = 0; let buffer: any[] = []; const subscription = characteristic.monitor((error, c) => { + const isCurrentMonitor = this.monitorTokens.get(uuid) === monitorToken; if (error) { Log?.debug( `error monitor ${characteristic.uuid}, deviceId: ${characteristic.deviceID}: ${ @@ -576,6 +830,33 @@ export default class ReactNativeBleTransport { Log?.debug('notify error ignored during FirmwareUpload write recovery: ', uuid); return; } + if (!isCurrentMonitor) { + Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId); + return; + } + if (this.deviceProtocol.get(uuid) === 'V2') { + let errorCode: + | typeof HardwareErrorCode.BleDeviceBondError + | typeof HardwareErrorCode.BleCharacteristicNotifyError + | typeof HardwareErrorCode.BleCharacteristicNotifyChangeFailure + | typeof HardwareErrorCode.BleTimeoutError = + HardwareErrorCode.BleCharacteristicNotifyError; + if (error.reason?.includes('The connection has timed out unexpectedly')) { + errorCode = HardwareErrorCode.BleTimeoutError; + } else if (error.reason?.includes('Encryption is insufficient')) { + errorCode = HardwareErrorCode.BleDeviceBondError; + } else if ( + error.reason?.includes('Cannot write client characteristic config descriptor') || + error.reason?.includes('Cannot find client characteristic config descriptor') || + error.reason?.includes('The handle is invalid') || + error.reason?.includes('Writing is not permitted') || + error.reason?.includes('notify change failed for device') + ) { + errorCode = HardwareErrorCode.BleCharacteristicNotifyChangeFailure; + } + this.rejectProtocolV2Frames(uuid, ERRORS.TypedError(errorCode)); + return; + } if (this.runPromise) { let ERROR: | typeof HardwareErrorCode.BleDeviceBondError @@ -595,27 +876,45 @@ export default class ReactNativeBleTransport { error.reason?.includes('Writing is not permitted') || // pro firmware 2.3.4 upgrade error.reason?.includes('notify change failed for device') ) { - this.runPromise.reject( - ERRORS.TypedError(HardwareErrorCode.BleCharacteristicNotifyChangeFailure) + const notifyError = ERRORS.TypedError( + HardwareErrorCode.BleCharacteristicNotifyChangeFailure ); + this.runPromise.reject(notifyError); + this.rejectAllProtocolV2Frames(notifyError); Log?.debug( `${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}` ); return; } - this.runPromise.reject(ERRORS.TypedError(ERROR)); + const notifyError = ERRORS.TypedError(ERROR); + this.runPromise.reject(notifyError); + this.rejectAllProtocolV2Frames(notifyError); Log?.debug(': monitor notify error, and has unreleased Promise', Error); } return; } + if (!isCurrentMonitor) { + Log?.debug('monitor data ignored for stale transport: ', uuid, notifyTransactionId); + return; + } + if (!c) { throw ERRORS.TypedError(HardwareErrorCode.BleMonitorError); } try { const data = Buffer.from(c.value as string, 'base64'); + const protocol = this.deviceProtocol.get(uuid); + if (!protocol) { + Log?.debug('monitor data ignored before protocol detection: ', uuid); + return; + } + if (protocol === 'V2') { + this.handleProtocolV2Notification(uuid, monitorToken, new Uint8Array(data)); + return; + } // console.log('[hd-transport-react-native] Received a packet, ', 'buffer: ', data); if (isHeaderChunk(data)) { bufferLength = data.readInt32BE(5); @@ -624,7 +923,7 @@ export default class ReactNativeBleTransport { buffer = buffer.concat([...data]); } - if (buffer.length - COMMON_HEADER_SIZE >= bufferLength) { + if (buffer.length - PROTOCOL_V1_MESSAGE_HEADER_SIZE >= bufferLength) { const value = Buffer.from(buffer); // console.log( // '[hd-transport-react-native] Received a complete packet of data, resolve Promise, this.runPromise: ', @@ -638,17 +937,41 @@ export default class ReactNativeBleTransport { } } catch (error) { Log?.debug('monitor data error: ', error); - this.runPromise?.reject(ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError)); + const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError); + if (this.deviceProtocol.get(uuid) === 'V2') { + this.rejectProtocolV2Frames(uuid, notifyError); + } else { + this.runPromise?.reject(notifyError); + } } - }, uuid); + }, notifyTransactionId); return subscription; } - async release(uuid: string) { + async release(uuid: string, onclose = false) { const transport = transportCache[uuid]; + await this.protocolV2Links.invalidateLink(uuid, 'React Native BLE transport released'); + if (this.runPromise) { + const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise); + this.runPromise.reject(error); + this.runPromise = null; + this.rejectAllProtocolV2Frames(error); + } else { + this.resetProtocolV2Frames(uuid); + } + + if (Platform.OS === 'android' && !onclose && transport) { + this.protocolV2Assemblers.get(uuid)?.reset(); + this.resetProtocolV2Frames(uuid); + return Promise.resolve(true); + } if (transport) { + if (this.monitorTokens.get(uuid) === transport.monitorToken) { + this.monitorTokens.delete(uuid); + } + // Clean up disconnect subscription first to prevent callbacks on released transport Log?.debug('release: removing disconnect subscription for device: ', uuid); transport.disconnectSubscription?.remove(); @@ -662,12 +985,27 @@ export default class ReactNativeBleTransport { transport.notifySubscription?.remove(); transport.notifySubscription = undefined; + if (transport.notifyTransactionId) { + try { + await this.blePlxManager?.cancelTransaction(transport.notifyTransactionId); + } catch (e) { + Log?.debug('release: cancel notify transaction error (ignored): ', e?.message || e); + } + } + delete transportCache[uuid]; + } - // Temporary close the Android disconnect after each request - if (Platform.OS === 'android') { - // await this.blePlxManager?.cancelDeviceConnection(uuid); - } + this.deviceProtocol.delete(uuid); + // 设备名称提示不依赖当前连接;保留它可让重连优先探测 V2。 + this.protocolV2Assemblers.get(uuid)?.reset(); + this.protocolV2Assemblers.delete(uuid); + this.resetProtocolV2Frames(uuid); + + try { + await this.blePlxManager?.cancelTransaction(uuid); + } catch (e) { + Log?.debug('release: cancel transaction error (ignored): ', e?.message || e); } return Promise.resolve(true); @@ -677,7 +1015,12 @@ export default class ReactNativeBleTransport { await this.call(session, name, data); } - async call(uuid: string, name: string, data: Record) { + async call( + uuid: string, + name: string, + data: Record, + options?: TransportCallOptions + ) { if (this.stopped) { // eslint-disable-next-line prefer-promise-reject-errors return Promise.reject(ERRORS.TypedError('Transport stopped.')); @@ -686,33 +1029,44 @@ export default class ReactNativeBleTransport { throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); } - const forceRun = name === 'Initialize' || name === 'Cancel'; + const protocol = this.getProtocolType(uuid); + if (!protocol) { + throw ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + `Device protocol has not been detected for ${uuid}` + ); + } + Log?.debug('transport call', createTransportCallLog(name, protocol, data)); + + if (protocol === 'V2') { + return this.callProtocolV2(uuid, name, data, options); + } - Log?.debug('transport-react-native call this.runPromise', this.runPromise); + const forceRun = name === 'Initialize' || name === 'Cancel'; if (this.runPromise && !forceRun) { throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress); } - const transport = transportCache[uuid]; - if (!transport) { - throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound); - } + return this.callProtocolV1(uuid, name, data, options); + } - this.runPromise = createDeferred(); - const messages = this._messages; - // Upload resources on low-end phones may OOM - if (name === 'ResourceUpdate' || name === 'ResourceAck') { - Log?.debug('transport-react-native', 'call-', ' name: ', name, ' data: ', { - file_name: data?.file_name, - hash: data?.hash, - }); - } else if (LogBlockCommand.has(name)) { - Log?.debug('transport-react-native', 'call-', ' name: ', name); - } else { - Log?.debug('transport-react-native', 'call-', ' name: ', name, ' data: ', data); + private async callProtocolV1( + uuid: string, + name: string, + data: Record, + options?: TransportCallOptions + ) { + if (!this._messages) { + throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); } - const buffers = buildBuffers(messages, name, data); + const transport = this.getCachedTransport(uuid); + const runPromise = createDeferred(); + runPromise.promise.catch(() => undefined); + this.runPromise = runPromise; + const messages = this._messages; + const buffers = ProtocolV1.encodeTransportPackets(messages, name, data); + let timeout: ReturnType | undefined; async function writeChunkedData( buffers: ByteBuffer[], @@ -786,14 +1140,13 @@ export default class ReactNativeBleTransport { } ); } else if (name === 'FirmwareUpload') { - Log?.debug('[ReactNativeBleTransport] FirmwareUpload write uses throttled BLE packets:', { + Log?.debug('[ReactNativeBleTransport] Firmware upload transport configured', { packetCapacity: FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY, burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE, pauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS, flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS, maxRetries: FIRMWARE_UPLOAD_WRITE_MAX_RETRIES, }); - await writeFirmwareUploadChunkedData( buffers, async data => { @@ -847,7 +1200,6 @@ export default class ReactNativeBleTransport { for (const o of buffers) { const outData = o.toString('base64'); // Upload resources on low-end phones may OOM - // this.Log.debug('send hex strting: ', o.toString('hex')); try { await transport.writeCharacteristic.writeWithoutResponse(outData); } catch (e) { @@ -865,20 +1217,40 @@ export default class ReactNativeBleTransport { } try { - const response = await this.runPromise.promise; + const response = await Promise.race([ + runPromise.promise, + new Promise((_, reject) => { + if (options?.timeoutMs) { + timeout = setTimeout(() => { + const error = ERRORS.TypedError( + HardwareErrorCode.BleTimeoutError, + `BLE response timeout after ${options.timeoutMs}ms for ${name}` + ); + runPromise.reject(error); + reject(error); + }, options.timeoutMs); + } + }), + ]); if (typeof response !== 'string') { throw new Error('Returning data is not string.'); } - Log?.debug('receive data: ', response); - const jsonData = receiveOne(messages, response); + const jsonData = ProtocolV1.decodeMessage(messages, response); return check.call(jsonData); } catch (e) { - Log?.error('call error: ', e); + if (name === 'Initialize' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) { + Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe call failed:', e); + } else { + Log?.error('call error: ', e); + } throw e; } finally { - this.runPromise = null; + if (timeout) clearTimeout(timeout); + if (this.runPromise === runPromise) { + this.runPromise = null; + } } } @@ -887,7 +1259,7 @@ export default class ReactNativeBleTransport { } async disconnect(session: string) { - Log?.debug('transport-react-native transport resetSession: ', session); + await this.protocolV2Links.invalidateLink(session, 'React Native BLE transport disconnected'); const transport = transportCache[session]; // Clean up disconnect subscription first to prevent onDisconnected callback @@ -945,6 +1317,10 @@ export default class ReactNativeBleTransport { if (transportCache[session]) { delete transportCache[session]; } + this.deviceProtocol.delete(session); + this.deviceProtocolHints.delete(session); + this.protocolV2Assemblers.delete(session); + this.resetProtocolV2Frames(session); // emit the disconnect event try { @@ -967,4 +1343,370 @@ export default class ReactNativeBleTransport { } this.runPromise = null; } + + private getCachedTransport(uuid: string) { + const transport = transportCache[uuid]; + if (!transport) { + throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound); + } + return transport; + } + + private createProtocolMismatchError(expected: ProtocolType) { + return ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol` + ); + } + + private createProtocolDetectionError() { + return ERRORS.TypedError( + HardwareErrorCode.BleTimeoutError, + 'Unable to detect BLE protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping' + ); + } + + private clearProbeProtocol(uuid: string, protocol: ProtocolType) { + if (this.deviceProtocol.get(uuid) === protocol) { + this.deviceProtocol.delete(uuid); + } + } + + private async detectProtocol( + uuid: string, + expectedProtocol?: ProtocolType, + protocolHint?: ProtocolType + ): Promise { + if (expectedProtocol === 'V1') { + if (await this.probeProtocolV1(uuid)) { + this.deviceProtocol.set(uuid, 'V1'); + Log?.debug('[ReactNativeBleTransport] protocol detected', { + deviceId: uuid, + protocol: 'V1', + source: 'expected', + }); + return 'V1'; + } + throw this.createProtocolMismatchError(expectedProtocol); + } + + if (expectedProtocol === 'V2') { + // 免探测路径:调用方显式承诺该设备是 V2(例如固件升级重启后的重连场景, + // 上层已经探测过协议并通过 expectedProtocol 传回),这里不再重复探测。 + this.deviceProtocol.set(uuid, 'V2'); + Log?.debug('[ReactNativeBleTransport] protocol detected', { + deviceId: uuid, + protocol: 'V2', + source: 'expected', + }); + return 'V2'; + } + + // 项目约束:协议判断必须在连接后主动探测,不能依赖设备名/PID/descriptor。 + // 设备名 hint(如 "Pro 2")只用于调整探测顺序:hint=V2 时先探 V2、失败回落 V1, + // 不能作为最终结论。 + const probeOrder: ProtocolType[] = + protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2']; + + for (let i = 0; i < probeOrder.length; i += 1) { + const protocol = probeOrder[i]; + if (i > 0) { + // 上一个协议探测失败后,重置订阅与缓冲,避免残留数据干扰下一个协议的探测。 + await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]); + } + const detected = + protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid); + if (detected) { + this.deviceProtocol.set(uuid, protocol); + Log?.debug('[ReactNativeBleTransport] protocol detected', { + deviceId: uuid, + protocol, + source: 'probe', + }); + return protocol; + } + } + + this.deviceProtocol.delete(uuid); + throw this.createProtocolDetectionError(); + } + + private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) { + const transport = transportCache[uuid]; + await this.protocolV2Links.invalidateLink( + uuid, + `Reset notify state after Protocol ${protocol} probe` + ); + this.protocolV2Assemblers.get(uuid)?.reset(); + this.resetProtocolV2Frames(uuid); + if (this.runPromise) { + const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise); + this.runPromise.reject(error); + this.runPromise = null; + } + + if (!transport) return; + + const previousNotifyTransactionId = transport.notifyTransactionId; + if (this.monitorTokens.get(uuid) === transport.monitorToken) { + this.monitorTokens.delete(uuid); + } + transport.notifySubscription?.remove(); + transport.notifySubscription = undefined; + if (previousNotifyTransactionId) { + try { + await this.blePlxManager?.cancelTransaction(previousNotifyTransactionId); + } catch (error) { + Log?.debug( + `[ReactNativeBleTransport] cancel notify after Protocol ${protocol} probe failed:`, + error?.message || error + ); + } + } + + const monitorToken = this.nextMonitorToken; + this.nextMonitorToken += 1; + const notifyTransactionId = `${uuid}:notify:${monitorToken}`; + transport.monitorToken = monitorToken; + transport.notifyTransactionId = notifyTransactionId; + this.monitorTokens.set(uuid, monitorToken); + transport.notifySubscription = this._monitorCharacteristic( + transport.notifyCharacteristic, + uuid, + monitorToken, + notifyTransactionId + ); + if (Platform.OS === 'ios') { + await new Promise(resolve => { + setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS); + }); + } + } + + private async probeProtocolV1(uuid: string) { + if (!this._messages) { + return false; + } + + try { + this.deviceProtocol.set(uuid, 'V1'); + await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS }); + return true; + } catch (error) { + this.clearProbeProtocol(uuid, 'V1'); + Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe failed:', error); + return false; + } + } + + private async probeProtocolV2(uuid: string) { + if (!this._messages || !this._messagesV2) { + return false; + } + + this.deviceProtocol.set(uuid, 'V2'); + this.protocolV2Assemblers.get(uuid)?.reset(); + const detected = await probeProtocolV2Helper({ + call: (name: string, data: Record, options?: TransportCallOptions) => + this.callProtocolV2(uuid, name, data, options), + timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT_MS, + logger: Log, + logPrefix: 'ProtocolV2 RN-BLE', + onProbeFailed: () => { + this.protocolV2Assemblers.get(uuid)?.reset(); + this.resetProtocolV2Frames(uuid); + }, + }); + if (!detected) { + this.clearProbeProtocol(uuid, 'V2'); + } + return detected; + } + + private handleProtocolV2Notification(uuid: string, monitorToken: number, data: Uint8Array) { + try { + if (this.monitorTokens.get(uuid) !== monitorToken) return; + + if (data.length === 0) return; + + const assembler = this.protocolV2Assemblers.get(uuid); + if (!assembler) return; + + for (const frameData of assembler.drain(data)) { + this.resolveProtocolV2Frame(uuid, frameData); + } + } catch (error) { + Log?.debug('[ReactNativeBleTransport] Protocol V2 notification error:', error); + const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError); + this.rejectProtocolV2Frames(uuid, notifyError); + this.protocolV2Links + .invalidateLink(uuid, `Protocol V2 notification error: ${error}`) + .catch(invalidateError => + Log?.debug( + '[ReactNativeBleTransport] Protocol V2 notify cleanup failed:', + invalidateError + ) + ); + } + } + + private getProtocolV2FrameQueue(uuid: string) { + let queue = this.protocolV2FrameQueues.get(uuid); + if (!queue) { + queue = []; + this.protocolV2FrameQueues.set(uuid, queue); + } + return queue; + } + + private resolveProtocolV2Frame(uuid: string, frame: Uint8Array) { + const framePromise = this.protocolV2FramePromises.get(uuid); + if (framePromise) { + framePromise.resolve(frame); + this.protocolV2FramePromises.delete(uuid); + return; + } + this.getProtocolV2FrameQueue(uuid).push(frame); + } + + private rejectAllProtocolV2Frames(error: Error) { + this.protocolV2FrameQueues.clear(); + for (const framePromise of this.protocolV2FramePromises.values()) { + framePromise.reject(error); + } + this.protocolV2FramePromises.clear(); + } + + private resetProtocolV2Frames(uuid: string) { + this.protocolV2FrameQueues.delete(uuid); + this.protocolV2FramePromises.delete(uuid); + } + + private rejectProtocolV2Frames(uuid: string, error: Error) { + this.protocolV2FrameQueues.delete(uuid); + const framePromise = this.protocolV2FramePromises.get(uuid); + if (framePromise) { + this.protocolV2FramePromises.delete(uuid); + framePromise.reject(error); + } + } + + private async readProtocolV2Frame(uuid: string) { + const queuedFrame = this.getProtocolV2FrameQueue(uuid).shift(); + if (queuedFrame) { + return queuedFrame; + } + + const framePromise = createDeferred(); + this.protocolV2FramePromises.set(uuid, framePromise); + try { + return await framePromise.promise; + } finally { + if (this.protocolV2FramePromises.get(uuid) === framePromise) { + this.protocolV2FramePromises.delete(uuid); + } + } + } + + private async writeProtocolV2Frame(transport: BleTransport, frame: Uint8Array) { + const tuning = getProtocolV2BleTuning(); + const packetCapacity = resolveProtocolV2PacketCapacity({ + platform: Platform.OS, + iosPacketLength: tuning.iosPacketLength, + androidPacketLength: tuning.androidPacketLength, + mtu: Platform.OS === 'android' ? transport.mtuSize : undefined, + }); + for (let offset = 0; offset < frame.length; offset += packetCapacity) { + const chunk = frame.slice(offset, offset + packetCapacity); + const base64 = Buffer.from(chunk).toString('base64'); + await transport.writeCharacteristic.writeWithoutResponse(base64); + } + } + + private async callProtocolV2( + uuid: string, + name: string, + data: Record, + options?: TransportCallOptions + ) { + if (!this._messages || !this._messagesV2) { + throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); + } + + const callOptions = { + ...options, + timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS, + }; + const highVolumeWrite = LogBlockCommand.has(name); + + if (highVolumeWrite) { + const tuning = getProtocolV2BleTuning(); + Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', { + name, + writeMode: 'withoutResponse', + packetCapacity: Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength, + }); + } + + try { + return await this.protocolV2Links.call( + uuid, + () => this.createProtocolV2Adapter(uuid), + name, + data, + callOptions + ); + } catch (e) { + Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e); + throw e; + } + } + + private createProtocolV2Adapter(uuid: string) { + const generation = this.monitorTokens.get(uuid) ?? 0; + const assertCurrentGeneration = () => { + if (this.monitorTokens.get(uuid) !== generation) { + throw new Error(`Protocol V2 monitor generation changed for ${uuid}`); + } + }; + + return { + router: PROTOCOL_V2_CHANNEL_BLE_UART, + maxFrameBytes: PROTOCOL_V2_BLE_FRAME_MAX_BYTES, + generation, + prepareCall: () => { + assertCurrentGeneration(); + this.protocolV2Assemblers.get(uuid)?.reset(); + this.resetProtocolV2Frames(uuid); + }, + writeFrame: async (frame: Uint8Array) => { + assertCurrentGeneration(); + const currentTransport = this.getCachedTransport(uuid); + await this.writeProtocolV2Frame(currentTransport, frame); + }, + readFrame: async () => { + assertCurrentGeneration(); + const rxFrame = await this.readProtocolV2Frame(uuid); + if (!(rxFrame instanceof Uint8Array)) { + throw new Error('Protocol V2 response is not Uint8Array'); + } + return rxFrame; + }, + reset: (reason: string) => { + this.protocolV2Assemblers.get(uuid)?.reset(); + this.rejectProtocolV2Frames(uuid, new Error(reason)); + }, + logger: Log, + logPrefix: 'ProtocolV2 RN-BLE', + createTimeoutError: (messageName: string, timeout: number) => + ERRORS.TypedError( + HardwareErrorCode.BleTimeoutError, + `BLE response timeout after ${timeout}ms for ${messageName}` + ), + }; + } + + getProtocolType(path: string): ProtocolType | undefined { + return this.deviceProtocol.get(path); + } } diff --git a/packages/hd-transport-react-native/src/logger.ts b/packages/hd-transport-react-native/src/logger.ts new file mode 100644 index 000000000..f4beeb42f --- /dev/null +++ b/packages/hd-transport-react-native/src/logger.ts @@ -0,0 +1,19 @@ +type LogMethod = (...args: unknown[]) => void; + +type TransportLogger = { + debug?: LogMethod; + error?: LogMethod; + warn?: LogMethod; +}; + +let activeLogger: TransportLogger | undefined; + +export const setBleLogger = (logger?: TransportLogger) => { + activeLogger = logger; +}; + +export const bleLogger = { + debug: (...args: unknown[]) => activeLogger?.debug?.(...args), + error: (...args: unknown[]) => activeLogger?.error?.(...args), + warn: (...args: unknown[]) => activeLogger?.warn?.(...args), +}; diff --git a/packages/hd-transport-react-native/src/subscribeBleOn.ts b/packages/hd-transport-react-native/src/subscribeBleOn.ts index 8d0e51f4e..71897711d 100644 --- a/packages/hd-transport-react-native/src/subscribeBleOn.ts +++ b/packages/hd-transport-react-native/src/subscribeBleOn.ts @@ -9,8 +9,6 @@ export const subscribeBleOn = (bleManager: BlePlxManager, ms = 1000): Promise { - console.log('ble state -> ', state); - if (state === 'PoweredOn') { if (done) return; clearTimeout(); diff --git a/packages/hd-transport-react-native/src/transportLog.ts b/packages/hd-transport-react-native/src/transportLog.ts new file mode 100644 index 000000000..d267ad42c --- /dev/null +++ b/packages/hd-transport-react-native/src/transportLog.ts @@ -0,0 +1,18 @@ +import type { ProtocolType } from '@onekeyfe/hd-transport'; + +export function createTransportCallLog( + name: string, + protocol: ProtocolType, + data: Record +) { + if (name === 'ResourceUpdate' || name === 'ResourceAck') { + return { + name, + protocol, + file_name: data.file_name, + hash: data.hash, + }; + } + + return { name, protocol }; +} diff --git a/packages/hd-transport-react-native/src/types.ts b/packages/hd-transport-react-native/src/types.ts index b5a933b14..3f5346c36 100644 --- a/packages/hd-transport-react-native/src/types.ts +++ b/packages/hd-transport-react-native/src/types.ts @@ -1,3 +1,5 @@ +import type { ProtocolType } from '@onekeyfe/hd-transport'; + export type { BleManager as BlePlxManager } from 'react-native-ble-plx'; export type TransportOptions = { @@ -7,4 +9,5 @@ export type TransportOptions = { export type BleAcquireInput = { uuid: string; forceCleanRunPromise?: boolean; + expectedProtocol?: ProtocolType; }; diff --git a/packages/hd-transport-react-native/src/utils/validateNotify.ts b/packages/hd-transport-react-native/src/utils/validateNotify.ts index 2d3d42960..0013ea163 100644 --- a/packages/hd-transport-react-native/src/utils/validateNotify.ts +++ b/packages/hd-transport-react-native/src/utils/validateNotify.ts @@ -1,13 +1,13 @@ -import { MESSAGE_HEADER_BYTE, MESSAGE_TOP_CHAR } from '@onekeyfe/hd-transport'; +import { PROTOCOL_V1_HEADER_BYTE, PROTOCOL_V1_REPORT_ID } from '@onekeyfe/hd-transport'; export const isHeaderChunk = (chunk: Buffer): boolean => { if (chunk.length < 9) return false; const [MagicQuestionMark, sharp1, sharp2] = chunk; if ( - String.fromCharCode(MagicQuestionMark) === String.fromCharCode(MESSAGE_TOP_CHAR) && - String.fromCharCode(sharp1) === String.fromCharCode(MESSAGE_HEADER_BYTE) && - String.fromCharCode(sharp2) === String.fromCharCode(MESSAGE_HEADER_BYTE) + String.fromCharCode(MagicQuestionMark) === String.fromCharCode(PROTOCOL_V1_REPORT_ID) && + String.fromCharCode(sharp1) === String.fromCharCode(PROTOCOL_V1_HEADER_BYTE) && + String.fromCharCode(sharp2) === String.fromCharCode(PROTOCOL_V1_HEADER_BYTE) ) { return true; } diff --git a/packages/hd-transport-usb/__tests__/protocol-v2-link.test.ts b/packages/hd-transport-usb/__tests__/protocol-v2-link.test.ts new file mode 100644 index 000000000..63edcf209 --- /dev/null +++ b/packages/hd-transport-usb/__tests__/protocol-v2-link.test.ts @@ -0,0 +1,260 @@ +import transportPackage, { PROTOCOL_V2_CHANNEL_USB, ProtocolV2 } from '@onekeyfe/hd-transport'; + +import NodeUsbTransport from '../src'; + +let mockUsbDevices: any[] = []; + +jest.mock('usb', () => ({ + getDeviceList: jest.fn(() => mockUsbDevices), +})); + +const { parseConfigure } = transportPackage; + +const protocolV1Schema = { + nested: { + Initialize: { fields: {} }, + Success: { + fields: { + message: { type: 'string', id: 1 }, + }, + }, + MessageType: { + values: { + MessageType_Initialize: 1, + MessageType_Success: 2, + }, + }, + }, +}; + +const protocolV2Schema = { + nested: { + Ping: { + fields: { + message: { type: 'string', id: 1 }, + }, + }, + Success: { + fields: { + message: { type: 'string', id: 1 }, + }, + }, + MessageType: { + values: { + MessageType_Ping: 60206, + MessageType_Success: 60207, + }, + }, + }, +}; + +const schemas = { + protocolV1: parseConfigure(protocolV1Schema), + protocolV2: parseConfigure(protocolV2Schema), +}; + +type PendingRead = { + started: Promise; + fail: (error?: Error) => void; +}; + +const createHarness = () => { + const path = '6136'; + const responseQueue: Buffer[] = []; + const sentSeqs: number[] = []; + let writeError: Error | undefined; + let holdNextRead: + | { + markStarted: () => void; + started: Promise; + callback?: (error?: Error, data?: Buffer) => void; + } + | undefined; + + const epIn = { + direction: 'in', + address: 0x81, + timeout: 30_000, + transfer: jest.fn((_length: number, callback: (error?: Error, data?: Buffer) => void) => { + if (epIn.timeout === 50) { + callback(new Error('LIBUSB_TRANSFER_TIMED_OUT')); + return; + } + if (holdNextRead) { + const pending = holdNextRead; + holdNextRead = undefined; + pending.callback = callback; + pending.markStarted(); + return; + } + const response = responseQueue.shift(); + if (!response) { + callback(new Error('LIBUSB_TRANSFER_TIMED_OUT')); + return; + } + callback(undefined, response); + }), + }; + + const epOut = { + direction: 'out', + address: 0x01, + timeout: 30_000, + transfer: jest.fn((data: Buffer, callback: (error?: Error) => void) => { + const seq = data[6]; + sentSeqs.push(seq); + if (writeError) { + const error = writeError; + writeError = undefined; + callback(error); + return; + } + responseQueue.push( + Buffer.from( + ProtocolV2.encodeFrame( + schemas, + 'Success', + { message: 'ok' }, + { router: PROTOCOL_V2_CHANNEL_USB, seq } + ) + ) + ); + callback(); + }), + }; + + const iface = { + descriptor: { bInterfaceClass: 0xff, bInterfaceNumber: 0 }, + endpoints: [epIn, epOut], + claim: jest.fn(), + release: jest.fn((callback: () => void) => callback()), + isKernelDriverActive: jest.fn(() => false), + detachKernelDriver: jest.fn(), + }; + + const device = { + busNumber: 1, + deviceAddress: 2, + timeout: 30_000, + deviceDescriptor: { + idVendor: 0x1209, + idProduct: 0x4f4a, + iSerialNumber: 1, + }, + interfaces: [iface], + open: jest.fn(), + close: jest.fn(), + getStringDescriptor: jest.fn( + (_index: number, callback: (error?: Error, value?: string) => void) => + callback(undefined, path) + ), + }; + + mockUsbDevices = [device]; + const transport = new NodeUsbTransport(); + transport.init({ debug: jest.fn(), error: jest.fn() }); + transport.configure(protocolV1Schema); + transport.configureProtocolV2(protocolV2Schema); + + return { + transport, + path, + device, + iface, + epIn, + epOut, + sentSeqs, + async acquire() { + await transport.enumerate(); + await transport.acquire({ path, expectedProtocol: 'V2' }); + }, + failNextWrite(error: Error) { + writeError = error; + }, + holdRead(): PendingRead { + let markStarted: () => void = () => undefined; + const started = new Promise(resolve => { + markStarted = resolve; + }); + const pending = { markStarted, started, callback: undefined }; + holdNextRead = pending; + return { + started, + fail(error = new Error('read released after test')) { + pending.callback?.(error); + }, + }; + }, + }; +}; + +describe('NodeUsbTransport Protocol V2 link lifecycle', () => { + test('keeps seq across probe, call and reacquire', async () => { + const harness = createHarness(); + const { transport, path, sentSeqs } = harness; + + await harness.acquire(); + await transport.call(path, 'Ping', { message: 'first' }); + await transport.release(path); + await harness.acquire(); + await transport.call(path, 'Ping', { message: 'second' }); + + expect(sentSeqs).toEqual([1, 2, 3, 4]); + await transport.release(path); + }); + + test('does not resend a Protocol V2 frame after transferOut fails', async () => { + const harness = createHarness(); + const { transport, path, epOut } = harness; + await harness.acquire(); + epOut.transfer.mockClear(); + harness.failNextWrite(new Error('LIBUSB_ERROR_IO')); + + await expect(transport.call(path, 'Ping', { message: 'write-failure' })).rejects.toThrow( + 'LIBUSB_ERROR_IO' + ); + + expect(epOut.transfer).toHaveBeenCalledTimes(1); + }); + + test('rejects a pending read when release invalidates the link', async () => { + const harness = createHarness(); + const { transport, path } = harness; + await harness.acquire(); + const pendingRead = harness.holdRead(); + + const call = transport.call(path, 'Ping', { message: 'pending' }, { timeoutMs: 5000 }); + const outcome = call.then( + () => 'resolved', + error => error.message + ); + await pendingRead.started; + await transport.release(path); + const settled = await Promise.race([ + outcome, + new Promise(resolve => { + setTimeout(() => resolve('still pending'), 50); + }), + ]); + pendingRead.fail(); + + expect(settled).not.toBe('still pending'); + }); + + test('keeps the cursor after a response timeout rebuilds the USB connection', async () => { + const harness = createHarness(); + const { transport, path, sentSeqs } = harness; + await harness.acquire(); + const pendingRead = harness.holdRead(); + + await expect( + transport.call(path, 'Ping', { message: 'timeout' }, { timeoutMs: 20 }) + ).rejects.toThrow('20ms'); + pendingRead.fail(); + await harness.acquire(); + await transport.call(path, 'Ping', { message: 'after-timeout' }); + + expect(sentSeqs).toEqual([1, 2, 3, 4]); + await transport.release(path); + }); +}); diff --git a/packages/hd-transport-usb/src/constants.ts b/packages/hd-transport-usb/src/constants.ts deleted file mode 100644 index 5aee0ac18..000000000 --- a/packages/hd-transport-usb/src/constants.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** USB packet size in bytes */ -export const PACKET_SIZE = 64; - -/** Protocol marker byte (0x3F = '?') — first byte of every packet */ -export const REPORT_ID = 0x3f; - -/** Usable payload per packet after stripping the 0x3F report byte */ -export const PAYLOAD_SIZE = PACKET_SIZE - 1; - -/** Protocol header length: typeId (2 bytes) + length (4 bytes) */ -export const HEADER_LENGTH = 6; diff --git a/packages/hd-transport-usb/src/index.ts b/packages/hd-transport-usb/src/index.ts index 5e648efee..78bfa08b8 100644 --- a/packages/hd-transport-usb/src/index.ts +++ b/packages/hd-transport-usb/src/index.ts @@ -1,14 +1,35 @@ import ByteBuffer from 'bytebuffer'; import * as usb from 'usb'; -import transport, { LogBlockCommand } from '@onekeyfe/hd-transport'; +import transport, { + PROTOCOL_V1_CHUNK_PAYLOAD_SIZE, + PROTOCOL_V1_MESSAGE_HEADER_SIZE, + PROTOCOL_V1_REPORT_ID, + PROTOCOL_V1_USB_PACKET_SIZE, + PROTOCOL_V2_CHANNEL_USB, + PROTOCOL_V2_FRAME_MAX_BYTES, + ProtocolV2UsbTransportBase, + probeProtocolV2 as probeProtocolV2Helper, +} from '@onekeyfe/hd-transport'; import { ERRORS, HardwareErrorCode, ONEKEY_WEBUSB_FILTER, wait } from '@onekeyfe/hd-shared'; -import { HEADER_LENGTH, PACKET_SIZE, PAYLOAD_SIZE, REPORT_ID } from './constants'; +import { createTransportCallLog, shouldSuppressHighVolumeCallLog } from './transportLog'; import type EventEmitter from 'events'; -import type { AcquireInput, OneKeyDeviceInfo } from '@onekeyfe/hd-transport'; - -const { parseConfigure, buildEncodeBuffers, decodeProtocol, receiveOne, check } = transport; +import type { + AcquireInput, + OneKeyDeviceInfo, + ProtocolType, + ProtocolV2CallContext, + ProtocolV2Schemas, + TransportCallOptions, +} from '@onekeyfe/hd-transport'; + +const { parseConfigure, ProtocolV1, check } = transport; + +const PACKET_SIZE = PROTOCOL_V1_USB_PACKET_SIZE; +const REPORT_ID = PROTOCOL_V1_REPORT_ID; +const PAYLOAD_SIZE = PROTOCOL_V1_CHUNK_PAYLOAD_SIZE; +const HEADER_LENGTH = PROTOCOL_V1_MESSAGE_HEADER_SIZE; /** USB interface number for vendor-specific communication */ const INTERFACE_NUMBER = 0; @@ -25,6 +46,7 @@ const SERIAL_READ_TIMEOUT_MS = 5000; /** Packet I/O retry configuration (matches WebUsbTransport) */ const PACKET_IO_MAX_RETRIES = 3; const PACKET_IO_RETRY_DELAY = 300; +const PROTOCOL_PROBE_TIMEOUT = 5000; /** * Opened device state — holds the USB device, claimed interface, and endpoints. @@ -160,9 +182,12 @@ function toArrayBuffer(buf: Buffer): ArrayBuffer { * * Modeled after WebUsbTransport. */ -export default class NodeUsbTransport { +export default class NodeUsbTransport extends ProtocolV2UsbTransportBase { messages: ReturnType | undefined; + /** Protobuf schema for Protocol V2 transports. */ + messagesV2: ReturnType | undefined; + name = 'NodeUsbTransport'; version = ''; @@ -181,12 +206,23 @@ export default class NodeUsbTransport { /** path → opened device state */ private openDevices = new Map(); + /** Per-path protocol type detected by active wire-level probe. */ + private deviceProtocol: Map = new Map(); + /** per-path reconnect lock to prevent concurrent reconnects */ private reconnectLocks = new Map>(); /** set to true when cancel() is called; checked by retry loops */ private cancelled = false; + constructor() { + super({ + router: PROTOCOL_V2_CHANNEL_USB, + maxFrameBytes: PROTOCOL_V2_FRAME_MAX_BYTES, + logPrefix: 'ProtocolV2 NodeUSB', + }); + } + /** * Initialize transport. * Signature matches the Transport.init interface (logger, emitter). @@ -204,12 +240,21 @@ export default class NodeUsbTransport { return Promise.resolve(); } + configureProtocolV2(signedData: any) { + this.messagesV2 = parseConfigure(signedData); + this.invalidateAllProtocolV2UsbLinks('Protocol V2 schema reconfigured').catch(error => + this.Log?.debug('[NodeUsbTransport] schema link cleanup failed:', error) + ); + } + listen() { // empty — could add hotplug events via usb.on('attach'/'detach') } stop() { - // Placeholder — no background listeners to tear down + this.disposeProtocolV2UsbLinks('Node USB transport stopped').catch(error => + this.Log?.debug('[NodeUsbTransport] stop link cleanup failed:', error) + ); } /** @@ -220,7 +265,7 @@ export default class NodeUsbTransport { if (!this.messages) { throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); } - const encodeBuffers = buildEncodeBuffers(this.messages, name, data); + const encodeBuffers = ProtocolV1.encodeMessageChunks(this.messages, name, data); await this.sendAllChunksWithRetry(path, encodeBuffers); } @@ -237,7 +282,7 @@ export default class NodeUsbTransport { if (!this.messages) { throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); } - return receiveOne(this.messages, resData); + return ProtocolV1.decodeMessage(this.messages, resData); } /** @@ -275,15 +320,20 @@ export default class NodeUsbTransport { /** * Acquire device — open USB device, claim interface, return path (string). */ - acquire(input: AcquireInput): Promise { + async acquire(input: AcquireInput): Promise { + this.cancelled = false; + const path = input.path ?? ''; if (!path) { throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, 'No device path provided'); } try { - this.openDevice(path); - return Promise.resolve(path); + await this.rotateProtocolV2UsbGeneration(path, 'Node USB transport acquired'); + await this.closeOpenDevice(path); + await this.openDevice(path); + await this.detectProtocol(path, input.expectedProtocol); + return path; } catch (error: any) { this.Log?.debug('NodeUsbTransport acquire error: ', error); throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, error.message ?? String(error)); @@ -294,6 +344,12 @@ export default class NodeUsbTransport { * Release device — release interface and close. */ async release(path: string, _onclose?: boolean): Promise { + await this.invalidateProtocolV2UsbLink(path, 'Node USB transport released'); + await this.closeOpenDevice(path); + this.deviceProtocol.delete(path); + } + + private async closeOpenDevice(path: string): Promise { const openDev = this.openDevices.get(path); if (!openDev) return; @@ -322,7 +378,12 @@ export default class NodeUsbTransport { * Call device method — encode protobuf, send packets, receive response. * This is the core method that replaces LowlevelTransport's call + UsbPlugin's send/receive. */ - async call(path: string, name: string, data: Record) { + async call( + path: string, + name: string, + data: Record, + options?: TransportCallOptions + ) { this.cancelled = false; if (!this.messages) { @@ -333,31 +394,51 @@ export default class NodeUsbTransport { throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device not acquired: ${path}`); } - const { messages } = this; - if (LogBlockCommand.has(name)) { - this.Log?.debug('NodeUsbTransport call-', ' name: ', name); - } else { - this.Log?.debug('NodeUsbTransport call-', ' name: ', name, ' data: ', data); + const protocol = this.deviceProtocol.get(path); + if (!protocol) { + throw ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + `Device protocol has not been detected for ${path}` + ); + } + if (!shouldSuppressHighVolumeCallLog(name)) { + this.Log?.debug('transport call', createTransportCallLog(name, protocol)); + } + + if (protocol === 'V2') { + return this.callProtocolV2(path, name, data, options); } + return this.callProtocolV1(path, name, data, options); + } + + private async callProtocolV1( + path: string, + name: string, + data: Record, + options?: TransportCallOptions + ) { + const { messages } = this; + if (!messages) { + throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); + } // Encode protobuf message into 63-byte chunks (same as WebUsbTransport) - const encodeBuffers = buildEncodeBuffers(messages, name, data); + const encodeBuffers = ProtocolV1.encodeMessageChunks(messages, name, data); // Send all chunks with retry — if any chunk fails and reconnects, // restart the entire send sequence from chunk 0 (device resets state on reconnect) await this.sendAllChunksWithRetry(path, encodeBuffers); // Receive response — re-resolve in case reconnect happened during send - const resData = await this.receiveData(path, this.getOpenDevice(path)); + const resData = await this.receiveData(path, this.getOpenDevice(path), options?.timeoutMs); if (typeof resData !== 'string') { throw ERRORS.TypedError(HardwareErrorCode.NetworkError, 'Returning data is not string.'); } - const jsonData = receiveOne(messages, resData); + const jsonData = ProtocolV1.decodeMessage(messages, resData); return check.call(jsonData); } cancel() { - this.Log?.debug('NodeUsbTransport cancel'); this.cancelled = true; } @@ -401,6 +482,26 @@ export default class NodeUsbTransport { ); } + private isUsbTransferTimeout(error: unknown): boolean { + const message = this.getErrorMessage(error).toLowerCase(); + return message.includes('timeout') || message.includes('timed_out'); + } + + private getDeviceInterface(dev: usb.Device): usb.Interface { + const { interfaces } = dev; + if (!interfaces?.length) { + throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, 'USB interface not found'); + } + + const vendorInterface = interfaces.find(iface => iface.descriptor.bInterfaceClass === 0xff); + const defaultInterface = interfaces.find( + iface => iface.descriptor.bInterfaceNumber === INTERFACE_NUMBER + ); + const iface = vendorInterface ?? defaultInterface ?? interfaces[0]; + + return iface; + } + /** * Reconnect device before retrying a failed transfer (aligned with WebUsbTransport). * Uses per-path lock to prevent concurrent reconnects to the same device. @@ -423,16 +524,20 @@ export default class NodeUsbTransport { ); await wait(attempt * PACKET_IO_RETRY_DELAY); - // Close the existing device + await this.rotateProtocolV2UsbGeneration( + path, + `Node USB Protocol V1 ${direction} reconnect attempt ${attempt}` + ); + // Close the existing device without clearing the detected protocol cache. try { - await this.release(path); + await this.closeOpenDevice(path); } catch (releaseError) { this.Log?.debug('[NodeUsbTransport] release before retry error:', releaseError); } // Re-enumerate to refresh device list, then re-open await this.enumerate(); - this.openDevice(path); + await this.openDevice(path); const openDev = this.openDevices.get(path); if (!openDev) { @@ -500,7 +605,8 @@ export default class NodeUsbTransport { private async transferInWithRetry( path: string, openDev: OpenDevice, - length: number + length: number, + options?: { waitIndefinitelyOnTimeout?: boolean } ): Promise { let lastError: unknown; let currentDev = openDev; @@ -512,20 +618,24 @@ export default class NodeUsbTransport { return await transferInOnce(currentDev.epIn, length); } catch (error) { lastError = error; - const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error); - if (!shouldRetry) { - throw error; - } - try { - currentDev = await this.reconnectForRetry(path, 'in', attempt, error); - } catch (reconnectError) { - lastError = reconnectError; - this.Log?.debug( - `[NodeUsbTransport] reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage( - reconnectError - )}` - ); - break; + if (options?.waitIndefinitelyOnTimeout && this.isUsbTransferTimeout(error)) { + attempt -= 1; + } else { + const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryableError(error); + if (!shouldRetry) { + throw error; + } + try { + currentDev = await this.reconnectForRetry(path, 'in', attempt, error); + } catch (reconnectError) { + lastError = reconnectError; + this.Log?.debug( + `[NodeUsbTransport] reconnect failed on retry ${attempt}/${PACKET_IO_MAX_RETRIES}: ${this.getErrorMessage( + reconnectError + )}` + ); + break; + } } } } @@ -535,7 +645,7 @@ export default class NodeUsbTransport { /** * Open a USB device by path (serial number), claim interface, cache endpoints. */ - private openDevice(path: string): void { + private async openDevice(path: string): Promise { const existing = this.openDevices.get(path); if (existing) return; @@ -547,12 +657,11 @@ export default class NodeUsbTransport { throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `USB device not found: ${path}`); } - dev.open(); - try { + dev.open(); dev.timeout = TRANSFER_TIMEOUT_MS; - const iface = dev.interface(INTERFACE_NUMBER); + const iface = this.getDeviceInterface(dev); // On Linux, detach kernel driver if active if (process.platform === 'linux') { @@ -567,12 +676,14 @@ export default class NodeUsbTransport { iface.claim(); - const epIn = iface.endpoints.find( - (e): e is usb.InEndpoint => e.direction === 'in' && e.address === ENDPOINT_IN - ); - const epOut = iface.endpoints.find( - (e): e is usb.OutEndpoint => e.direction === 'out' && e.address === ENDPOINT_OUT - ); + const epIn = + iface.endpoints.find( + (e): e is usb.InEndpoint => e.direction === 'in' && e.address === ENDPOINT_IN + ) ?? iface.endpoints.find((e): e is usb.InEndpoint => e.direction === 'in'); + const epOut = + iface.endpoints.find( + (e): e is usb.OutEndpoint => e.direction === 'out' && e.address === ENDPOINT_OUT + ) ?? iface.endpoints.find((e): e is usb.OutEndpoint => e.direction === 'out'); if (!epIn || !epOut) { throw ERRORS.TypedError( @@ -584,6 +695,8 @@ export default class NodeUsbTransport { epIn.timeout = TRANSFER_TIMEOUT_MS; epOut.timeout = TRANSFER_TIMEOUT_MS; + await this.drainStaleInput(epIn); + this.openDevices.set(path, { device: dev, iface, epIn, epOut }); } catch (err) { try { @@ -595,17 +708,250 @@ export default class NodeUsbTransport { } } + private async drainStaleInput(epIn: usb.InEndpoint): Promise { + const originalTimeout = epIn.timeout; + epIn.timeout = 50; + try { + // Drain a small bounded number of packets left by the previous USB session. + for (let index = 0; index < 16; index += 1) { + try { + await transferInOnce(epIn, PACKET_SIZE); + } catch { + break; + } + } + } finally { + epIn.timeout = originalTimeout; + } + } + + private createProtocolMismatchError(expected: ProtocolType) { + return ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol` + ); + } + + private createProtocolDetectionError() { + return ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + 'Unable to detect USB protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping' + ); + } + + private async detectProtocol( + path: string, + expectedProtocol?: ProtocolType + ): Promise { + if (expectedProtocol === 'V1') { + if (await this.probeProtocolV1(path)) { + this.deviceProtocol.set(path, 'V1'); + return 'V1'; + } + throw this.createProtocolMismatchError(expectedProtocol); + } + + if (expectedProtocol === 'V2') { + if (await this.probeProtocolV2(path)) { + this.deviceProtocol.set(path, 'V2'); + return 'V2'; + } + throw this.createProtocolMismatchError(expectedProtocol); + } + + if (this.deviceProtocol.get(path) === 'V2' && (await this.probeProtocolV2(path))) { + this.deviceProtocol.set(path, 'V2'); + return 'V2'; + } + + if (await this.probeProtocolV1(path)) { + this.deviceProtocol.set(path, 'V1'); + return 'V1'; + } + + if (await this.probeProtocolV2(path)) { + this.deviceProtocol.set(path, 'V2'); + return 'V2'; + } + + this.deviceProtocol.delete(path); + throw this.createProtocolDetectionError(); + } + + private async resetConnectionAfterProbe(path: string) { + await this.rotateProtocolV2UsbGeneration(path, 'Node USB protocol probe reset'); + + try { + await this.closeOpenDevice(path); + } catch (error) { + this.Log?.debug('[NodeUsbTransport] close after protocol probe error:', error); + } + + await this.enumerate(); + await this.openDevice(path); + } + + private async withProtocolReadTimeout( + path: string, + promise: Promise, + timeoutMs: number, + protocol: ProtocolType + ): Promise { + let timer: ReturnType | undefined; + let timedOut = false; + const waitForeverAfterTimeout = () => new Promise(() => {}); + const guardedPromise = promise.then( + value => (timedOut ? waitForeverAfterTimeout() : value), + error => { + if (timedOut) { + return waitForeverAfterTimeout(); + } + throw error; + } + ); + try { + return await Promise.race([ + guardedPromise, + new Promise((_, reject) => { + timer = setTimeout(async () => { + timedOut = true; + try { + await this.resetConnectionAfterProbe(path); + } catch (error) { + this.Log?.debug( + `[NodeUsbTransport] reset after Protocol ${protocol} timeout failed:`, + error + ); + } finally { + reject(new Error(`Protocol ${protocol} read timeout after ${timeoutMs}ms`)); + } + }, timeoutMs); + }), + ]); + } finally { + if (timer) clearTimeout(timer); + } + } + + private async probeProtocolV1(path: string) { + if (!this.messages) { + return false; + } + + try { + await this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT }); + return true; + } catch (_error) { + return false; + } + } + + private async probeProtocolV2(path: string) { + if (!this.messages || !this.messagesV2) { + return false; + } + + return probeProtocolV2Helper({ + call: (name, data, options) => this.callProtocolV2(path, name, data, options), + timeoutMs: PROTOCOL_PROBE_TIMEOUT, + logger: this.Log, + logPrefix: 'ProtocolV2 NodeUSB', + onProbeFailed: () => this.resetConnectionAfterProbe(path), + }); + } + + protected getProtocolV2UsbSchemas(): ProtocolV2Schemas { + if (!this.messages || !this.messagesV2) { + throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); + } + return { + protocolV1: this.messages, + protocolV2: this.messagesV2, + }; + } + + protected getProtocolV2UsbLogger() { + return this.Log; + } + + protected async writeProtocolV2UsbPacket( + path: string, + frame: Uint8Array, + _context: ProtocolV2CallContext + ): Promise { + if (this.cancelled) { + throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled'); + } + await transferOutOnce(this.getOpenDevice(path).epOut, Buffer.from(frame)); + } + + protected async readProtocolV2UsbPacket( + path: string, + _context: ProtocolV2CallContext + ): Promise { + for (;;) { + if (this.cancelled) { + throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromOutside, 'Cancelled'); + } + try { + const packet = await transferInOnce( + this.getOpenDevice(path).epIn, + PROTOCOL_V2_FRAME_MAX_BYTES + ); + return new Uint8Array( + packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength) + ); + } catch (error) { + if (!this.isUsbTransferTimeout(error)) { + throw error; + } + } + } + } + + protected async resetProtocolV2UsbNativeLink(path: string, _reason: string): Promise { + await this.closeOpenDevice(path); + } + + protected onProtocolV2UsbLinkInvalidated(path: string, reason: string) { + this.deviceProtocol.delete(path); + this.Log?.debug(`[NodeUsbTransport] Protocol V2 link invalidated: ${path}`, reason); + } + + protected createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error { + return new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${name}`); + } + + private async callProtocolV2( + path: string, + name: string, + data: Record, + options?: TransportCallOptions + ) { + return this.callProtocolV2Usb(path, name, data, options); + } + /** * Receive a complete protobuf response from the device. * Reads 64-byte packets, strips 0x3F marker, reassembles into hex string. */ - private async receiveData(path: string, dev: OpenDevice): Promise { + private async receiveData(path: string, dev: OpenDevice, timeoutMs?: number): Promise { + const deadline = timeoutMs ? Date.now() + timeoutMs : undefined; + const readPacket = async () => { + const transferIn = this.transferInWithRetry(path, this.getOpenDevice(path), PACKET_SIZE); + return deadline + ? this.withProtocolReadTimeout(path, transferIn, Math.max(deadline - Date.now(), 1), 'V1') + : transferIn; + }; + // Read first packet, skip report byte - const firstPacket = await this.transferInWithRetry(path, dev, PACKET_SIZE); + const firstPacket = timeoutMs + ? await readPacket() + : await this.transferInWithRetry(path, dev, PACKET_SIZE); const firstData = skipReportByte(firstPacket); // Decode header: ## marker → { typeId, length, restBuffer } - const { length, typeId, restBuffer } = decodeProtocol.decodeChunked(toArrayBuffer(firstData)); + const { length, typeId, restBuffer } = ProtocolV1.decodeFirstChunk(toArrayBuffer(firstData)); // Allocate result: typeId(2) + length(4) + payload(length) const lengthWithHeader = Number(length) + HEADER_LENGTH; @@ -619,7 +965,7 @@ export default class NodeUsbTransport { // Read subsequent packets until complete // Re-resolve device on each iteration so we use a fresh handle after any reconnect while (decoded.offset < lengthWithHeader) { - const packet = await this.transferInWithRetry(path, this.getOpenDevice(path), PACKET_SIZE); + const packet = await readPacket(); const pktData = skipReportByte(packet); const buf = toArrayBuffer(pktData); if (lengthWithHeader - decoded.offset >= PAYLOAD_SIZE) { @@ -633,6 +979,8 @@ export default class NodeUsbTransport { const result = decoded.toBuffer(); return Buffer.from(result as unknown as ArrayBuffer).toString('hex'); } -} -export { PACKET_SIZE } from './constants'; + getProtocolType(path: string): ProtocolType | undefined { + return this.deviceProtocol.get(path); + } +} diff --git a/packages/hd-transport-usb/src/transportLog.ts b/packages/hd-transport-usb/src/transportLog.ts new file mode 100644 index 000000000..ef5543604 --- /dev/null +++ b/packages/hd-transport-usb/src/transportLog.ts @@ -0,0 +1,11 @@ +import type { ProtocolType } from '@onekeyfe/hd-transport'; + +const HIGH_VOLUME_CALLS = new Set(['FileWrite', 'FilesystemFileWrite', 'EmmcFileWrite']); + +export function shouldSuppressHighVolumeCallLog(name: string) { + return HIGH_VOLUME_CALLS.has(name); +} + +export function createTransportCallLog(name: string, protocol: ProtocolType) { + return { name, protocol }; +} diff --git a/packages/hd-transport-web-device/__tests__/electron-ble-transport.test.ts b/packages/hd-transport-web-device/__tests__/electron-ble-transport.test.ts new file mode 100644 index 000000000..d534844ab --- /dev/null +++ b/packages/hd-transport-web-device/__tests__/electron-ble-transport.test.ts @@ -0,0 +1,342 @@ +import transport, { PROTOCOL_V2_CHANNEL_BLE_UART, bytesToHex } from '@onekeyfe/hd-transport'; +import { HardwareErrorCode } from '@onekeyfe/hd-shared'; + +import ElectronBleTransport from '../src/electron-ble-transport'; + +const { ProtocolV1, ProtocolV2, parseConfigure } = transport; + +const protocolV1Schema = { + nested: { + Initialize: { + fields: {}, + }, + Success: { + fields: { + message: { + type: 'string', + id: 1, + }, + }, + }, + MessageType: { + values: { + MessageType_Initialize: 1, + MessageType_Success: 2, + }, + }, + }, +}; + +const protocolV2Schema = { + nested: { + ProtocolInfoRequest: { + fields: {}, + }, + ProtocolInfo: { + fields: { + version: { + type: 'uint32', + id: 1, + }, + supported_messages: { + rule: 'repeated', + type: 'uint32', + id: 2, + options: { + packed: false, + }, + }, + protobuf_definition: { + type: 'string', + id: 3, + }, + }, + }, + Ping: { + fields: { + message: { + type: 'string', + id: 1, + }, + }, + }, + Success: { + fields: { + message: { + type: 'string', + id: 1, + }, + }, + }, + MessageType: { + values: { + MessageType_ProtocolInfoRequest: 60200, + MessageType_ProtocolInfo: 60201, + MessageType_Ping: 60206, + MessageType_Success: 60207, + }, + }, + }, +}; + +const schemas = { + protocolV1: parseConfigure(protocolV1Schema), + protocolV2: parseConfigure(protocolV2Schema), +}; + +jest.setTimeout(10_000); + +const createLogger = () => ({ + debug: jest.fn(), + error: jest.fn(), +}); + +const createNobleBle = (device = { id: 'flaky-pro2-id', name: 'Unknown BLE Device' }) => ({ + enumerate: jest.fn(() => Promise.resolve([device])), + getDevice: jest.fn(() => Promise.resolve(device)), + connect: jest.fn(() => Promise.resolve()), + disconnect: jest.fn(() => Promise.resolve()), + subscribe: jest.fn(() => Promise.resolve()), + unsubscribe: jest.fn(() => Promise.resolve()), + write: jest.fn(() => Promise.resolve()), + onNotification: jest.fn(() => jest.fn()), + onDeviceDisconnected: jest.fn(() => jest.fn()), + checkAvailability: jest.fn(() => + Promise.resolve({ + available: true, + state: 'poweredOn', + unsupported: false, + initialized: true, + }) + ), +}); + +const configureTransport = (nobleBle: ReturnType) => { + (global as any).window = { + desktopApi: { + nobleBle, + }, + }; + + const transport = new ElectronBleTransport(); + transport.init(createLogger()); + transport.configure(protocolV1Schema); + transport.configureProtocolV2(protocolV2Schema); + return transport; +}; + +describe('ElectronBleTransport protocol detection', () => { + afterEach(() => { + delete (global as any).window; + jest.clearAllMocks(); + }); + + test('detects Protocol V2 after Protocol V1 probe timeout', async () => { + const device = { id: 'unknown-pro2-id', name: 'Unknown BLE Device' }; + const nobleBle = createNobleBle(device); + let notificationHandler: ((deviceId: string, data: string) => void) | undefined; + const probeResponse = ProtocolV2.encodeFrame( + schemas, + 'Success', + { message: 'ok' }, + { router: PROTOCOL_V2_CHANNEL_BLE_UART } + ); + + nobleBle.onNotification.mockImplementation(handler => { + notificationHandler = handler; + return jest.fn(); + }); + let writeCount = 0; + nobleBle.write.mockImplementation(() => { + writeCount += 1; + if (writeCount === 2) { + setTimeout(() => notificationHandler?.(device.id, bytesToHex(probeResponse)), 0); + } + return Promise.resolve(); + }); + const transport = configureTransport(nobleBle); + + try { + await expect(transport.acquire({ uuid: device.id })).resolves.toEqual( + expect.objectContaining({ + uuid: device.id, + protocolType: 'V2', + }) + ); + expect(transport.getProtocolType(device.id)).toBe('V2'); + } finally { + await transport.release(device.id); + } + }); + + test('detects Protocol V1 when device responds to Initialize', async () => { + const device = { id: 'classic-id', name: 'OneKey Classic' }; + const nobleBle = createNobleBle(device); + let notificationHandler: ((deviceId: string, data: string) => void) | undefined; + + // Build a V1 Success notification (no 64-byte padding, matching real BLE behaviour). + // Format: ?## (3f2323) + typeId BE (0002) + length BE (00000004) + protobuf payload (0a026f6b) + const v1ResponseHex = '3f23230002000000040a026f6b'; + + nobleBle.onNotification.mockImplementation(handler => { + notificationHandler = handler; + return jest.fn(); + }); + nobleBle.write.mockImplementation(() => { + // Respond to first write (V1 Initialize probe) with V1 Success + setTimeout(() => notificationHandler?.(device.id, v1ResponseHex), 0); + return Promise.resolve(); + }); + const transport = configureTransport(nobleBle); + + try { + await expect(transport.acquire({ uuid: device.id })).resolves.toEqual( + expect.objectContaining({ + uuid: device.id, + }) + ); + expect(transport.getProtocolType(device.id)).toBe('V1'); + } finally { + await transport.release(device.id); + } + }); + + test('throws when both protocol probes fail', async () => { + const device = { id: 'dead-device-id', name: 'Unknown Device' }; + const nobleBle = createNobleBle(device); + + // Never respond to writes — both probes will timeout + nobleBle.onNotification.mockImplementation(() => jest.fn()); + + const transport = configureTransport(nobleBle); + + await expect(transport.acquire({ uuid: device.id })).rejects.toThrow( + /Unable to detect BLE protocol/ + ); + expect(transport.getProtocolType(device.id)).toBeUndefined(); + }); + + test('probes Protocol V2 instead of trusting the Pro2 name hint', async () => { + const device = { id: 'named-pro2-id', name: 'OneKey Pro 2' }; + const nobleBle = createNobleBle(device); + let notificationHandler: ((deviceId: string, data: string) => void) | undefined; + const probeResponse = ProtocolV2.encodeFrame( + schemas, + 'Success', + { message: 'ok' }, + { router: PROTOCOL_V2_CHANNEL_BLE_UART } + ); + + nobleBle.onNotification.mockImplementation(handler => { + notificationHandler = handler; + return jest.fn(); + }); + nobleBle.write.mockImplementation(() => { + setTimeout(() => notificationHandler?.(device.id, bytesToHex(probeResponse)), 0); + return Promise.resolve(); + }); + const transport = configureTransport(nobleBle); + + try { + await expect(transport.acquire({ uuid: device.id })).resolves.toEqual( + expect.objectContaining({ + uuid: device.id, + protocolType: 'V2', + }) + ); + expect(nobleBle.write).toHaveBeenCalledTimes(1); + expect(transport.getProtocolType(device.id)).toBe('V2'); + await expect(transport.call(device.id, 'Ping', { message: 'after-probe' })).resolves.toEqual({ + type: 'Success', + message: { message: 'ok' }, + }); + const sentSeqs = nobleBle.write.mock.calls.map(([, hex]) => + Number.parseInt(hex.slice(12, 14), 16) + ); + expect(sentSeqs).toEqual([1, 2]); + } finally { + await transport.release(device.id); + } + }); + + test('rejects the active Protocol V2 reader when pairing is rejected', async () => { + const device = { id: 'pairing-rejected-pro2-id', name: 'OneKey Pro 2' }; + const nobleBle = createNobleBle(device); + let notificationHandler: ((deviceId: string, data: string) => void) | undefined; + let pairingRejected = false; + const probeResponse = ProtocolV2.encodeFrame( + schemas, + 'Success', + { message: 'ok' }, + { router: PROTOCOL_V2_CHANNEL_BLE_UART } + ); + + nobleBle.onNotification.mockImplementation(handler => { + notificationHandler = handler; + return jest.fn(); + }); + nobleBle.write.mockImplementation(() => { + setTimeout( + () => + notificationHandler?.( + device.id, + pairingRejected ? 'PAIRING_REJECTED' : bytesToHex(probeResponse) + ), + 0 + ); + return Promise.resolve(); + }); + const transport = configureTransport(nobleBle); + + try { + await transport.acquire({ uuid: device.id }); + pairingRejected = true; + + await expect( + transport.call(device.id, 'Ping', { message: 'pairing' }, { timeoutMs: 50 }) + ).rejects.toMatchObject({ errorCode: HardwareErrorCode.BleDeviceBondedCanceled }); + } finally { + await transport.release(device.id); + } + }); + + test('rebuilds the active link when Core acquires the same device again', async () => { + const device = { id: 'repeated-acquire-pro2-id', name: 'OneKey Pro 2' }; + const nobleBle = createNobleBle(device); + let notificationHandler: ((deviceId: string, data: string) => void) | undefined; + const response = ProtocolV2.encodeFrame( + schemas, + 'Success', + { message: 'ok' }, + { router: PROTOCOL_V2_CHANNEL_BLE_UART } + ); + + nobleBle.onNotification.mockImplementation(handler => { + notificationHandler = handler; + return jest.fn(); + }); + nobleBle.write.mockImplementation(() => { + setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0); + return Promise.resolve(); + }); + const transport = configureTransport(nobleBle); + + try { + await transport.acquire({ uuid: device.id }); + await transport.acquire({ uuid: device.id, expectedProtocol: 'V2' }); + await expect( + transport.call(device.id, 'Ping', { message: 'after-reacquire' }) + ).resolves.toEqual({ + type: 'Success', + message: { message: 'ok' }, + }); + + const sentSeqs = nobleBle.write.mock.calls.map(([, hex]) => + Number.parseInt(hex.slice(12, 14), 16) + ); + expect(sentSeqs).toEqual([1, 2]); + } finally { + await transport.release(device.id); + } + }); +}); diff --git a/packages/hd-transport-web-device/src/electron-ble-transport.ts b/packages/hd-transport-web-device/src/electron-ble-transport.ts index 9b58c8fb7..7f5e425f0 100644 --- a/packages/hd-transport-web-device/src/electron-ble-transport.ts +++ b/packages/hd-transport-web-device/src/electron-ble-transport.ts @@ -1,20 +1,30 @@ -import transport, { COMMON_HEADER_SIZE, LogBlockCommand } from '@onekeyfe/hd-transport'; +import transport, { + PROTOCOL_V1_MESSAGE_HEADER_SIZE, + PROTOCOL_V2_CHANNEL_BLE_UART, + ProtocolV2FrameAssembler, + ProtocolV2LinkManager, + bytesToHex, + hexToBytes, + probeProtocolV2 as probeProtocolV2Helper, +} from '@onekeyfe/hd-transport'; import { ERRORS, HardwareErrorCode, HardwareErrorCodeMessage, createDeferred, isHeaderChunk, + wait, } from '@onekeyfe/hd-shared'; +import { createTransportCallLog, shouldSuppressHighVolumeCallLog } from './transportLog'; + import type { Deferred } from '@onekeyfe/hd-shared'; -import type EventEmitter from 'events'; -// Import DesktopAPI type from hd-transport-electron import type { DesktopAPI } from '@onekeyfe/hd-transport-electron'; +import type { OneKeyDeviceInfo, ProtocolType, TransportCallOptions } from '@onekeyfe/hd-transport'; +import type EventEmitter from 'events'; -const { parseConfigure, buildBuffers, receiveOne, check } = transport; +const { parseConfigure, ProtocolV1, check } = transport; -// Noble BLE specific API interface declare global { interface Window { desktopApi?: DesktopAPI; @@ -24,44 +34,104 @@ declare global { export type BleAcquireInput = { uuid: string; forceCleanRunPromise?: boolean; + expectedProtocol?: ProtocolType; }; -// Packet processing result interface interface PacketProcessResult { isComplete: boolean; completePacket?: string; error?: string; } +function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined { + return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined; +} + +const toBleDescriptor = ( + device: { id: string; name: string | null }, + protocolType?: ProtocolType +): OneKeyDeviceInfo => + ({ + id: device.id, + name: device.name, + path: device.id, + debug: false, + commType: 'electron-ble', + ...(protocolType ? { protocolType } : {}), + } as OneKeyDeviceInfo); + +const BLE_PACKET_SIZE = 192; +const BLE_WRITE_DELAY_MS = 5; +const BLE_RESPONSE_TIMEOUT_MS = 30_000; +const PROTOCOL_PROBE_TIMEOUT_MS = 1000; +const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000; + +/** + * Desktop Electron BLE transport with automatic Protocol V1/V2 detection. + * + * Protocol V1 devices continue using chunked packets. Protocol V2 is detected + * after a Protocol V1 Initialize timeout by probing Protocol V2 Ping. + */ export default class ElectronBleTransport { - _messages: ReturnType | undefined; + private _messages: ReturnType | undefined; + + private _messagesV2: ReturnType | undefined; name = 'ElectronBleTransport'; configured = false; - runPromise: Deferred | null = null; + runPromise: Deferred | null = null; Log?: any; emitter?: EventEmitter; - // Cache for connected devices private connectedDevices: Set = new Set(); - // Data processing state - private dataBuffers: Map = new Map(); + private deviceProtocol: Map = new Map(); + + private deviceProtocolHints: Map = new Map(); + + private v1Buffers: Map = new Map(); + + private v2Assemblers: Map = new Map(); + + private v2FrameQueues: Map = new Map(); + + private v2FramePromises: Map> = new Map(); + + private protocolV2Links = new ProtocolV2LinkManager({ + getSchemas: () => { + if (!this._messages || !this._messagesV2) { + throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); + } + return { + protocolV1: this._messages, + protocolV2: this._messagesV2, + }; + }, + classifyError: () => 'link-fatal', + onLinkInvalidated: async (uuid, reason) => { + this.v2Assemblers.get(uuid)?.reset(); + this.rejectProtocolV2Frames(uuid, new Error(reason)); + this.Log?.debug('[Electron BLE] Protocol V2 link invalidated:', uuid, reason); + if (reason.startsWith('Protocol V2 link-fatal error:')) { + await this.release(uuid); + } + }, + }); - // Notification cleanup functions private notificationCleanups: Map void> = new Map(); - // Disconnect listener cleanup functions private disconnectCleanups: Map void> = new Map(); - // Handle bluetooth related errors with proper error code mapping + private notificationTokens: Map = new Map(); + + private nextNotificationToken = 1; + private handleBluetoothError(error: any): never { if (error && typeof error === 'object') { - // Check for specific bluetooth error codes if ('code' in error) { if (error.code === HardwareErrorCode.BlePoweredOff) { throw ERRORS.TypedError(HardwareErrorCode.BlePoweredOff); @@ -73,7 +143,6 @@ export default class ElectronBleTransport { throw ERRORS.TypedError(HardwareErrorCode.BlePermissionError); } } - // Check for error message containing bluetooth state related text using predefined messages const errorMessage = error.message || String(error); const poweredOffMessage = HardwareErrorCodeMessage[HardwareErrorCode.BlePoweredOff]; const unsupportedMessage = HardwareErrorCodeMessage[HardwareErrorCode.BleUnsupported]; @@ -89,23 +158,28 @@ export default class ElectronBleTransport { throw ERRORS.TypedError(HardwareErrorCode.BlePermissionError); } } - throw error; } - // Clean up all device state and listeners - unified cleanup function private cleanupDeviceState(deviceId: string): void { + this.protocolV2Links + .invalidateLink(deviceId, 'Electron BLE device state cleaned') + .catch(error => this.Log?.debug('[Electron BLE] link cleanup failed:', error)); this.connectedDevices.delete(deviceId); - this.dataBuffers.delete(deviceId); + this.deviceProtocol.delete(deviceId); + // Keep deviceProtocolHints — it's inferred from device name (e.g. "Pro 2" → V2) + // and doesn't depend on connection state. Preserving it avoids redundant V1 probe on reconnect. + this.v1Buffers.delete(deviceId); + this.v2Assemblers.delete(deviceId); + this.resetProtocolV2Frames(deviceId); + this.notificationTokens.delete(deviceId); - // Clean up notification listener const notifyCleanup = this.notificationCleanups.get(deviceId); if (notifyCleanup) { notifyCleanup(); this.notificationCleanups.delete(deviceId); } - // Clean up disconnect listener const disconnectCleanup = this.disconnectCleanups.get(deviceId); if (disconnectCleanup) { disconnectCleanup(); @@ -117,7 +191,6 @@ export default class ElectronBleTransport { this.Log = logger; this.emitter = emitter; - // Check if Noble BLE API is available if (!window.desktopApi?.nobleBle) { throw ERRORS.TypedError( HardwareErrorCode.RuntimeError, @@ -125,41 +198,62 @@ export default class ElectronBleTransport { ); } - this.Log?.debug('[Transport] Noble BLE Transport initialized'); + this.Log?.debug('[Electron BLE] Transport initialized'); } configure(signedData: any) { - const messages = parseConfigure(signedData); + this._messages = parseConfigure(signedData); this.configured = true; - this._messages = messages; } - listen() {} + configureProtocolV2(signedData: any) { + this._messagesV2 = parseConfigure(signedData); + this.protocolV2Links + .invalidateAllLinks('Protocol V2 schema reconfigured') + .catch(error => this.Log?.debug('[Electron BLE] schema link cleanup failed:', error)); + } + + async listen() { + return this.enumerate(); + } - async enumerate(): Promise<{ id: string; name: string }[]> { + async enumerate(): Promise { try { if (!window.desktopApi?.nobleBle) { throw new Error('Noble BLE API not available'); } - const devices = await window.desktopApi.nobleBle.enumerate(); - return devices; + this.Log?.debug(`[Electron BLE] enumerate found ${devices.length} device(s):`); + for (const dev of devices) { + this.Log?.debug(`[Electron BLE] id="${dev.id}" name="${dev.name}"`); + const protocolHint = inferProtocolHintFromDeviceName(dev.name); + if (protocolHint) { + this.deviceProtocolHints.set(dev.id, protocolHint); + } + } + return devices.map(device => toBleDescriptor(device)); } catch (error) { - this.Log?.error('[Transport] Noble BLE enumerate failed:', error); + this.Log?.error('[Electron BLE] enumerate failed:', error); this.handleBluetoothError(error); } } async acquire(input: BleAcquireInput) { - const { uuid, forceCleanRunPromise } = input; + const { uuid, forceCleanRunPromise, expectedProtocol } = input; if (!uuid) { throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID); } - // Force clean running Promise + if (this.connectedDevices.has(uuid)) { + await this.release(uuid); + } + if (forceCleanRunPromise && this.runPromise) { - this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise)); + const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise); + this.runPromise.reject(error); + this.rejectAllProtocolV2Frames(error); + this.runPromise = null; } try { @@ -167,13 +261,17 @@ export default class ElectronBleTransport { throw new Error('Noble BLE API not available'); } - // Check if device is available const device = await window.desktopApi.nobleBle.getDevice(uuid); if (!device) { throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device ${uuid} not found`); } + const protocolHint = expectedProtocol + ? undefined + : this.deviceProtocolHints.get(uuid) ?? inferProtocolHintFromDeviceName(device.name); + if (protocolHint) { + this.deviceProtocolHints.set(uuid, protocolHint); + } - // Connect to device try { await window.desktopApi.nobleBle.connect(uuid); this.connectedDevices.add(uuid); @@ -181,29 +279,18 @@ export default class ElectronBleTransport { this.handleBluetoothError(error); } - // Initialize data buffer for this device - this.dataBuffers.set(uuid, { buffer: [], bufferLength: 0 }); + this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 }); + this.v2Assemblers.set(uuid, new ProtocolV2FrameAssembler()); - // Subscribe to notifications await window.desktopApi.nobleBle.subscribe(uuid); - // Set up notification listener - const cleanup = window.desktopApi.nobleBle.onNotification( - (deviceId: string, data: string) => { - if (deviceId === uuid) { - this.handleNotificationData(uuid, data); - } - } - ); + const cleanup = this.createNotificationSubscription(uuid); this.notificationCleanups.set(uuid, cleanup); - // Set up disconnect listener const disconnectCleanup = window.desktopApi.nobleBle.onDeviceDisconnected( (disconnectedDevice: any) => { if (disconnectedDevice.id === uuid) { this.cleanupDeviceState(uuid); - - // Trigger disconnect event this.emitter?.emit('device-disconnect', { name: disconnectedDevice.name, id: disconnectedDevice.id, @@ -214,174 +301,544 @@ export default class ElectronBleTransport { ); this.disconnectCleanups.set(uuid, disconnectCleanup); - // Trigger connect event + const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint); + this.emitter?.emit('device-connect', { name: device.name, id: device.id, connectId: device.id, }); - return { uuid, path: uuid }; + return { + ...toBleDescriptor({ id: device.id, name: device.name }, protocolType), + uuid, + }; } catch (error) { - this.Log?.error('[Transport] Noble BLE acquire failed:', error); + this.Log?.error('[Electron BLE] acquire failed:', error); + try { + if (window.desktopApi?.nobleBle && this.connectedDevices.has(uuid)) { + await window.desktopApi.nobleBle.unsubscribe(uuid); + await window.desktopApi.nobleBle.disconnect(uuid); + } + } catch (cleanupError) { + this.Log?.debug('[Electron BLE] acquire cleanup failed:', cleanupError); + } + this.cleanupDeviceState(uuid); throw error; } } async release(id: string) { try { + await this.protocolV2Links.invalidateLink(id, 'Electron BLE transport released'); if (this.connectedDevices.has(id)) { - // Unsubscribe from notifications if (window.desktopApi?.nobleBle) { await window.desktopApi.nobleBle.unsubscribe(id); - } - - // Disconnect device - if (window.desktopApi?.nobleBle) { await window.desktopApi.nobleBle.disconnect(id); } - - // Clean up all device state this.cleanupDeviceState(id); } } catch (error) { - this.Log?.error('[Transport] Noble BLE release failed:', error); - // Clean up local state even if release fails + this.Log?.error('[Electron BLE] release failed:', error); this.cleanupDeviceState(id); } } - // Handle notification data from Noble BLE - private handleNotificationData(deviceId: string, hexData: string): void { - // Check for pairing rejection + private createProtocolMismatchError(expected: ProtocolType) { + return ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol` + ); + } + + private createProtocolDetectionError() { + return ERRORS.TypedError( + HardwareErrorCode.BleTimeoutError, + 'Unable to detect BLE protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping' + ); + } + + private clearProbeProtocol(uuid: string, protocol: ProtocolType) { + if (this.deviceProtocol.get(uuid) === protocol) { + this.deviceProtocol.delete(uuid); + } + } + + private async detectProtocol( + uuid: string, + expectedProtocol?: ProtocolType, + protocolHint?: ProtocolType + ): Promise { + if (expectedProtocol === 'V1') { + if (await this.probeProtocolV1(uuid)) { + this.deviceProtocol.set(uuid, 'V1'); + this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V1 (expected)`); + return 'V1'; + } + throw this.createProtocolMismatchError(expectedProtocol); + } + + if (expectedProtocol === 'V2') { + // 免探测路径:调用方显式承诺该设备是 V2(例如固件升级重启后的重连场景, + // 上层已经探测过协议并通过 expectedProtocol 传回),这里不再重复探测。 + this.deviceProtocol.set(uuid, 'V2'); + this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V2 (expected)`); + return 'V2'; + } + + // 项目约束:协议判断必须在连接后主动探测,不能依赖设备名/PID/descriptor。 + // 设备名 hint(如 "Pro 2")只用于调整探测顺序:hint=V2 时先探 V2、失败回落 V1, + // 不能作为最终结论。 + const probeOrder: ProtocolType[] = + protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2']; + + for (let i = 0; i < probeOrder.length; i += 1) { + const protocol = probeOrder[i]; + if (i > 0) { + // 上一个协议探测失败后,重置订阅与缓冲,避免残留数据干扰下一个协议的探测。 + await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]); + } + const detected = + protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid); + if (detected) { + this.deviceProtocol.set(uuid, protocol); + this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> ${protocol}`); + return protocol; + } + } + + this.deviceProtocol.delete(uuid); + throw this.createProtocolDetectionError(); + } + + private createNotificationSubscription(uuid: string) { + if (!window.desktopApi?.nobleBle) { + throw new Error('Noble BLE API not available'); + } + + const notificationToken = this.nextNotificationToken; + this.nextNotificationToken += 1; + this.notificationTokens.set(uuid, notificationToken); + + return window.desktopApi.nobleBle.onNotification((deviceId: string, data: string) => { + if (deviceId === uuid && this.notificationTokens.get(uuid) === notificationToken) { + this.handleNotification(uuid, data); + } + }); + } + + private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) { + await this.protocolV2Links.invalidateLink( + uuid, + `Reset notify state after Protocol ${protocol} probe` + ); + this.v1Buffers.set(uuid, { buffer: [], bufferLength: 0 }); + this.v2Assemblers.get(uuid)?.reset(); + this.resetProtocolV2Frames(uuid); + if (this.runPromise) { + const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise); + this.runPromise.reject(error); + this.runPromise = null; + } + + const notifyCleanup = this.notificationCleanups.get(uuid); + if (notifyCleanup) { + notifyCleanup(); + this.notificationCleanups.delete(uuid); + } + this.notificationTokens.delete(uuid); + + try { + await window.desktopApi?.nobleBle?.unsubscribe(uuid); + } catch (error) { + this.Log?.debug(`[Electron BLE] unsubscribe after Protocol ${protocol} probe failed:`, error); + } + try { + await window.desktopApi?.nobleBle?.subscribe(uuid); + } catch (error) { + this.Log?.debug(`[Electron BLE] resubscribe after Protocol ${protocol} probe failed:`, error); + throw error; + } + + const cleanup = this.createNotificationSubscription(uuid); + this.notificationCleanups.set(uuid, cleanup); + } + + private async probeProtocolV1(uuid: string) { + if (!this._messages) { + return false; + } + + try { + this.deviceProtocol.set(uuid, 'V1'); + await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS }); + return true; + } catch (error) { + this.clearProbeProtocol(uuid, 'V1'); + this.Log?.debug('[Electron BLE] Protocol V1 Initialize probe failed:', error); + return false; + } + } + + private async probeProtocolV2(uuid: string) { + if (!this._messages || !this._messagesV2) { + return false; + } + + this.deviceProtocol.set(uuid, 'V2'); + this.v2Assemblers.get(uuid)?.reset(); + const detected = await probeProtocolV2Helper({ + call: (name, data, options) => this.callProtocolV2(uuid, name, data, options), + timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT_MS, + logger: this.Log, + logPrefix: 'ProtocolV2 BLE', + onProbeFailed: () => { + this.v2Assemblers.get(uuid)?.reset(); + this.resetProtocolV2Frames(uuid); + }, + }); + if (!detected) { + this.clearProbeProtocol(uuid, 'V2'); + } + return detected; + } + + private async writeWithChunking(uuid: string, hexData: string): Promise { + const totalBytes = hexData.length / 2; + + if (totalBytes <= BLE_PACKET_SIZE) { + await wait(BLE_WRITE_DELAY_MS); + await this.writeOnce(uuid, hexData); + return; + } + + for (let offset = 0; offset < hexData.length; ) { + const chunkHexLen = Math.min(BLE_PACKET_SIZE * 2, hexData.length - offset); + const chunkHex = hexData.substring(offset, offset + chunkHexLen); + offset += chunkHexLen; + + await this.writeOnce(uuid, chunkHex); + + if (offset < hexData.length) { + await wait(BLE_WRITE_DELAY_MS); + } + } + } + + private async writeOnce(uuid: string, hexData: string): Promise { + const nobleBle = window.desktopApi?.nobleBle; + if (!nobleBle) { + throw new Error('Noble BLE API not available'); + } + + await nobleBle.write(uuid, hexData); + } + + private handleNotification(deviceId: string, hexData: string): void { if (hexData === 'PAIRING_REJECTED') { - this.Log?.debug('[Transport] Pairing rejection detected for device:', deviceId); - if (this.runPromise) { - this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleDeviceBondedCanceled)); + this.Log?.debug('[Electron BLE] Pairing rejection detected for device:', deviceId); + const error = ERRORS.TypedError(HardwareErrorCode.BleDeviceBondedCanceled); + if (this.deviceProtocol.get(deviceId) === 'V2') { + this.rejectProtocolV2Frames(deviceId, error); + } else if (this.runPromise) { + this.runPromise.reject(error); } return; } - const result = this.processNotificationPacket(deviceId, hexData); + const protocol = this.deviceProtocol.get(deviceId); + if (!protocol) { + this.Log?.debug('[Electron BLE] Ignore notification before protocol detection:', deviceId); + return; + } + if (protocol === 'V2') { + this.handleProtocolV2Notification(deviceId, hexData); + return; + } + this.handleProtocolV1Notification(deviceId, hexData); + } + + private handleProtocolV2Notification(deviceId: string, hexData: string): void { + try { + const bytes = hexToBytes(hexData); + if (bytes.length === 0) return; + + const assembler = this.v2Assemblers.get(deviceId); + if (!assembler) return; + + for (const frameData of assembler.drain(bytes)) { + this.resolveProtocolV2Frame(deviceId, frameData); + } + } catch (error) { + this.Log?.error('[Electron BLE] Protocol V2 notification error:', error); + const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError); + this.rejectProtocolV2Frames(deviceId, notifyError); + } + } + + private getProtocolV2FrameQueue(uuid: string) { + let queue = this.v2FrameQueues.get(uuid); + if (!queue) { + queue = []; + this.v2FrameQueues.set(uuid, queue); + } + return queue; + } + + private resolveProtocolV2Frame(uuid: string, frame: Uint8Array) { + const framePromise = this.v2FramePromises.get(uuid); + if (framePromise) { + framePromise.resolve(frame); + this.v2FramePromises.delete(uuid); + return; + } + this.getProtocolV2FrameQueue(uuid).push(frame); + } + + private rejectAllProtocolV2Frames(error: Error) { + this.v2FrameQueues.clear(); + for (const framePromise of this.v2FramePromises.values()) { + framePromise.reject(error); + } + this.v2FramePromises.clear(); + } + + private resetProtocolV2Frames(uuid: string) { + this.v2FrameQueues.delete(uuid); + this.v2FramePromises.delete(uuid); + } + + private rejectProtocolV2Frames(uuid: string, error: Error) { + this.v2FrameQueues.delete(uuid); + const framePromise = this.v2FramePromises.get(uuid); + if (framePromise) { + this.v2FramePromises.delete(uuid); + framePromise.reject(error); + } + } + + private async readProtocolV2Frame(uuid: string) { + const queuedFrame = this.getProtocolV2FrameQueue(uuid).shift(); + if (queuedFrame) { + return queuedFrame; + } + + const framePromise = createDeferred(); + this.v2FramePromises.set(uuid, framePromise); + try { + return await framePromise.promise; + } finally { + if (this.v2FramePromises.get(uuid) === framePromise) { + this.v2FramePromises.delete(uuid); + } + } + } + + private handleProtocolV1Notification(deviceId: string, hexData: string): void { + const result = this.processProtocolV1Notification(deviceId, hexData); if (result.error) { - this.Log?.error('[Transport] Packet processing error:', result.error); + this.Log?.error('[Electron BLE] Protocol V1 packet processing error:', result.error); if (this.runPromise) { this.runPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError)); } return; } - if (result.isComplete && result.completePacket) { - if (this.runPromise) { - this.runPromise.resolve(result.completePacket); - } + if (result.isComplete && result.completePacket && this.runPromise) { + this.runPromise.resolve(result.completePacket); } } - async call(uuid: string, name: string, data: Record) { - if (this._messages == null) { + async call( + uuid: string, + name: string, + data: Record, + options?: TransportCallOptions + ) { + if (!this._messages) { throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); } - const forceRun = name === 'Initialize' || name === 'Cancel'; + if (!this.connectedDevices.has(uuid)) { + throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound, `Device ${uuid} not connected`); + } - if (this.runPromise && !forceRun) { - throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress); + const protocol = this.deviceProtocol.get(uuid); + if (!protocol) { + throw ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + `Device protocol has not been detected for ${uuid}` + ); + } + if (!shouldSuppressHighVolumeCallLog(name)) { + this.Log?.debug('transport call', createTransportCallLog(name, protocol)); } - if (!this.connectedDevices.has(uuid)) { - throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound, `Device ${uuid} not connected`); + if (protocol === 'V2') { + return this.callProtocolV2(uuid, name, data, options); } + return this.callProtocolV1(uuid, name, data, options); + } - this.runPromise = createDeferred(); - const messages = this._messages; + private async callProtocolV1( + uuid: string, + name: string, + data: Record, + options?: TransportCallOptions + ) { + if (!this._messages) { + throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); + } - // Log different types of commands appropriately - if (name === 'ResourceUpdate' || name === 'ResourceAck') { - this.Log?.debug('[Transport] Noble BLE call', 'name:', name, 'data:', { - file_name: data?.file_name, - hash: data?.hash, - }); - } else if (LogBlockCommand.has(name)) { - this.Log?.debug('[Transport] Noble BLE call', 'name:', name); - } else { - this.Log?.debug('[Transport] Noble BLE call', 'name:', name, 'data:', data); + const forceRun = name === 'Initialize' || name === 'Cancel'; + if (this.runPromise && !forceRun) { + throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress); } - const buffers = buildBuffers(messages, name, data); + const runPromise = createDeferred(); + runPromise.promise.catch(() => undefined); + this.runPromise = runPromise; + const messages = this._messages; + const buffers = ProtocolV1.encodeTransportPackets(messages, name, data); + let timeout: ReturnType | undefined; try { if (!window.desktopApi?.nobleBle) { throw new Error('Noble BLE write API not available'); } - // Write each buffer to the device for (let i = 0; i < buffers.length; i++) { const buffer = buffers[i]; - if (!buffer || typeof buffer.toString !== 'function') { - this.Log?.error(`[Transport] Noble BLE buffer ${i + 1} is invalid:`, buffer); throw new Error(`Buffer ${i + 1} is invalid`); } - - // Use ByteBuffer's toString('hex') method directly, similar to other transports const hexString = buffer.toString('hex'); - if (hexString.length === 0) { - this.Log?.error(`[Transport] Noble BLE buffer ${i + 1} generated empty hex string`); throw new Error(`Buffer ${i + 1} is empty`); } - await window.desktopApi.nobleBle.write(uuid, hexString); } - // Wait for response - const response = await this.runPromise.promise; - + const response = await Promise.race([ + runPromise.promise, + new Promise((_, reject) => { + if (options?.timeoutMs) { + timeout = setTimeout(() => { + const error = ERRORS.TypedError( + HardwareErrorCode.BleTimeoutError, + `BLE response timeout after ${options.timeoutMs}ms for ${name}` + ); + runPromise.reject(error); + reject(error); + }, options.timeoutMs); + } + }), + ]); if (typeof response !== 'string') { throw new Error('Returning data is not string.'); } - const jsonData = receiveOne(messages, response); + const jsonData = ProtocolV1.decodeMessage(messages, response); return check.call(jsonData); } catch (e) { - this.Log?.error('[Transport] Noble BLE call error:', e); + this.Log?.error('[Electron BLE] Protocol V1 call error:', e); throw e; } finally { - this.runPromise = null; + if (timeout) clearTimeout(timeout); + if (this.runPromise === runPromise) { + this.runPromise = null; + } } } - // Process hex data from notification with validation and packet reassembly - private processNotificationPacket(deviceId: string, hexData: string): PacketProcessResult { + private async callProtocolV2( + uuid: string, + name: string, + data: Record, + options?: TransportCallOptions + ) { + if (!this._messages || !this._messagesV2) { + throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); + } + + const callOptions = { + ...options, + timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS, + }; + try { - // Validate input - if (typeof hexData !== 'string') { - return { isComplete: false, error: 'Invalid hexData type' }; - } + return await this.protocolV2Links.call( + uuid, + () => this.createProtocolV2Adapter(uuid), + name, + data, + callOptions + ); + } catch (e) { + this.Log?.error('[Electron BLE] Protocol V2 call error:', e); + throw e; + } + } - // Clean and validate hex format - const cleanHexData = hexData.replace(/\s+/g, ''); - if (!/^[0-9A-Fa-f]*$/.test(cleanHexData)) { - return { isComplete: false, error: 'Invalid hex data format' }; + private createProtocolV2Adapter(uuid: string) { + const generation = this.notificationTokens.get(uuid) ?? 0; + const assertCurrentGeneration = () => { + if (this.notificationTokens.get(uuid) !== generation) { + throw new Error(`Protocol V2 notification generation changed for ${uuid}`); } + }; + + return { + router: PROTOCOL_V2_CHANNEL_BLE_UART, + generation, + prepareCall: () => { + assertCurrentGeneration(); + this.v2Assemblers.get(uuid)?.reset(); + this.resetProtocolV2Frames(uuid); + }, + writeFrame: (frame: Uint8Array) => { + assertCurrentGeneration(); + return this.writeWithChunking(uuid, bytesToHex(frame)); + }, + readFrame: async () => { + assertCurrentGeneration(); + const rxFrame = await this.readProtocolV2Frame(uuid); + if (!(rxFrame instanceof Uint8Array)) { + throw new Error('Response is not Uint8Array'); + } + return rxFrame; + }, + reset: (reason: string) => { + this.v2Assemblers.get(uuid)?.reset(); + this.rejectProtocolV2Frames(uuid, new Error(reason)); + }, + logger: this.Log, + logPrefix: 'ProtocolV2 BLE', + createTimeoutError: (messageName: string, timeout: number) => + ERRORS.TypedError( + HardwareErrorCode.BleTimeoutError, + `BLE response timeout after ${timeout}ms for ${messageName}` + ), + }; + } - // Convert hex string to Uint8Array - const hexMatch = cleanHexData.match(/.{1,2}/g); - if (!hexMatch) { - return { isComplete: false, error: 'Failed to parse hex data' }; + private processProtocolV1Notification(deviceId: string, hexData: string): PacketProcessResult { + try { + if (typeof hexData !== 'string') { + return { isComplete: false, error: 'Invalid hexData type' }; } - const data = new Uint8Array(hexMatch.map(byte => parseInt(byte, 16))); + const data = hexToBytes(hexData); + if (data.length === 0) { + return { isComplete: false, error: 'Empty or invalid hex data' }; + } - // Get buffer state - const bufferState = this.dataBuffers.get(deviceId); + const bufferState = this.v1Buffers.get(deviceId); if (!bufferState) { return { isComplete: false, error: 'No buffer state for device' }; } - // Process header or data chunk if (isHeaderChunk(data)) { const dataView = new DataView(data.buffer); bufferState.bufferLength = dataView.getInt32(5, false); @@ -390,20 +847,12 @@ export default class ElectronBleTransport { bufferState.buffer = bufferState.buffer.concat([...data]); } - // Check if packet is complete - if (bufferState.buffer.length - COMMON_HEADER_SIZE >= bufferState.bufferLength) { + if (bufferState.buffer.length - PROTOCOL_V1_MESSAGE_HEADER_SIZE >= bufferState.bufferLength) { const completeBuffer = new Uint8Array(bufferState.buffer); - - // Reset buffer state bufferState.bufferLength = 0; bufferState.buffer = []; - // Convert to hex string - const hexString = Array.from(completeBuffer) - .map(b => b.toString(16).padStart(2, '0')) - .join(''); - - return { isComplete: true, completePacket: hexString }; + return { isComplete: true, completePacket: bytesToHex(completeBuffer) }; } return { isComplete: false }; @@ -411,4 +860,8 @@ export default class ElectronBleTransport { return { isComplete: false, error: `Packet processing error: ${error}` }; } } + + getProtocolType(path: string): ProtocolType | undefined { + return this.deviceProtocol.get(path); + } } diff --git a/packages/hd-transport-web-device/src/transportLog.ts b/packages/hd-transport-web-device/src/transportLog.ts new file mode 100644 index 000000000..ef5543604 --- /dev/null +++ b/packages/hd-transport-web-device/src/transportLog.ts @@ -0,0 +1,11 @@ +import type { ProtocolType } from '@onekeyfe/hd-transport'; + +const HIGH_VOLUME_CALLS = new Set(['FileWrite', 'FilesystemFileWrite', 'EmmcFileWrite']); + +export function shouldSuppressHighVolumeCallLog(name: string) { + return HIGH_VOLUME_CALLS.has(name); +} + +export function createTransportCallLog(name: string, protocol: ProtocolType) { + return { name, protocol }; +} diff --git a/packages/hd-transport-web-device/src/webusb.ts b/packages/hd-transport-web-device/src/webusb.ts index 411c293e3..8a90baf9f 100644 --- a/packages/hd-transport-web-device/src/webusb.ts +++ b/packages/hd-transport-web-device/src/webusb.ts @@ -1,5 +1,15 @@ /* eslint-disable no-undef */ -import transport, { LogBlockCommand } from '@onekeyfe/hd-transport'; +import transport, { + PROTOCOL_V1_CHUNK_PAYLOAD_SIZE, + PROTOCOL_V1_MESSAGE_HEADER_SIZE, + PROTOCOL_V1_REPORT_ID, + PROTOCOL_V1_USB_PACKET_SIZE, + PROTOCOL_V2_CHANNEL_USB, + PROTOCOL_V2_FRAME_MAX_BYTES, + ProtocolV2FrameAssembler, + ProtocolV2Session, + probeProtocolV2 as probeProtocolV2Helper, +} from '@onekeyfe/hd-transport'; import { ERRORS, HardwareErrorCode, @@ -9,17 +19,30 @@ import { } from '@onekeyfe/hd-shared'; import ByteBuffer from 'bytebuffer'; -import type { AcquireInput, OneKeyDeviceInfoBase } from '@onekeyfe/hd-transport'; +import { createTransportCallLog, shouldSuppressHighVolumeCallLog } from './transportLog'; -const { parseConfigure, buildEncodeBuffers, decodeProtocol, receiveOne, check } = transport; +import type { + AcquireInput, + OneKeyDeviceInfoBase, + ProtocolType, + TransportCallOptions, +} from '@onekeyfe/hd-transport'; + +const { parseConfigure, check, ProtocolV1 } = transport; const CONFIGURATION_ID = 1; const INTERFACE_ID = 0; const ENDPOINT_ID = 1; -const PACKET_SIZE = 64; -const HEADER_LENGTH = 6; +const PACKET_SIZE = PROTOCOL_V1_USB_PACKET_SIZE; +const PAYLOAD_SIZE = PROTOCOL_V1_CHUNK_PAYLOAD_SIZE; +const REPORT_ID = PROTOCOL_V1_REPORT_ID; +const HEADER_LENGTH = PROTOCOL_V1_MESSAGE_HEADER_SIZE; const PACKET_IO_MAX_RETRIES = 3; const PACKET_IO_RETRY_DELAY = 300; +const PROTOCOL_PROBE_TIMEOUT = 1000; +function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined { + return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined; +} /** * Device information with path and WebUSB device instance @@ -27,11 +50,52 @@ const PACKET_IO_RETRY_DELAY = 300; export interface DeviceInfo extends OneKeyDeviceInfoBase { path: string; device: USBDevice; + protocolType?: ProtocolType; +} + +/** USB endpoint pair discovered at connect time */ +interface DeviceEndpoints { + interfaceNumber: number; + endpointIn: number; + endpointOut: number; +} + +interface TransferCancelToken { + cancelled: boolean; } export default class WebUsbTransport { messages: ReturnType | undefined; + /** Protobuf schema for Protocol V2 transports. */ + messagesV2: ReturnType | undefined; + + /** Per-path protocol type detected by active wire-level probe. */ + private deviceProtocol: Map = new Map(); + + private deviceProtocolHints: Map = new Map(); + + /** 按设备缓存 Protocol V2 frame assembler,保留同一次读取里多出来的后续 frame。 */ + private protocolV2Assemblers: Map = new Map(); + + /** 按设备缓存 Protocol V2 session,保持 seq 与设备会话一致递增。 */ + private protocolV2Sessions: Map = new Map(); + + /** 当前 Protocol V2 调用的读取超时,由缓存 session 的 readFrame 闭包读取。 */ + private protocolV2ReadTimeouts: Map = new Map(); + + /** Per-path USB endpoint / interface numbers (discovered from USB descriptors) */ + private deviceEndpoints: Map = new Map(); + + /** + * 早期 Pro2 工程板 USB descriptor 没有烧录 serial number。 + * 为这类设备生成会话内稳定的 mock path(同一 USBDevice 实例复用同一 path), + * 避免设备因为空 serial 被发现流程整体丢弃。重新插拔后实例变化,path 会重新生成。 + */ + private mockSerialPaths: WeakMap = new WeakMap(); + + private mockSerialCounter = 0; + name = 'WebUsbTransport'; stopped = false; @@ -71,7 +135,7 @@ export default class WebUsbTransport { } /** - * Configure transport protocol + * Configure Protocol V1 protobuf schema (legacy chunked 0x3F framing). */ configure(signedData: any) { const messages = parseConfigure(signedData); @@ -79,6 +143,15 @@ export default class WebUsbTransport { this.messages = messages; } + /** + * Cache the Protocol V2 protobuf schema. + */ + configureProtocolV2(signedData: any) { + this.messagesV2 = parseConfigure(signedData); + this.protocolV2Sessions.clear(); + this.protocolV2ReadTimeouts.clear(); + } + /** * Request user to select a device * This method must be called in response to a user action @@ -108,6 +181,26 @@ export default class WebUsbTransport { return this.deviceList; } + /** + * 设备 path:正常设备直接用 USB serial number; + * 空 serial(早期工程板)回退到会话内稳定的 mock path。 + */ + private getDevicePath(device: USBDevice): string { + if (typeof device.serialNumber === 'string' && device.serialNumber.length > 0) { + return device.serialNumber; + } + let path = this.mockSerialPaths.get(device); + if (!path) { + this.mockSerialCounter += 1; + path = `mock-serial:${device.vendorId.toString(16)}:${device.productId.toString(16)}:${ + this.mockSerialCounter + }`; + this.mockSerialPaths.set(device, path); + this.Log?.debug(`[WebUSB] device has no serial number, using mock path: ${path}`); + } + return path; + } + /** * Get list of connected devices */ @@ -117,18 +210,24 @@ export default class WebUsbTransport { const devices = await this.usb.getDevices(); const onekeyDevices = devices.filter(dev => { const isOneKey = ONEKEY_WEBUSB_FILTER.some( - (desc: { vendorId: number; productId: number }) => - dev.vendorId === desc.vendorId && dev.productId === desc.productId + desc => dev.vendorId === desc.vendorId && dev.productId === desc.productId ); - const hasSerialNumber = typeof dev.serialNumber === 'string' && dev.serialNumber.length > 0; - return isOneKey && hasSerialNumber && !isKnownTrezorWebUsbDevice(dev); + return isOneKey && !isKnownTrezorWebUsbDevice(dev); }); - this.deviceList = onekeyDevices.map(device => ({ - path: device.serialNumber as string, - device, - commType: 'webusb', - })); + this.deviceList = onekeyDevices.map(device => { + const path = this.getDevicePath(device); + const protocolHint = inferProtocolHintFromDeviceName(device.productName); + if (protocolHint) { + this.deviceProtocolHints.set(path, protocolHint); + } + + return { + path, + device, + commType: 'webusb', + }; + }); return this.deviceList; } @@ -139,7 +238,17 @@ export default class WebUsbTransport { async acquire(input: AcquireInput) { if (!input.path) return; try { + await this.closeOpenDevice(input.path); await this.connect(input.path ?? '', true); + const deviceName = this.deviceList.find(device => device.path === input.path)?.device + .productName; + const protocolHint = input.expectedProtocol + ? undefined + : this.deviceProtocolHints.get(input.path) ?? inferProtocolHintFromDeviceName(deviceName); + if (protocolHint) { + this.deviceProtocolHints.set(input.path, protocolHint); + } + await this.detectProtocol(input.path, input.expectedProtocol, protocolHint); return await Promise.resolve(input.path); } catch (e) { this.Log.debug('acquire error: ', e instanceof Error ? `${e.name}: ${e.message}` : String(e)); @@ -147,6 +256,64 @@ export default class WebUsbTransport { } } + /** + * Determine protocol type after connect. + * Probe Protocol V1 first with Initialize. If it does not answer in time, + * fall back to a Protocol V2 Ping probe. + */ + private createProtocolMismatchError(expected: ProtocolType) { + return ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol` + ); + } + + private createProtocolDetectionError() { + return ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + 'Unable to detect USB protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping' + ); + } + + private async detectProtocol( + path: string, + expectedProtocol?: ProtocolType, + protocolHint?: ProtocolType + ): Promise { + if (expectedProtocol === 'V1') { + if (await this.probeProtocolV1(path)) { + this.deviceProtocol.set(path, 'V1'); + return 'V1'; + } + throw this.createProtocolMismatchError(expectedProtocol); + } + + if (expectedProtocol === 'V2') { + // 免探测路径:调用方显式承诺该设备是 V2(例如固件升级重启后的重连场景, + // 上层已经探测过协议并通过 expectedProtocol 传回),这里不再重复探测。 + this.deviceProtocol.set(path, 'V2'); + return 'V2'; + } + + // 项目约束:协议判断必须在连接后主动探测,不能依赖设备名/PID/descriptor。 + // 设备名 hint(如 "Pro 2")只用于调整探测顺序:hint=V2 时先探 V2、失败回落 V1, + // 不能作为最终结论。 + const probeOrder: ProtocolType[] = + protocolHint === 'V2' || this.deviceProtocol.get(path) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2']; + + for (const protocol of probeOrder) { + const detected = + protocol === 'V1' ? await this.probeProtocolV1(path) : await this.probeProtocolV2(path); + if (detected) { + this.deviceProtocol.set(path, protocol); + return protocol; + } + } + + this.deviceProtocol.delete(path); + throw this.createProtocolDetectionError(); + } + /** * Find device by path */ @@ -189,22 +356,109 @@ export default class WebUsbTransport { } /** - * Connect to specific device + * Discover vendor-class (0xFF) interface and its IN/OUT endpoint numbers from USB descriptors. + * Falls back to legacy hardcoded values if no vendor interface is found. + */ + private discoverEndpoints(device: USBDevice): DeviceEndpoints { + for (const config of device.configurations) { + for (const iface of config.interfaces) { + for (const alt of iface.alternates) { + if (alt.interfaceClass === 0xff) { + let endpointIn = this.endpointId; + let endpointOut = this.endpointId; + for (const ep of alt.endpoints) { + if (ep.direction === 'in') endpointIn = ep.endpointNumber; + else endpointOut = ep.endpointNumber; + } + this.Log?.debug( + `[WebUsbTransport] discovered vendor interface ${iface.interfaceNumber}, ` + + `endpointIn=${endpointIn}, endpointOut=${endpointOut}` + ); + return { interfaceNumber: iface.interfaceNumber, endpointIn, endpointOut }; + } + } + } + } + // Fallback: legacy hardcoded values + this.Log?.debug('[WebUsbTransport] no vendor interface found, using defaults'); + return { + interfaceNumber: this.interfaceId, + endpointIn: this.endpointId, + endpointOut: this.endpointId, + }; + } + + /** + * Connect to specific device. + * Discovers interface/endpoint numbers from USB descriptors on first connection. */ async connectToDevice(path: string, first: boolean) { - const device: USBDevice = await this.findDevice(path); - await device.open(); + let device: USBDevice = await this.findDevice(path); + if (!device.opened) { + await device.open(); + } + try { + await device.reset(); + } catch (error) { + this.Log?.debug('[WebUsbTransport] reset before claim failed, continuing:', error); + } + await this.getConnectedDevices(); + device = await this.findDevice(path); + if (!device.opened) { + await device.open(); + } - if (first) { + if ( + first || + !device.configuration || + device.configuration.configurationValue !== this.configurationId + ) { await device.selectConfiguration(this.configurationId); - try { - await device.reset(); - } catch (error) { - // Ignore reset errors - } } - await device.claimInterface(this.interfaceId); + // Discover endpoints from USB descriptors; descriptors are not used for protocol selection. + const endpoints = this.discoverEndpoints(device); + this.deviceEndpoints.set(path, endpoints); + this.protocolV2Assemblers.get(path)?.reset(); + this.protocolV2Assemblers.set(path, new ProtocolV2FrameAssembler(PROTOCOL_V2_FRAME_MAX_BYTES)); + + await device.claimInterface(endpoints.interfaceNumber); + await this.clearEndpointHalt(device, 'in', endpoints.endpointIn); + await this.clearEndpointHalt(device, 'out', endpoints.endpointOut); + } + + private async closeOpenDevice(path: string) { + this.protocolV2Assemblers.get(path)?.reset(); + const current = this.deviceList.find(device => device.path === path)?.device; + if (!current?.opened) return; + + const endpoints = this.deviceEndpoints.get(path); + const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId; + try { + await current.releaseInterface(ifaceNum); + } catch (error) { + this.Log?.debug('[WebUsbTransport] releaseInterface before reconnect failed:', error); + } + try { + await current.close(); + } catch (error) { + this.Log?.debug('[WebUsbTransport] close before reconnect failed:', error); + } + } + + private async clearEndpointHalt( + device: USBDevice, + direction: USBDirection, + endpointNumber: number + ) { + try { + await device.clearHalt(direction, endpointNumber); + } catch (error) { + this.Log?.debug( + `[WebUsbTransport] clearHalt ${direction} endpoint ${endpointNumber} failed, continuing:`, + error + ); + } } async post(session: string, name: string, data: Record) { @@ -250,8 +504,10 @@ export default class WebUsbTransport { try { const currentDevice = await this.findDevice(path); if (currentDevice.opened) { + const endpoints = this.deviceEndpoints.get(path); + const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId; try { - await currentDevice.releaseInterface(this.interfaceId); + await currentDevice.releaseInterface(ifaceNum); } catch (releaseError) { this.Log.debug('[WebUsbTransport] releaseInterface before retry error:', releaseError); } @@ -288,14 +544,7 @@ export default class WebUsbTransport { let lastError: unknown; for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt += 1) { try { - const device = await this.findDevice(path); - if (!device.opened) { - await this.connect(path, false); - } - const transferBuffer = this.toArrayBuffer( - packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength) - ); - await device.transferOut(this.endpointId, transferBuffer); + await this.transferOutOnce(path, packet); return; } catch (error) { lastError = error; @@ -318,18 +567,43 @@ export default class WebUsbTransport { throw lastError; } - private async transferInWithRetry(path: string, length: number): Promise { + private async transferOutOnce(path: string, packet: Uint8Array) { + const device = await this.findDevice(path); + if (!device.opened) { + await this.connect(path, false); + } + const endpoints = this.deviceEndpoints.get(path); + const endpointOut = endpoints?.endpointOut ?? this.endpointId; + const transferBuffer = this.toArrayBuffer( + packet.buffer.slice(packet.byteOffset, packet.byteOffset + packet.byteLength) + ); + await device.transferOut(endpointOut, transferBuffer); + } + + private async transferInWithRetry( + path: string, + length: number, + cancelToken?: TransferCancelToken + ): Promise { let lastError: unknown; for (let attempt = 1; attempt <= PACKET_IO_MAX_RETRIES; attempt += 1) { + if (cancelToken?.cancelled) { + throw new Error('transferIn cancelled'); + } try { const device = await this.findDevice(path); if (!device.opened) { await this.connect(path, false); } - const result = await device.transferIn(this.endpointId, length); + const endpoints = this.deviceEndpoints.get(path); + const endpointIn = endpoints?.endpointIn ?? this.endpointId; + const result = await device.transferIn(endpointIn, length); return this.getTransferInData(result); } catch (error) { lastError = error; + if (cancelToken?.cancelled) { + throw error; + } const shouldRetry = attempt < PACKET_IO_MAX_RETRIES && this.isRetryablePacketIoError(error); if (!shouldRetry) { throw error; @@ -349,10 +623,102 @@ export default class WebUsbTransport { throw lastError; } + private async resetConnectionAfterProbe(path: string) { + this.protocolV2Assemblers.get(path)?.reset(); + this.protocolV2Sessions.delete(path); + this.protocolV2ReadTimeouts.delete(path); + + try { + const device = await this.findDevice(path); + if (device.opened) { + const endpoints = this.deviceEndpoints.get(path); + const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId; + try { + await device.releaseInterface(ifaceNum); + } catch (error) { + this.Log.debug('[WebUsbTransport] releaseInterface after protocol probe error:', error); + } + await device.close(); + } + } catch (error) { + this.Log.debug('[WebUsbTransport] close after protocol probe error:', error); + } + + await this.getConnectedDevices(); + await this.connect(path, false); + } + + private async withProtocolReadTimeout( + _path: string, + promise: Promise, + timeoutMs: number, + protocol: ProtocolType, + onTimeout?: () => void + ): Promise { + let timer: ReturnType | undefined; + let timedOut = false; + const waitForeverAfterTimeout = () => new Promise(() => {}); + const guardedPromise = promise.then( + value => (timedOut ? waitForeverAfterTimeout() : value), + error => { + if (timedOut) { + return waitForeverAfterTimeout(); + } + throw error; + } + ); + try { + return await Promise.race([ + guardedPromise, + new Promise((_, reject) => { + timer = setTimeout(() => { + timedOut = true; + onTimeout?.(); + reject(new Error(`Protocol ${protocol} read timeout after ${timeoutMs}ms`)); + }, timeoutMs); + }), + ]); + } finally { + if (timer) clearTimeout(timer); + } + } + + private async probeProtocolV1(path: string) { + if (!this.messages) { + return false; + } + + try { + await this.callProtocolV1(path, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT }); + return true; + } catch (_error) { + return false; + } + } + + private async probeProtocolV2(path: string) { + if (!this.messages || !this.messagesV2) { + return false; + } + + return probeProtocolV2Helper({ + call: (name, data, options) => this.callProtocolV2(path, name, data, options), + timeoutMs: PROTOCOL_PROBE_TIMEOUT, + logger: this.Log, + logPrefix: 'ProtocolV2 WebUSB', + onProbeFailed: () => this.resetConnectionAfterProbe(path), + }); + } + /** - * Call device method + * Call device method — branches to Protocol V1 or Protocol V2 based on active probe. */ - async call(path: string, name: string, data: Record) { + async call( + path: string, + name: string, + data: Record, + options?: TransportCallOptions + ) { if (this.messages == null) { throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); } @@ -362,37 +728,168 @@ export default class WebUsbTransport { throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound); } + const protocol = this.deviceProtocol.get(path); + if (!protocol) { + throw ERRORS.TypedError( + HardwareErrorCode.RuntimeError, + `Device protocol has not been detected for ${path}` + ); + } + + if (!shouldSuppressHighVolumeCallLog(name)) { + this.Log.debug('transport call', createTransportCallLog(name, protocol)); + } + + if (protocol === 'V2') { + return this.callProtocolV2(path, name, data, options); + } + + return this.callProtocolV1(path, name, data, options); + } + + private async callProtocolV1( + path: string, + name: string, + data: Record, + options?: TransportCallOptions + ) { const { messages } = this; - if (LogBlockCommand.has(name)) { - this.Log.debug('call-', ' name: ', name); - } else { - this.Log.debug('call-', ' name: ', name, ' data: ', data); + if (!messages) { + throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); } - const encodeBuffers = buildEncodeBuffers(messages, name, data); + + const encodeBuffers = ProtocolV1.encodeMessageChunks(messages, name, data); for (const buffer of encodeBuffers) { const newArray: Uint8Array = new Uint8Array(PACKET_SIZE); - newArray[0] = 63; + newArray[0] = REPORT_ID; newArray.set(new Uint8Array(buffer), 1); - // console.log('send packet: ', newArray); await this.transferOutWithRetry(path, newArray); } - const resData = await this.receiveData(path); + const resData = await this.receiveData(path, options?.timeoutMs); if (typeof resData !== 'string') { throw ERRORS.TypedError(HardwareErrorCode.NetworkError, 'Returning data is not string.'); } - const jsonData = receiveOne(messages, resData); + const jsonData = ProtocolV1.decodeMessage(messages, resData); return check.call(jsonData); } + /** + * Send/receive a single call over Protocol V2 (0x5A framing). + * + * Encoding: protobuf message → 2-byte LE messageTypeId + pb bytes → Protocol V2 frame + * Decoding: Protocol V2 frame → messageTypeId + pb bytes → protobuf message + */ + private async callProtocolV2( + path: string, + name: string, + data: Record, + options?: TransportCallOptions + ) { + const protocolV1Messages = this.messages; + if (!this.messagesV2) { + throw ERRORS.TypedError( + HardwareErrorCode.TransportNotConfigured, + 'Protocol V2 schema not configured' + ); + } + if (!protocolV1Messages) { + throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured); + } + + let session = this.protocolV2Sessions.get(path); + if (!session) { + session = new ProtocolV2Session({ + schemas: { + protocolV1: protocolV1Messages, + protocolV2: this.messagesV2, + }, + router: PROTOCOL_V2_CHANNEL_USB, + writeFrame: (frame: Uint8Array) => this.transferOutOnce(path, frame), + readFrame: () => this.receiveProtocolV2Frame(path, this.protocolV2ReadTimeouts.get(path)), + logger: this.Log, + logPrefix: 'ProtocolV2 WebUSB', + createTimeoutError: (messageName: string, timeoutMs: number) => + new Error(`Protocol V2 response timeout after ${timeoutMs}ms for ${messageName}`), + }); + this.protocolV2Sessions.set(path, session); + } + + this.protocolV2ReadTimeouts.set(path, options?.timeoutMs); + this.protocolV2Assemblers.get(path)?.reset(); + try { + return await session.call(name, data, options); + } finally { + this.protocolV2ReadTimeouts.delete(path); + } + } + + private async receiveProtocolV2Frame(path: string, timeoutMs?: number): Promise { + let assembler = this.protocolV2Assemblers.get(path); + if (!assembler) { + assembler = new ProtocolV2FrameAssembler(PROTOCOL_V2_FRAME_MAX_BYTES); + this.protocolV2Assemblers.set(path, assembler); + } + + let frame: Uint8Array | undefined = assembler.push(new Uint8Array(0)); + const deadline = timeoutMs ? Date.now() + timeoutMs : undefined; + + while (!frame) { + const cancelToken = { cancelled: false }; + const transferIn = this.transferInWithRetry(path, PROTOCOL_V2_FRAME_MAX_BYTES, cancelToken); + const dataView = deadline + ? await this.withProtocolReadTimeout( + path, + transferIn, + Math.max(deadline - Date.now(), 1), + 'V2', + () => { + cancelToken.cancelled = true; + } + ) + : await transferIn; + const bytes = new Uint8Array( + this.toArrayBuffer( + dataView.buffer.slice(dataView.byteOffset, dataView.byteOffset + dataView.byteLength) + ) + ); + try { + frame = assembler.push(bytes); + } catch (error) { + throw ERRORS.TypedError( + HardwareErrorCode.NetworkError, + error instanceof Error ? error.message : String(error) + ); + } + } + return frame; + } + /** * Receive data from device */ - async receiveData(path: string) { - const firstPacketData = await this.transferInWithRetry(path, PACKET_SIZE); + async receiveData(path: string, timeoutMs?: number) { + const deadline = timeoutMs ? Date.now() + timeoutMs : undefined; + const readPacket = async () => { + const cancelToken = { cancelled: false }; + const transferIn = this.transferInWithRetry(path, PACKET_SIZE, cancelToken); + return deadline + ? this.withProtocolReadTimeout( + path, + transferIn, + Math.max(deadline - Date.now(), 1), + 'V1', + () => { + cancelToken.cancelled = true; + } + ) + : transferIn; + }; + + const firstPacketData = await readPacket(); const firstData = this.toArrayBuffer(firstPacketData.buffer.slice(1)); - const { length, typeId, restBuffer } = decodeProtocol.decodeChunked(firstData); + const { length, typeId, restBuffer } = ProtocolV1.decodeFirstChunk(firstData); // eslint-disable-next-line @typescript-eslint/restrict-plus-operands const lengthWithHeader = Number(length + HEADER_LENGTH); @@ -404,9 +901,9 @@ export default class WebUsbTransport { } while (decoded.offset < lengthWithHeader) { - const packetData = await this.transferInWithRetry(path, PACKET_SIZE); + const packetData = await readPacket(); const buffer = this.toArrayBuffer(packetData.buffer.slice(1)); - if (lengthWithHeader - decoded.offset >= PACKET_SIZE) { + if (lengthWithHeader - decoded.offset >= PAYLOAD_SIZE) { decoded.append(buffer); } else { decoded.append(buffer.slice(0, lengthWithHeader - decoded.offset)); @@ -422,7 +919,22 @@ export default class WebUsbTransport { */ async release(path: string) { const device: USBDevice = await this.findDevice(path); - await device.releaseInterface(this.interfaceId); + const endpoints = this.deviceEndpoints.get(path); + const ifaceNum = endpoints?.interfaceNumber ?? this.interfaceId; + await device.releaseInterface(ifaceNum); await device.close(); + this.deviceProtocol.delete(path); + this.deviceProtocolHints.delete(path); + this.protocolV2Assemblers.get(path)?.reset(); + this.protocolV2Assemblers.delete(path); + this.deviceEndpoints.delete(path); + } + + /** + * Expose the detected protocol type for a given device path. + * Used by upper layers (e.g. TransportManager) to select the correct schema. + */ + getProtocolType(path: string): ProtocolType | undefined { + return this.deviceProtocol.get(path); } } diff --git a/packages/hd-transport/__tests__/build-receive.test.js b/packages/hd-transport/__tests__/build-receive.test.js index c22acf980..0eb3c6f70 100644 --- a/packages/hd-transport/__tests__/build-receive.test.js +++ b/packages/hd-transport/__tests__/build-receive.test.js @@ -1,7 +1,5 @@ const { parseConfigure } = require('../src/serialization/protobuf/messages'); -const { buildOne } = require('../src/serialization/send'); -const { receiveOne } = require('../src/serialization/receive'); -const { buildEncodeBuffers } = require('../src/serialization/send'); +const { ProtocolV1 } = require('../src/protocols'); const messages = { StellarPaymentOp: { @@ -96,17 +94,17 @@ const parsedMessages = parseConfigure({ describe('encoding json -> protobuf -> json', () => { fixtures.forEach(f => { describe(f.name, () => { - test('buildOne - receiveOne', () => { + test('encodeEnvelope - decodeMessage', () => { // encoded message - const encodedMessage = buildOne(parsedMessages, f.name, f.in); + const encodedMessage = ProtocolV1.encodeEnvelope(parsedMessages, f.name, f.in); // then decode message and check, whether decoded message matches original json - const decodedMessage = receiveOne(parsedMessages, encodedMessage); + const decodedMessage = ProtocolV1.decodeMessage(parsedMessages, encodedMessage); expect(decodedMessage.type).toEqual(f.name); expect(decodedMessage.message).toEqual(f.in); }); - test('buildBuffers - receiveAndParse', () => { - const result = buildEncodeBuffers(parsedMessages, f.name, f.in); + test('encodeMessageChunks - receiveAndParse', () => { + const result = ProtocolV1.encodeMessageChunks(parsedMessages, f.name, f.in); result.forEach(r => { expect(r.byteLength).toBeLessThanOrEqual(63); }); diff --git a/packages/hd-transport/__tests__/decode-features.test.js b/packages/hd-transport/__tests__/decode-features.test.js index 15b0b0b78..b24e18f60 100644 --- a/packages/hd-transport/__tests__/decode-features.test.js +++ b/packages/hd-transport/__tests__/decode-features.test.js @@ -1,8 +1,9 @@ +/* eslint-disable import/order */ const ProtoBuf = require('protobufjs/light'); const ByteBuffer = require('bytebuffer'); const { decode } = require('../src/serialization/protobuf/decode'); -const { decode: decodeProtocol } = require('../src/serialization/protocol/decode'); +const { decodeEnvelope } = require('../src/protocols/v1/decode'); // Reuse the messages.json already committed alongside @onekeyfe/hd-core // (runtime data for DataManager). hd-transport's own messages.json is @@ -61,7 +62,7 @@ describe('Fix messages decode', () => { test('decode', () => { // deserialize const encoded = ByteBuffer.fromHex(f.encodeMessage); - const { buffer } = decodeProtocol(encoded); + const { buffer } = decodeEnvelope(encoded); const decoded = decode(Message, buffer); // filter null values diff --git a/packages/hd-transport/__tests__/messages.test.js b/packages/hd-transport/__tests__/messages.test.js index 0d65c68b8..9193fb5ff 100644 --- a/packages/hd-transport/__tests__/messages.test.js +++ b/packages/hd-transport/__tests__/messages.test.js @@ -3,6 +3,10 @@ const { createMessageFromType, parseConfigure, } = require('../src/serialization/protobuf/messages'); +const v1Messages = require('../../core/src/data/messages/messages.json'); +const v2Messages = require('../messages-protocol-v2.json'); +const coreV2Messages = require('../../core/src/data/messages/messages-protocol-v2.json'); + // const json = require('./data/messages.json'); const json = { @@ -58,6 +62,71 @@ const json = { }; describe('messages', () => { + test('V1 GetPassphraseState matches the current firmware schema', () => { + const { fields } = v1Messages.nested.GetPassphraseState; + + expect(fields.passphrase_state).toMatchObject({ id: 1, type: 'string' }); + expect(fields).not.toHaveProperty('_only_main_pin'); + expect(fields).not.toHaveProperty('allow_create_attach_pin'); + }); + + test('Protocol V2 firmware targets match the current firmware-pro2 enum', () => { + expect(v2Messages.nested.DeviceFirmwareTargetType.values).toEqual({ + FW_MGMT_TARGET_INVALID: 0, + FW_MGMT_TARGET_CRATE: 1, + FW_MGMT_TARGET_ROMLOADER: 2, + FW_MGMT_TARGET_BOOTLOADER: 3, + FW_MGMT_TARGET_APPLICATION_P1: 4, + FW_MGMT_TARGET_APPLICATION_P2: 5, + FW_MGMT_TARGET_COPROCESSOR: 6, + FW_MGMT_TARGET_SE01: 7, + FW_MGMT_TARGET_SE02: 8, + FW_MGMT_TARGET_SE03: 9, + FW_MGMT_TARGET_SE04: 10, + }); + }); + + test('Protocol V2 device status and session messages match firmware-pro2', () => { + expect(v2Messages.nested.MessageType.values).toMatchObject({ + MessageType_DeviceStatusGet: 60602, + MessageType_DeviceStatus: 60603, + MessageType_DeviceSessionGet: 60606, + MessageType_DeviceSession: 60607, + MessageType_DeviceSessionAskPin: 60608, + }); + expect(v2Messages.nested.DeviceStatusGet).toEqual({ fields: {} }); + expect(v2Messages.nested.DeviceSessionGet.fields.session_id).toMatchObject({ + id: 1, + type: 'bytes', + }); + expect(v2Messages.nested.DeviceSession.fields).toMatchObject({ + session_id: { id: 1, type: 'bytes' }, + btc_test_address: { id: 2, type: 'string' }, + }); + expect(v2Messages.nested.DeviceSessionAskPin).toEqual({ fields: {} }); + expect(v2Messages.nested.DeviceSessionAskPin_FailureSubCodes.values).toEqual({ + UserCancel: 1, + }); + expect(v2Messages.nested.MessageType.values).not.toHaveProperty( + 'MessageType_DeviceSessionPinResult' + ); + }); + + test('Protocol V2 does not restore retired unlock or passphrase ids', () => { + expect(v2Messages.nested.MessageType.values).not.toHaveProperty('MessageType_UnLockDevice'); + expect(v2Messages.nested.MessageType.values).not.toHaveProperty( + 'MessageType_UnLockDeviceResponse' + ); + expect(v2Messages.nested.MessageType.values).not.toHaveProperty( + 'MessageType_GetPassphraseState' + ); + expect(v2Messages.nested.MessageType.values).not.toHaveProperty('MessageType_PassphraseState'); + }); + + test('Protocol V2 transport and core schemas stay identical', () => { + expect(coreV2Messages).toEqual(v2Messages); + }); + test('createMessageFromName (common case)', () => { const messages = parseConfigure(json); const name = 'Initialize'; @@ -70,6 +139,14 @@ describe('messages', () => { expect(() => createMessageFromType(messages, 0)).not.toThrow(); }); + test('createMessageFromType throws a readable error for unknown ids', () => { + const messages = parseConfigure(json); + + expect(() => createMessageFromType(messages, 99999)).toThrow( + 'MessageType id "99999" is not defined in protobuf schema' + ); + }); + test('createMessageFromName (wire_type case)', () => { const messages = parseConfigure(json); const name = 'TxAckInput'; @@ -83,4 +160,12 @@ describe('messages', () => { expect(() => createMessageFromName(messages, name)).not.toThrow(); }); + + test('createMessageFromName throws when message type id is missing', () => { + const messages = parseConfigure(json); + + expect(() => createMessageFromName(messages, 'TxAckInputWrapper')).toThrow( + 'MessageType for "TxAckInputWrapper" is not defined in protobuf schema' + ); + }); }); diff --git a/packages/hd-transport/__tests__/protocol-v2-link-manager.test.js b/packages/hd-transport/__tests__/protocol-v2-link-manager.test.js new file mode 100644 index 000000000..f57bce795 --- /dev/null +++ b/packages/hd-transport/__tests__/protocol-v2-link-manager.test.js @@ -0,0 +1,326 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { ProtocolV2, ProtocolV2LinkManager } = require('../src'); +const { parseConfigure } = require('../src/serialization/protobuf/messages'); +const protocolV2 = require('../src/protocols/v2'); + +const protocolV1Messages = parseConfigure({ + nested: { + Failure: { + fields: { + code: { type: 'uint32', id: 1 }, + message: { type: 'string', id: 2 }, + }, + }, + MessageType: { + values: { + MessageType_Failure: 3, + }, + }, + }, +}); + +const protocolV2Messages = parseConfigure({ + nested: { + Ping: { + fields: { + message: { type: 'string', id: 1 }, + }, + }, + Success: { + fields: { + message: { type: 'string', id: 1 }, + }, + }, + MessageType: { + values: { + MessageType_Ping: 60206, + MessageType_Success: 60207, + }, + }, + }, +}); + +const schemas = { + protocolV1: protocolV1Messages, + protocolV2: protocolV2Messages, +}; + +const rewriteSeq = (frame, seq) => { + const copy = new Uint8Array(frame); + copy[6] = seq; + copy[copy.length - 1] = protocolV2.crc8(copy, copy.length - 1); + return copy; +}; + +const createAdapterFactory = sentSeqs => { + const adapters = []; + let generation = 0; + const success = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' }); + + return { + adapters, + createAdapter: () => { + let requestSeq = 0; + const adapter = { + router: 1, + generation: ++generation, + prepareCall: jest.fn(), + writeFrame: jest.fn(frame => { + const [, , , , , , seq] = frame; + requestSeq = seq; + sentSeqs.push(requestSeq); + return Promise.resolve(); + }), + readFrame: jest.fn(() => Promise.resolve(rewriteSeq(success, requestSeq))), + reset: jest.fn(() => Promise.resolve()), + }; + adapters.push(adapter); + return adapter; + }, + }; +}; + +describe('ProtocolV2LinkManager', () => { + test('retains the device sequence cursor when an active link is rebuilt', async () => { + const sentSeqs = []; + const { adapters, createAdapter } = createAdapterFactory(sentSeqs); + const manager = new ProtocolV2LinkManager({ + getSchemas: () => schemas, + classifyError: () => 'recoverable', + }); + + await manager.call('device-a', createAdapter, 'Ping', { message: '1' }); + await manager.invalidateLink('device-a', 'reconnect'); + await manager.call('device-a', createAdapter, 'Ping', { message: '2' }); + + expect(sentSeqs).toEqual([1, 2]); + expect(adapters).toHaveLength(2); + expect(adapters[0].reset).toHaveBeenCalledWith('reconnect'); + }); + + test('prepares the active adapter with the same call context used for frame IO', async () => { + const sentSeqs = []; + const { adapters, createAdapter } = createAdapterFactory(sentSeqs); + const manager = new ProtocolV2LinkManager({ + getSchemas: () => schemas, + classifyError: () => 'recoverable', + }); + + await manager.call('device-a', createAdapter, 'Ping', { message: '1' }, { timeoutMs: 123 }); + + expect(adapters[0].prepareCall).toHaveBeenCalledWith({ + messageName: 'Ping', + timeoutMs: 123, + highVolume: false, + generation: 1, + }); + }); + + test('invalidates a link-fatal call before the next call creates a new link', async () => { + const sentSeqs = []; + const onLinkInvalidated = jest.fn(); + const { adapters, createAdapter: createWorkingAdapter } = createAdapterFactory(sentSeqs); + const createAdapter = jest + .fn() + .mockImplementationOnce(() => { + const adapter = createWorkingAdapter(); + adapter.writeFrame.mockImplementationOnce(frame => { + sentSeqs.push(frame[6]); + return Promise.reject(new Error('transport write failed')); + }); + return adapter; + }) + .mockImplementation(() => createWorkingAdapter()); + const manager = new ProtocolV2LinkManager({ + getSchemas: () => schemas, + classifyError: () => 'link-fatal', + onLinkInvalidated, + }); + + await expect(manager.call('device-a', createAdapter, 'Ping', { message: '1' })).rejects.toThrow( + 'transport write failed' + ); + await expect( + manager.call('device-a', createAdapter, 'Ping', { message: '2' }) + ).resolves.toEqual({ + type: 'Success', + message: { message: 'ok' }, + }); + + expect(createAdapter).toHaveBeenCalledTimes(2); + expect(adapters[0].reset).toHaveBeenCalledWith( + expect.stringContaining('transport write failed') + ); + expect(onLinkInvalidated).toHaveBeenCalledWith( + 'device-a', + expect.stringContaining('transport write failed') + ); + expect(sentSeqs).toEqual([1, 2]); + }); + + test('invalidates every active link while retaining per-device cursors', async () => { + const sentSeqs = []; + const { adapters, createAdapter } = createAdapterFactory(sentSeqs); + const manager = new ProtocolV2LinkManager({ + getSchemas: () => schemas, + classifyError: () => 'recoverable', + }); + + await manager.call('device-a', createAdapter, 'Ping', { message: 'a1' }); + await manager.call('device-b', createAdapter, 'Ping', { message: 'b1' }); + await manager.invalidateAllLinks('schema changed'); + await manager.call('device-a', createAdapter, 'Ping', { message: 'a2' }); + await manager.call('device-b', createAdapter, 'Ping', { message: 'b2' }); + + expect(sentSeqs).toEqual([1, 1, 2, 2]); + expect(adapters).toHaveLength(4); + expect(adapters[0].reset).toHaveBeenCalledWith('schema changed'); + expect(adapters[1].reset).toHaveBeenCalledWith('schema changed'); + }); + + test('dispose clears active links and sequence cursors', async () => { + const sentSeqs = []; + const { adapters, createAdapter } = createAdapterFactory(sentSeqs); + const manager = new ProtocolV2LinkManager({ + getSchemas: () => schemas, + classifyError: () => 'recoverable', + }); + + await manager.call('device-a', createAdapter, 'Ping', { message: '1' }); + await manager.dispose('transport disposed'); + await manager.call('device-a', createAdapter, 'Ping', { message: '2' }); + + expect(sentSeqs).toEqual([1, 1]); + expect(adapters[0].reset).toHaveBeenCalledWith('transport disposed'); + }); + + test('serializes calls per device without blocking another device', async () => { + const events = []; + let releaseDeviceA; + const deviceABlocked = new Promise(resolve => { + releaseDeviceA = resolve; + }); + const success = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' }); + const createAdapter = key => { + let requestSeq = 0; + return { + router: 1, + generation: 1, + prepareCall: jest.fn(), + writeFrame: jest.fn(frame => { + const [, , , , , , seq] = frame; + requestSeq = seq; + events.push(`write:${key}:${requestSeq}`); + return Promise.resolve(); + }), + readFrame: jest.fn(async () => { + if (key === 'device-a' && requestSeq === 1) { + await deviceABlocked; + } + events.push(`read:${key}:${requestSeq}`); + return rewriteSeq(success, requestSeq); + }), + reset: jest.fn(), + }; + }; + const manager = new ProtocolV2LinkManager({ + getSchemas: () => schemas, + classifyError: () => 'recoverable', + }); + + const firstA = manager.call('device-a', () => createAdapter('device-a'), 'Ping', { + message: 'a1', + }); + const secondA = manager.call('device-a', () => createAdapter('device-a'), 'Ping', { + message: 'a2', + }); + const firstB = manager.call('device-b', () => createAdapter('device-b'), 'Ping', { + message: 'b1', + }); + + await firstB; + expect(events).toEqual(['write:device-a:1', 'write:device-b:1', 'read:device-b:1']); + releaseDeviceA(); + await Promise.all([firstA, secondA]); + expect(events).toEqual([ + 'write:device-a:1', + 'write:device-b:1', + 'read:device-b:1', + 'read:device-a:1', + 'write:device-a:2', + 'read:device-a:2', + ]); + }); + + test('keeps a recoverable link after a call error', async () => { + const sentSeqs = []; + const { adapters, createAdapter } = createAdapterFactory(sentSeqs); + const manager = new ProtocolV2LinkManager({ + getSchemas: () => schemas, + classifyError: () => 'recoverable', + }); + const adapter = createAdapter(); + adapter.writeFrame.mockImplementationOnce(frame => { + sentSeqs.push(frame[6]); + return Promise.reject(new Error('application retryable error')); + }); + const factory = jest.fn(() => adapter); + + await expect(manager.call('device-a', factory, 'Ping', { message: '1' })).rejects.toThrow( + 'application retryable error' + ); + await manager.call('device-a', factory, 'Ping', { message: '2' }); + + expect(factory).toHaveBeenCalledTimes(1); + expect(adapter.reset).not.toHaveBeenCalled(); + expect(sentSeqs).toEqual([1, 2]); + }); + + test('removes a settled per-device call queue', async () => { + const sentSeqs = []; + const { createAdapter } = createAdapterFactory(sentSeqs); + const manager = new ProtocolV2LinkManager({ + getSchemas: () => schemas, + classifyError: () => 'recoverable', + }); + + await manager.call('device-a', createAdapter, 'Ping', { message: 'done' }); + + expect(manager.callQueues.size).toBe(0); + }); + + test('stops an in-flight call before reading when its link is invalidated during write', async () => { + let releaseWrite; + let markWriteStarted; + const writeStarted = new Promise(resolve => { + markWriteStarted = resolve; + }); + const writeBlocked = new Promise(resolve => { + releaseWrite = resolve; + }); + const success = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' }); + const adapter = { + router: 1, + generation: 1, + prepareCall: jest.fn(), + writeFrame: jest.fn(async () => { + markWriteStarted(); + await writeBlocked; + }), + readFrame: jest.fn(() => Promise.resolve(success)), + reset: jest.fn(), + }; + const manager = new ProtocolV2LinkManager({ + getSchemas: () => schemas, + classifyError: () => 'recoverable', + }); + + const call = manager.call('device-a', () => adapter, 'Ping', { message: 'pending' }); + await writeStarted; + await manager.invalidateLink('device-a', 'device disconnected'); + releaseWrite(); + + await expect(call).rejects.toThrow('device disconnected'); + expect(adapter.readFrame).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/hd-transport/__tests__/protocol-v2-usb-transport-base.test.js b/packages/hd-transport/__tests__/protocol-v2-usb-transport-base.test.js new file mode 100644 index 000000000..f2b6c9a38 --- /dev/null +++ b/packages/hd-transport/__tests__/protocol-v2-usb-transport-base.test.js @@ -0,0 +1,296 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const { + PROTOCOL_V2_CHANNEL_USB, + PROTOCOL_V2_FRAME_MAX_BYTES, + ProtocolV2, + ProtocolV2UsbTransportBase, +} = require('../src'); +const { parseConfigure } = require('../src/serialization/protobuf/messages'); + +const protocolV1Messages = parseConfigure({ + nested: { + Failure: { + fields: { + code: { type: 'uint32', id: 1 }, + message: { type: 'string', id: 2 }, + }, + }, + MessageType: { + values: { + MessageType_Failure: 3, + }, + }, + }, +}); + +const protocolV2Messages = parseConfigure({ + nested: { + Ping: { + fields: { + message: { type: 'string', id: 1 }, + }, + }, + Success: { + fields: { + message: { type: 'string', id: 1 }, + }, + }, + MessageType: { + values: { + MessageType_Ping: 60206, + MessageType_Success: 60207, + }, + }, + }, +}); + +const schemas = { + protocolV1: protocolV1Messages, + protocolV2: protocolV2Messages, +}; + +class FakeUsbTransport extends ProtocolV2UsbTransportBase { + constructor() { + super({ + router: PROTOCOL_V2_CHANNEL_USB, + maxFrameBytes: PROTOCOL_V2_FRAME_MAX_BYTES, + logPrefix: 'ProtocolV2 FakeUSB', + }); + this.sentSeqs = []; + this.readContexts = []; + this.readCounts = new Map(); + this.packetQueues = new Map(); + this.nativeResets = []; + this.invalidations = []; + this.nextReadError = undefined; + this.writeBlock = undefined; + this.readBlock = undefined; + } + + callDevice(key, message, timeoutMs) { + return this.callProtocolV2Usb( + key, + 'Ping', + { message }, + timeoutMs === undefined ? undefined : { timeoutMs } + ); + } + + rotate(key, reason = 'USB connection rotated') { + return this.rotateProtocolV2UsbGeneration(key, reason); + } + + invalidate(key, reason) { + return this.invalidateProtocolV2UsbLink(key, reason); + } + + dispose(reason) { + return this.disposeProtocolV2UsbLinks(reason); + } + + failNextRead(error) { + this.nextReadError = error; + } + + blockNextWrite() { + let release; + let markStarted; + const started = new Promise(resolve => { + markStarted = resolve; + }); + const blocked = new Promise(resolve => { + release = resolve; + }); + this.writeBlock = { blocked, markStarted }; + return { started, release }; + } + + blockNextRead() { + let release; + let markStarted; + const started = new Promise(resolve => { + markStarted = resolve; + }); + const blocked = new Promise(resolve => { + release = resolve; + }); + this.readBlock = { blocked, markStarted }; + return { started, release }; + } + + getProtocolV2UsbSchemas() { + return schemas; + } + + getProtocolV2UsbLogger() { + return undefined; + } + + async writeProtocolV2UsbPacket(key, frame) { + const seq = frame[6]; + this.sentSeqs.push([key, seq]); + const response = ProtocolV2.encodeFrame( + schemas, + 'Success', + { message: 'ok' }, + { router: PROTOCOL_V2_CHANNEL_USB, seq } + ); + const splitAt = Math.min(4, response.length); + this.packetQueues.set(key, [response.slice(0, splitAt), response.slice(splitAt)]); + + const block = this.writeBlock; + if (block) { + this.writeBlock = undefined; + block.markStarted(); + await block.blocked; + } + } + + readProtocolV2UsbPacket(key, context) { + this.readContexts.push([key, context.timeoutMs]); + this.readCounts.set(key, Number(this.readCounts.get(key) ?? 0) + 1); + if (this.nextReadError) { + const error = this.nextReadError; + this.nextReadError = undefined; + return Promise.reject(error); + } + const readPacket = async () => { + const block = this.readBlock; + if (block) { + this.readBlock = undefined; + block.markStarted(); + await block.blocked; + } + const packet = this.packetQueues.get(key)?.shift(); + if (!packet) throw new Error(`No queued USB packet for ${key}`); + return packet; + }; + return readPacket(); + } + + resetProtocolV2UsbNativeLink(key, reason) { + this.nativeResets.push([key, reason]); + return Promise.resolve(); + } + + onProtocolV2UsbLinkInvalidated(key, reason) { + this.invalidations.push([key, reason]); + } + + createProtocolV2UsbTimeoutError(name, timeoutMs) { + return new Error(`USB timeout after ${timeoutMs}ms for ${name}`); + } +} + +describe('ProtocolV2UsbTransportBase', () => { + test('keeps seq across USB generation rotation', async () => { + const transport = new FakeUsbTransport(); + + await transport.rotate('device-a'); + await transport.callDevice('device-a', 'first'); + await transport.rotate('device-a', 'USB reconnected'); + await transport.callDevice('device-a', 'second'); + + expect(transport.sentSeqs).toEqual([ + ['device-a', 1], + ['device-a', 2], + ]); + expect(transport.nativeResets).toEqual([['device-a', 'USB reconnected']]); + }); + + test('passes each queued call its own timeout context', async () => { + const transport = new FakeUsbTransport(); + await transport.rotate('device-a'); + + await Promise.all([ + transport.callDevice('device-a', 'first', 111), + transport.callDevice('device-a', 'second', 222), + ]); + + expect(transport.readContexts).toEqual([ + ['device-a', 111], + ['device-a', 111], + ['device-a', 222], + ['device-a', 222], + ]); + }); + + test('does not block a different USB path', async () => { + const transport = new FakeUsbTransport(); + await transport.rotate('device-a'); + await transport.rotate('device-b'); + const blockedWrite = transport.blockNextWrite(); + + const callA = transport.callDevice('device-a', 'blocked'); + await blockedWrite.started; + await expect(transport.callDevice('device-b', 'parallel')).resolves.toMatchObject({ + type: 'Success', + }); + blockedWrite.release(); + await expect(callA).resolves.toMatchObject({ type: 'Success' }); + }); + + test('invalidates an in-flight generation before it can read', async () => { + const transport = new FakeUsbTransport(); + await transport.rotate('device-a'); + const blockedWrite = transport.blockNextWrite(); + + const call = transport.callDevice('device-a', 'blocked'); + await blockedWrite.started; + await transport.rotate('device-a', 'USB generation changed'); + blockedWrite.release(); + + await expect(call).rejects.toThrow('USB generation changed'); + expect(transport.readCounts.get('device-a') ?? 0).toBe(0); + }); + + test('rejects an in-flight packet read without waiting for native IO to settle', async () => { + const transport = new FakeUsbTransport(); + await transport.rotate('device-a'); + const blockedRead = transport.blockNextRead(); + + const call = transport.callDevice('device-a', 'blocked-read', 5000); + const outcome = call.then( + () => 'resolved', + error => error.message + ); + await blockedRead.started; + await transport.invalidate('device-a', 'USB transport released'); + const settled = await Promise.race([ + outcome, + new Promise(resolve => { + setTimeout(() => resolve('still pending'), 50); + }), + ]); + blockedRead.release(); + + expect(settled).toContain('USB transport released'); + }); + + test('invalidates the native link after a packet read failure', async () => { + const transport = new FakeUsbTransport(); + await transport.rotate('device-a'); + transport.failNextRead(new Error('USB transfer failed')); + + await expect(transport.callDevice('device-a', 'failure')).rejects.toThrow( + 'USB transfer failed' + ); + expect(transport.nativeResets).toEqual([ + ['device-a', expect.stringContaining('USB transfer failed')], + ]); + expect(transport.invalidations).toEqual([ + ['device-a', expect.stringContaining('USB transfer failed')], + ]); + }); + + test('dispose clears the cursor and restarts seq from one', async () => { + const transport = new FakeUsbTransport(); + await transport.rotate('device-a'); + await transport.callDevice('device-a', 'before-dispose'); + await transport.dispose('transport disposed'); + await transport.rotate('device-a'); + await transport.callDevice('device-a', 'after-dispose'); + + expect(transport.sentSeqs.map(([, seq]) => seq)).toEqual([1, 1]); + }); +}); diff --git a/packages/hd-transport/__tests__/protocol-v2.test.js b/packages/hd-transport/__tests__/protocol-v2.test.js new file mode 100644 index 000000000..25a0b99b7 --- /dev/null +++ b/packages/hd-transport/__tests__/protocol-v2.test.js @@ -0,0 +1,899 @@ +const { ProtocolV2 } = require('../src/protocols'); +const { parseConfigure } = require('../src/serialization/protobuf/messages'); +const { + ProtocolV2FrameAssembler, + ProtocolV2SequenceCursor, + ProtocolV2Session, + hexToBytes, + probeProtocolV2, +} = require('../src/protocols/v2/session'); +const protocolV2 = require('../src/protocols/v2'); + +const protocolV1Messages = parseConfigure({ + nested: { + Success: { + fields: { + message: { + type: 'string', + id: 1, + }, + }, + }, + Failure: { + fields: { + code: { + type: 'uint32', + id: 1, + }, + message: { + type: 'string', + id: 2, + }, + }, + }, + ButtonRequest: { + fields: { + code: { + type: 'uint32', + id: 1, + }, + }, + }, + OnekeyGetFeatures: { + fields: {}, + }, + OnekeyFeatures: { + fields: {}, + }, + MessageType: { + values: { + MessageType_Success: 2, + MessageType_Failure: 3, + MessageType_ButtonRequest: 26, + MessageType_OnekeyGetFeatures: 10025, + MessageType_OnekeyFeatures: 10026, + }, + }, + }, +}); + +const protocolV2Messages = parseConfigure({ + nested: { + ProtocolInfoRequest: { + fields: {}, + }, + ProtocolInfo: { + fields: { + version: { + type: 'uint32', + id: 1, + }, + supported_messages: { + type: 'uint32', + id: 2, + rule: 'repeated', + }, + protobuf_definition: { + type: 'string', + id: 3, + }, + }, + }, + Ping: { + fields: { + message: { + type: 'string', + id: 1, + }, + }, + }, + Success: { + fields: { + message: { + type: 'string', + id: 1, + }, + }, + }, + DeviceFirmwareTarget: { + fields: { + target_id: { + type: 'uint32', + id: 1, + }, + path: { + type: 'string', + id: 2, + }, + }, + }, + DeviceFirmwareUpdateRequest: { + fields: { + targets: { + type: 'DeviceFirmwareTarget', + id: 1, + rule: 'repeated', + }, + }, + }, + DeviceFirmwareUpdateRecord: { + fields: { + target_id: { + type: 'uint32', + id: 1, + }, + status: { + type: 'uint32', + id: 10, + }, + payload_version: { + type: 'uint32', + id: 20, + }, + path: { + type: 'string', + id: 30, + }, + }, + }, + DeviceFirmwareUpdateStatus: { + fields: { + records: { + type: 'DeviceFirmwareUpdateRecord', + id: 1, + rule: 'repeated', + }, + }, + }, + FileWrite: { + fields: {}, + }, + PartialNested: { + fields: { + child: { + type: 'NestedChild', + id: 1, + }, + label: { + type: 'string', + id: 2, + }, + }, + }, + NestedChild: { + fields: { + value: { + type: 'string', + id: 1, + }, + }, + }, + MessageType: { + values: { + MessageType_ProtocolInfoRequest: 60200, + MessageType_ProtocolInfo: 60201, + MessageType_Ping: 60206, + MessageType_Success: 60207, + MessageType_FileWrite: 60805, + MessageType_DeviceFirmwareUpdateRequest: 61000, + MessageType_DeviceFirmwareUpdateStatus: 61002, + MessageType_PartialNested: 62000, + }, + }, + }, +}); + +const schemas = { + protocolV1: protocolV1Messages, + protocolV2: protocolV2Messages, +}; + +const rewriteSeq = (frame, seq) => { + const copy = new Uint8Array(frame); + copy[6] = seq; + copy[copy.length - 1] = protocolV2.crc8(copy, copy.length - 1); + return copy; +}; + +describe('Protocol V2 framing and session', () => { + test('encodes and decodes Protocol V2 protobuf frames', () => { + const frame = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', { + version: 1, + supported_messages: [60200, 60201], + protobuf_definition: 'proto', + }); + + const parsed = protocolV2.decodeFrame(frame); + expect(parsed.messageTypeId).toBe(60201); + + const decoded = ProtocolV2.decodeFrame(schemas, frame); + expect(decoded).toEqual({ + type: 'ProtocolInfo', + messageName: 'ProtocolInfo', + messageTypeId: 60201, + pbPayload: parsed.pbPayload, + seq: parsed.seq, + message: { + version: 1, + supported_messages: [60200, 60201], + protobuf_definition: 'proto', + }, + }); + }); + + test('does not encode V1-only messages into Protocol V2 frames', () => { + expect(() => ProtocolV2.encodeFrame(schemas, 'Ping', { message: 'ok' })).not.toThrow(); + expect(() => ProtocolV2.encodeFrame(schemas, 'OnekeyGetFeatures', {})).toThrow( + 'Protocol V2 message "OnekeyGetFeatures" is not defined' + ); + }); + + test('decodes Protocol V2 frames with the Protocol V2 catalog first', () => { + const frame = ProtocolV2.encodeFrame(schemas, 'Success', { + message: 'ok', + }); + + const parsed = protocolV2.decodeFrame(frame); + expect(parsed.messageTypeId).toBe(60207); + + const decoded = ProtocolV2.decodeFrame(schemas, frame); + expect(decoded.type).toBe('Success'); + expect(decoded.message).toEqual({ message: 'ok' }); + }); + + test('decodes missing optional nested messages as null', () => { + const frame = ProtocolV2.encodeFrame(schemas, 'PartialNested', { + label: 'only label', + }); + + const decoded = ProtocolV2.decodeFrame(schemas, frame); + expect(decoded.message).toEqual({ + child: null, + label: 'only label', + }); + }); + + test('reassembles split Protocol V2 frames and rejects oversized frames', () => { + const frame = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', { + version: 1, + supported_messages: [], + }); + const assembler = new ProtocolV2FrameAssembler(); + + expect(assembler.push(frame.slice(0, 4))).toBeUndefined(); + expect(assembler.push(frame.slice(4))).toEqual(frame); + + const oversized = new Uint8Array([0x5a, 0xff, 0xff]); + expect(() => assembler.push(oversized)).toThrow('Protocol V2 frame too large'); + }); + + test('keeps bytes after the first complete frame for the next read', () => { + const first = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', { + version: 1, + supported_messages: [], + }); + const second = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', { + version: 2, + supported_messages: [], + }); + const assembler = new ProtocolV2FrameAssembler(); + const combined = new Uint8Array(first.length + second.length); + combined.set(first, 0); + combined.set(second, first.length); + + expect(assembler.push(combined)).toEqual(first); + expect(assembler.push(new Uint8Array(0))).toEqual(second); + }); + + test('session writes one encoded frame and decodes the response frame', async () => { + const written = []; + const response = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', { + version: 2, + supported_messages: [60206], + }); + const session = new ProtocolV2Session({ + schemas, + router: 1, + writeFrame: frame => { + written.push(frame); + return Promise.resolve(); + }, + readFrame: () => + Promise.resolve(rewriteSeq(response, protocolV2.decodeFrame(written[0]).seq)), + }); + + const result = await session.call('ProtocolInfoRequest', {}); + + expect(written).toHaveLength(1); + expect(written[0][4]).toBe(1); + expect(written[0][5]).toBe(0); + expect(protocolV2.decodeFrame(written[0]).messageTypeId).toBe(60200); + expect(result).toEqual({ + type: 'ProtocolInfo', + message: { + version: 2, + supported_messages: [60206], + protobuf_definition: null, + }, + }); + }); + + test('session skips Proto Link ACK frames before decoding the protobuf response', async () => { + const ack = new Uint8Array(8); + ack[0] = 0x5a; + ack[1] = 8; + ack[2] = 0; + ack[4] = 1; + ack[5] = 1; + ack[6] = 1; + ack[3] = protocolV2.crc8(ack, 3); + ack[7] = protocolV2.crc8(ack, 7); + + const response = ProtocolV2.encodeFrame(schemas, 'Success', { + message: 'ok', + }); + const readFrame = jest.fn().mockResolvedValueOnce(ack).mockResolvedValueOnce(response); + const session = new ProtocolV2Session({ + schemas, + router: 1, + writeFrame: () => Promise.resolve(), + readFrame, + }); + + await expect(session.call('Ping', { message: 'hello' })).resolves.toEqual({ + type: 'Success', + message: { + message: 'ok', + }, + }); + expect(readFrame).toHaveBeenCalledTimes(2); + }); + + test('session rejects BLE frames above its configured frame limit before writing', async () => { + const writeFrame = jest.fn().mockResolvedValue(undefined); + const response = ProtocolV2.encodeFrame(schemas, 'Success', { + message: 'ok', + }); + const session = new ProtocolV2Session({ + schemas, + router: 1, + maxFrameBytes: 2048, + writeFrame, + readFrame: () => Promise.resolve(response), + }); + + await expect(session.call('Ping', { message: 'x'.repeat(2048) })).rejects.toThrow( + 'Protocol V2 frame too large for transport: 2061 > 2048' + ); + expect(writeFrame).not.toHaveBeenCalled(); + }); + + test('session starts response timeout after the frame is written', async () => { + const response = ProtocolV2.encodeFrame(schemas, 'Success', { + message: 'ok', + }); + const session = new ProtocolV2Session({ + schemas, + router: 1, + writeFrame: () => + new Promise(resolve => { + setTimeout(resolve, 30); + }), + readFrame: () => Promise.resolve(response), + }); + + await expect( + session.call('Ping', { message: 'hello' }, { timeoutMs: 10, expectedTypes: ['Success'] }) + ).resolves.toEqual({ + type: 'Success', + message: { + message: 'ok', + }, + }); + }); + + test('session accepts response frames with a device-owned seq without logging frame details', async () => { + const response = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', { + version: 2, + supported_messages: [], + }); + const logger = { + debug: jest.fn(), + }; + const session = new ProtocolV2Session({ + schemas, + router: 1, + writeFrame: () => Promise.resolve(), + readFrame: () => Promise.resolve(rewriteSeq(response, 200)), + logger, + }); + + await expect(session.call('ProtocolInfoRequest', {})).resolves.toEqual({ + type: 'ProtocolInfo', + message: { + version: 2, + supported_messages: [], + protobuf_definition: null, + }, + }); + expect(logger.debug).not.toHaveBeenCalled(); + }); + + test('session does not log transmit or receive payload details', async () => { + const response = ProtocolV2.encodeFrame(schemas, 'Success', { + message: 'accepted', + }); + const logger = { + debug: jest.fn(), + }; + const session = new ProtocolV2Session({ + schemas, + router: 1, + writeFrame: () => Promise.resolve(), + readFrame: () => Promise.resolve(response), + logger, + logPrefix: 'ProtocolV2 Test', + }); + + await expect(session.call('Ping', { message: 'hello' })).resolves.toEqual({ + type: 'Success', + message: { + message: 'accepted', + }, + }); + + expect(logger.debug).not.toHaveBeenCalled(); + }); + + test('session suppresses debug logs for file transfer calls', async () => { + const response = ProtocolV2.encodeFrame(schemas, 'Success', { + message: 'ok', + }); + const logger = { + debug: jest.fn(), + }; + const session = new ProtocolV2Session({ + schemas, + router: 1, + writeFrame: () => Promise.resolve(), + readFrame: () => Promise.resolve(response), + logger, + }); + + await expect(session.call('FileWrite', {})).resolves.toEqual({ + type: 'Success', + message: { + message: 'ok', + }, + }); + + expect(logger.debug).not.toHaveBeenCalled(); + }); + + test('session skips unrelated terminal frames when expected response types are provided', async () => { + const stale = ProtocolV2.encodeFrame(schemas, 'Success', { + message: 'stale response', + }); + const response = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', { + version: 2, + supported_messages: [], + }); + const logger = { + debug: jest.fn(), + }; + const readFrame = jest.fn().mockResolvedValueOnce(stale).mockResolvedValueOnce(response); + const session = new ProtocolV2Session({ + schemas, + router: 1, + writeFrame: () => Promise.resolve(), + readFrame, + logger, + }); + + await expect( + session.call('ProtocolInfoRequest', {}, { expectedTypes: ['ProtocolInfo'] }) + ).resolves.toEqual({ + type: 'ProtocolInfo', + message: { + version: 2, + supported_messages: [], + protobuf_definition: null, + }, + }); + + expect(readFrame).toHaveBeenCalledTimes(2); + expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining('skip unexpected response')); + }); + + test('session consumes intermediate response frames before returning the final response', async () => { + const written = []; + const progress = ProtocolV2.encodeFrame(schemas, 'DeviceFirmwareUpdateStatus', { + records: [{ target_id: 4, status: 1 }], + }); + const success = ProtocolV2.encodeFrame(schemas, 'Success', { + message: 'ok', + }); + const onIntermediateResponse = jest.fn(); + const readFrame = jest.fn(() => { + const [writtenFrame] = written; + const { seq } = protocolV2.decodeFrame(writtenFrame); + return Promise.resolve( + readFrame.mock.calls.length === 1 ? rewriteSeq(progress, seq) : rewriteSeq(success, seq) + ); + }); + const session = new ProtocolV2Session({ + schemas, + router: 1, + writeFrame: frame => { + written.push(frame); + return Promise.resolve(); + }, + readFrame, + }); + + const result = await session.call( + 'DeviceFirmwareUpdateRequest', + { targets: [{ target_id: 4, path: 'vol1:firmware.bin' }] }, + { + intermediateTypes: ['DeviceFirmwareUpdateStatus'], + onIntermediateResponse, + } + ); + + expect(readFrame).toHaveBeenCalledTimes(2); + expect(onIntermediateResponse).toHaveBeenCalledWith({ + type: 'DeviceFirmwareUpdateStatus', + message: { + records: [{ target_id: 4, status: 1, payload_version: null, path: null }], + }, + }); + expect(result).toEqual({ + type: 'Success', + message: { + message: 'ok', + }, + }); + }); + + test('probeProtocolV2 accepts Success as a normal V2 probe response', async () => { + await expect( + probeProtocolV2({ + call: () => Promise.resolve({ type: 'Success', message: {} }), + timeoutMs: 1, + }) + ).resolves.toBe(true); + + await expect( + probeProtocolV2({ + call: () => Promise.resolve({ type: 'Failure', message: {} }), + timeoutMs: 1, + }) + ).resolves.toBe(false); + }); + + test('decodeFrame rejects frames that are too short', () => { + expect(() => protocolV2.decodeFrame(new Uint8Array([0x5a, 0x08, 0x00]))).toThrow( + 'Protocol V2 frame too short' + ); + }); + + test('decodeFrame rejects frames with an invalid SOF byte', () => { + const frame = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' }); + const corrupted = new Uint8Array(frame); + corrupted[0] = 0x00; + expect(() => protocolV2.decodeFrame(corrupted)).toThrow('Invalid SOF byte'); + }); + + test('decodeFrame rejects frames with a header CRC mismatch', () => { + const frame = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' }); + const corrupted = new Uint8Array(frame); + corrupted[3] = (corrupted[3] + 1) % 256; + expect(() => protocolV2.decodeFrame(corrupted)).toThrow('Header CRC mismatch'); + }); + + test('decodeFrame rejects frames with a frame CRC mismatch', () => { + const frame = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' }); + const corrupted = new Uint8Array(frame); + corrupted[corrupted.length - 1] = (corrupted[corrupted.length - 1] + 1) % 256; + expect(() => protocolV2.decodeFrame(corrupted)).toThrow('Frame CRC mismatch'); + }); + + test('decodeFrame rejects frames whose payload is too short for a messageTypeId', () => { + // Raw frame with empty payload: 8 bytes of overhead, no messageTypeId. + const frame = protocolV2.encodeFrame(null); + expect(() => protocolV2.decodeFrame(frame)).toThrow('payload too short'); + }); + + test('session call rejects when no response frame arrives before the timeout', async () => { + const session = new ProtocolV2Session({ + schemas, + router: 1, + writeFrame: () => Promise.resolve(), + readFrame: () => new Promise(() => {}), + }); + + await expect(session.call('Ping', { message: 'x' }, { timeoutMs: 20 })).rejects.toThrow( + 'Protocol V2 response timeout after 20ms for Ping' + ); + }); + + test('session stops the read loop after a timeout instead of consuming later frames', async () => { + const success = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'late' }); + let resolveRead; + const readFrame = jest.fn( + () => + new Promise(resolve => { + resolveRead = resolve; + }) + ); + const session = new ProtocolV2Session({ + schemas, + router: 1, + writeFrame: () => Promise.resolve(), + readFrame, + }); + + await expect( + session.call('ProtocolInfoRequest', {}, { timeoutMs: 10, expectedTypes: ['ProtocolInfo'] }) + ).rejects.toThrow('Protocol V2 response timeout'); + + // Without cancellation the loop would skip this unexpected Success frame + // and call readFrame again, stealing frames from the next call. + resolveRead(success); + await new Promise(resolve => { + setTimeout(resolve, 20); + }); + expect(readFrame).toHaveBeenCalledTimes(1); + }); + + test('session serializes concurrent calls so responses cannot be stolen', async () => { + const events = []; + const written = []; + const session = new ProtocolV2Session({ + schemas, + router: 1, + writeFrame: frame => { + written.push(frame); + events.push(`write:${written.length}`); + return new Promise(resolve => { + setTimeout(resolve, 10); + }); + }, + readFrame: () => { + events.push(`read:${written.length}`); + const [frame] = written.slice(-1); + const { seq } = protocolV2.decodeFrame(frame); + const response = + written.length === 1 + ? ProtocolV2.encodeFrame(schemas, 'Success', { message: 'first' }) + : ProtocolV2.encodeFrame(schemas, 'Success', { message: 'second' }); + return Promise.resolve(rewriteSeq(response, seq)); + }, + }); + + const [first, second] = await Promise.all([ + session.call('Ping', { message: '1' }, { expectedTypes: ['Success'] }), + session.call('Ping', { message: '2' }, { expectedTypes: ['Success'] }), + ]); + + expect(first.message).toEqual({ message: 'first' }); + expect(second.message).toEqual({ message: 'second' }); + // The second call must not start writing before the first call finished. + expect(events).toEqual(['write:1', 'read:1', 'write:2', 'read:2']); + }); + + test('session keeps serving calls after a previous call failed', async () => { + const response = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' }); + let shouldFail = true; + const session = new ProtocolV2Session({ + schemas, + router: 1, + writeFrame: () => { + if (shouldFail) { + shouldFail = false; + return Promise.reject(new Error('transport write failed')); + } + return Promise.resolve(); + }, + readFrame: () => Promise.resolve(response), + }); + + await expect(session.call('Ping', { message: '1' })).rejects.toThrow('transport write failed'); + await expect(session.call('Ping', { message: '2' })).resolves.toEqual({ + type: 'Success', + message: { message: 'ok' }, + }); + }); + + test('session uses a per-session sequence counter starting at 1', async () => { + const written = []; + const makeSession = () => + new ProtocolV2Session({ + schemas, + router: 1, + writeFrame: frame => { + written.push(frame); + return Promise.resolve(); + }, + readFrame: () => { + const [frame] = written.slice(-1); + const { seq } = protocolV2.decodeFrame(frame); + return Promise.resolve( + rewriteSeq(ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' }), seq) + ); + }, + }); + + const sessionA = makeSession(); + await sessionA.call('Ping', { message: '1' }); + await sessionA.call('Ping', { message: '2' }); + const sessionB = makeSession(); + await sessionB.call('Ping', { message: '3' }); + + expect(written.map(frame => frame[6])).toEqual([1, 2, 1]); + }); + + test('session reuses an injected sequence cursor across recreated sessions', async () => { + const written = []; + const cursor = new ProtocolV2SequenceCursor(); + const makeSession = () => + new ProtocolV2Session({ + schemas, + router: 1, + sequenceCursor: cursor, + writeFrame: frame => { + written.push(frame); + return Promise.resolve(); + }, + readFrame: () => { + const [frame] = written.slice(-1); + const { seq } = protocolV2.decodeFrame(frame); + return Promise.resolve( + rewriteSeq(ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' }), seq) + ); + }, + }); + + await makeSession().call('Ping', { message: '1' }); + await makeSession().call('Ping', { message: '2' }); + + expect(written.map(frame => frame[6])).toEqual([1, 2]); + }); + + test('session passes per-call context to frame IO callbacks', async () => { + const writeContexts = []; + const readContexts = []; + const response = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' }); + const session = new ProtocolV2Session({ + schemas, + router: 1, + generation: 7, + writeFrame: (_frame, context) => { + writeContexts.push(context); + return Promise.resolve(); + }, + readFrame: context => { + readContexts.push(context); + return Promise.resolve(response); + }, + }); + + await session.call('Ping', { message: 'ping' }, { timeoutMs: 123 }); + await session.call('FileWrite', {}, { timeoutMs: 456 }); + + expect(writeContexts).toEqual([ + { messageName: 'Ping', timeoutMs: 123, highVolume: false, generation: 7 }, + { messageName: 'FileWrite', timeoutMs: 456, highVolume: true, generation: 7 }, + ]); + expect(readContexts).toEqual(writeContexts); + }); + + test('assembler throws and resets on frames with an impossible length field', () => { + const assembler = new ProtocolV2FrameAssembler(); + // expectedLen = 0 < 8-byte minimum: without the guard this poisons the + // buffer forever and deadlocks drain loops. + expect(() => assembler.push(new Uint8Array([0x5a, 0x00, 0x00]))).toThrow( + 'Protocol V2 frame length too small: 0' + ); + + // Buffer must have been reset so the next valid frame goes through. + const frame = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' }); + expect(assembler.push(frame)).toEqual(frame); + }); + + test('assembler validates the header CRC as soon as 4 bytes arrive', () => { + const assembler = new ProtocolV2FrameAssembler(); + const header = new Uint8Array([0x5a, 0x10, 0x00, 0x00]); + header[3] = (protocolV2.crc8(header, 3) + 1) % 256; + + expect(() => assembler.push(header)).toThrow('Protocol V2 header CRC mismatch'); + + // Buffer was reset: a valid frame parses afterwards. + const frame = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' }); + expect(assembler.push(frame)).toEqual(frame); + }); + + test('assembler drain returns every buffered complete frame', () => { + const first = ProtocolV2.encodeFrame(schemas, 'ProtocolInfo', { + version: 1, + supported_messages: [], + }); + const second = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'ok' }); + const third = ProtocolV2.encodeFrame(schemas, 'Success', { message: 'last' }); + const assembler = new ProtocolV2FrameAssembler(); + + const combined = new Uint8Array(first.length + second.length + 3); + combined.set(first, 0); + combined.set(second, first.length); + combined.set(third.slice(0, 3), first.length + second.length); + + expect(assembler.drain(combined)).toEqual([first, second]); + expect(assembler.drain()).toEqual([]); + expect(assembler.drain(third.slice(3))).toEqual([third]); + }); + + test('decodes allowlisted legacy V1 interaction messages as a fallback', () => { + // ButtonRequest only exists in the V1 schema; the V2 decoder should fall + // back to it because it is on the legacy decode allowlist. + const frame = protocolV2.encodeProtobufFrame(26, new Uint8Array(0)); + const decoded = ProtocolV2.decodeFrame(schemas, frame); + expect(decoded.type).toBe('ButtonRequest'); + }); + + test('decodes legacy V1 Failure as a Protocol V2 fallback', () => { + // Some device-side rejection paths still return legacy Failure(type=3) + // inside a Protocol V2 frame. It must surface as a device Failure, not as + // a protobuf catalog TypeError. + const frame = ProtocolV2.encodeFrame( + { ...schemas, protocolV2: schemas.protocolV1 }, + 'Failure', + { + code: 1, + message: 'Action cancelled', + } + ); + const decoded = ProtocolV2.decodeFrame(schemas, frame); + expect(decoded.type).toBe('Failure'); + expect(decoded.message).toEqual({ + code: 1, + message: 'Action cancelled', + }); + }); + + test('does not fall back to legacy V1 messages outside the allowlist', () => { + // OnekeyFeatures exists only in the V1 schema and is not allowlisted. + const frame = protocolV2.encodeProtobufFrame(10026, new Uint8Array(0)); + expect(() => ProtocolV2.decodeFrame(schemas, frame)).toThrow(); + }); + + test('hexToBytes converts valid hex and rejects malformed input', () => { + expect(hexToBytes('5a0102')).toEqual(new Uint8Array([0x5a, 0x01, 0x02])); + expect(hexToBytes('')).toEqual(new Uint8Array(0)); + expect(() => hexToBytes('abc')).toThrow('Invalid hex string: odd length'); + expect(() => hexToBytes('zz')).toThrow('contains non-hex characters'); + }); + + test('probeProtocolV2 only uses Ping for acquire probing', async () => { + const call = jest.fn().mockRejectedValue(new Error('Ping timeout')); + const onProbeFailed = jest.fn(); + + await expect( + probeProtocolV2({ + call, + timeoutMs: 1, + onProbeFailed, + }) + ).resolves.toBe(false); + expect(call).toHaveBeenNthCalledWith( + 1, + 'Ping', + { message: 'protocol-v2-probe' }, + { + timeoutMs: 1, + expectedTypes: ['Success'], + } + ); + expect(call).toHaveBeenCalledTimes(1); + expect(onProbeFailed).toHaveBeenCalledWith(expect.any(Error)); + }); +}); diff --git a/packages/hd-transport/messages-protocol-v2.json b/packages/hd-transport/messages-protocol-v2.json new file mode 100644 index 000000000..6b417ad6e --- /dev/null +++ b/packages/hd-transport/messages-protocol-v2.json @@ -0,0 +1,13431 @@ +{ + "nested": { + "wire_in": { + "type": "bool", + "id": 50002, + "extend": "google.protobuf.EnumValueOptions" + }, + "wire_out": { + "type": "bool", + "id": 50003, + "extend": "google.protobuf.EnumValueOptions" + }, + "wire_debug_in": { + "type": "bool", + "id": 50004, + "extend": "google.protobuf.EnumValueOptions" + }, + "wire_debug_out": { + "type": "bool", + "id": 50005, + "extend": "google.protobuf.EnumValueOptions" + }, + "wire_tiny": { + "type": "bool", + "id": 50006, + "extend": "google.protobuf.EnumValueOptions" + }, + "wire_bootloader": { + "type": "bool", + "id": 50007, + "extend": "google.protobuf.EnumValueOptions" + }, + "wire_no_fsm": { + "type": "bool", + "id": 50008, + "extend": "google.protobuf.EnumValueOptions" + }, + "bitcoin_only": { + "type": "bool", + "id": 60000, + "extend": "google.protobuf.EnumValueOptions" + }, + "has_bitcoin_only_values": { + "type": "bool", + "id": 51001, + "extend": "google.protobuf.EnumOptions" + }, + "experimental_message": { + "type": "bool", + "id": 52001, + "extend": "google.protobuf.MessageOptions" + }, + "wire_type": { + "type": "uint32", + "id": 52002, + "extend": "google.protobuf.MessageOptions" + }, + "experimental_field": { + "type": "bool", + "id": 53001, + "extend": "google.protobuf.FieldOptions" + }, + "include_in_bitcoin_only": { + "type": "bool", + "id": 60000, + "extend": "google.protobuf.FileOptions" + }, + "MessageType": { + "options": { + "(has_bitcoin_only_values)": true + }, + "values": { + "MessageType_Initialize": 0, + "MessageType_ChangePin": 4, + "MessageType_WipeDevice": 5, + "MessageType_GetEntropy": 9, + "MessageType_Entropy": 10, + "MessageType_LoadDevice": 13, + "MessageType_ResetDevice": 14, + "MessageType_SetBusy": 16, + "MessageType_Features": 17, + "MessageType_PinMatrixRequest": 18, + "MessageType_PinMatrixAck": 19, + "MessageType_Cancel": 20, + "MessageType_LockDevice": 24, + "MessageType_ApplySettings": 25, + "MessageType_ButtonRequest": 26, + "MessageType_ButtonAck": 27, + "MessageType_ApplyFlags": 28, + "MessageType_GetNonce": 31, + "MessageType_Nonce": 33, + "MessageType_BackupDevice": 34, + "MessageType_EntropyRequest": 35, + "MessageType_EntropyAck": 36, + "MessageType_PassphraseRequest": 41, + "MessageType_PassphraseAck": 42, + "MessageType_RecoveryDevice": 45, + "MessageType_WordRequest": 46, + "MessageType_WordAck": 47, + "MessageType_GetFeatures": 55, + "MessageType_SdProtect": 79, + "MessageType_ChangeWipeCode": 82, + "MessageType_DoPreauthorized": 84, + "MessageType_PreauthorizedRequest": 85, + "MessageType_CancelAuthorization": 86, + "MessageType_GetFirmwareHash": 88, + "MessageType_FirmwareHash": 89, + "MessageType_UnlockPath": 93, + "MessageType_UnlockedPathRequest": 94, + "MessageType_SetU2FCounter": 63, + "MessageType_GetNextU2FCounter": 80, + "MessageType_NextU2FCounter": 81, + "MessageType_Deprecated_PassphraseStateRequest": 77, + "MessageType_Deprecated_PassphraseStateAck": 78, + "MessageType_GetPublicKey": 11, + "MessageType_PublicKey": 12, + "MessageType_SignTx": 15, + "MessageType_TxRequest": 21, + "MessageType_TxAck": 22, + "MessageType_GetAddress": 29, + "MessageType_Address": 30, + "MessageType_TxAckPaymentRequest": 37, + "MessageType_SignMessage": 38, + "MessageType_VerifyMessage": 39, + "MessageType_MessageSignature": 40, + "MessageType_GetOwnershipId": 43, + "MessageType_OwnershipId": 44, + "MessageType_GetOwnershipProof": 49, + "MessageType_OwnershipProof": 50, + "MessageType_AuthorizeCoinJoin": 51, + "MessageType_SignPsbt": 10052, + "MessageType_SignedPsbt": 10053, + "MessageType_CipherKeyValue": 23, + "MessageType_CipheredKeyValue": 48, + "MessageType_SignIdentity": 53, + "MessageType_SignedIdentity": 54, + "MessageType_GetECDHSessionKey": 61, + "MessageType_ECDHSessionKey": 62, + "MessageType_CosiCommit": 71, + "MessageType_CosiCommitment": 72, + "MessageType_CosiSign": 73, + "MessageType_CosiSignature": 74, + "MessageType_BatchGetPublickeys": 10016, + "MessageType_EcdsaPublicKeys": 10017, + "MessageType_DebugLinkDecision": 100, + "MessageType_DebugLinkGetState": 101, + "MessageType_DebugLinkState": 102, + "MessageType_DebugLinkStop": 103, + "MessageType_DebugLinkLog": 104, + "MessageType_DebugLinkMemoryRead": 110, + "MessageType_DebugLinkMemory": 111, + "MessageType_DebugLinkMemoryWrite": 112, + "MessageType_DebugLinkFlashErase": 113, + "MessageType_DebugLinkLayout": 9001, + "MessageType_DebugLinkReseedRandom": 9002, + "MessageType_DebugLinkRecordScreen": 9003, + "MessageType_DebugLinkEraseSdCard": 9005, + "MessageType_DebugLinkWatchLayout": 9006, + "MessageType_EthereumGetPublicKey": 450, + "MessageType_EthereumPublicKey": 451, + "MessageType_EthereumGetAddress": 56, + "MessageType_EthereumAddress": 57, + "MessageType_EthereumSignTx": 58, + "MessageType_EthereumSignTxEIP1559": 452, + "MessageType_EthereumTxRequest": 59, + "MessageType_EthereumTxAck": 60, + "MessageType_EthereumSignMessage": 64, + "MessageType_EthereumVerifyMessage": 65, + "MessageType_EthereumMessageSignature": 66, + "MessageType_EthereumSignTypedData": 464, + "MessageType_EthereumTypedDataStructRequest": 465, + "MessageType_EthereumTypedDataStructAck": 466, + "MessageType_EthereumTypedDataValueRequest": 467, + "MessageType_EthereumTypedDataValueAck": 468, + "MessageType_EthereumTypedDataSignature": 469, + "MessageType_EthereumSignTypedHash": 470, + "MessageType_EthereumGetPublicKeyOneKey": 20100, + "MessageType_EthereumPublicKeyOneKey": 20101, + "MessageType_EthereumGetAddressOneKey": 20102, + "MessageType_EthereumAddressOneKey": 20103, + "MessageType_EthereumSignTxOneKey": 20104, + "MessageType_EthereumSignTxEIP1559OneKey": 20105, + "MessageType_EthereumTxRequestOneKey": 20106, + "MessageType_EthereumTxAckOneKey": 20107, + "MessageType_EthereumSignMessageOneKey": 20108, + "MessageType_EthereumVerifyMessageOneKey": 20109, + "MessageType_EthereumMessageSignatureOneKey": 20110, + "MessageType_EthereumSignTypedDataOneKey": 20111, + "MessageType_EthereumTypedDataStructRequestOneKey": 20112, + "MessageType_EthereumTypedDataStructAckOneKey": 20113, + "MessageType_EthereumTypedDataValueRequestOneKey": 20114, + "MessageType_EthereumTypedDataValueAckOneKey": 20115, + "MessageType_EthereumTypedDataSignatureOneKey": 20116, + "MessageType_EthereumSignTypedHashOneKey": 20117, + "MessageType_EthereumGnosisSafeTxAck": 20118, + "MessageType_EthereumGnosisSafeTxRequest": 20119, + "MessageType_EthereumSignTxEIP7702OneKey": 20120, + "MessageType_EthereumSignTypedDataQR": 20121, + "MessageType_NEMGetAddress": 67, + "MessageType_NEMAddress": 68, + "MessageType_NEMSignTx": 69, + "MessageType_NEMSignedTx": 70, + "MessageType_NEMDecryptMessage": 75, + "MessageType_NEMDecryptedMessage": 76, + "MessageType_TezosGetAddress": 150, + "MessageType_TezosAddress": 151, + "MessageType_TezosSignTx": 152, + "MessageType_TezosSignedTx": 153, + "MessageType_TezosGetPublicKey": 154, + "MessageType_TezosPublicKey": 155, + "MessageType_StellarSignTx": 202, + "MessageType_StellarTxOpRequest": 203, + "MessageType_StellarGetAddress": 207, + "MessageType_StellarAddress": 208, + "MessageType_StellarCreateAccountOp": 210, + "MessageType_StellarPaymentOp": 211, + "MessageType_StellarPathPaymentStrictReceiveOp": 212, + "MessageType_StellarManageSellOfferOp": 213, + "MessageType_StellarCreatePassiveSellOfferOp": 214, + "MessageType_StellarSetOptionsOp": 215, + "MessageType_StellarChangeTrustOp": 216, + "MessageType_StellarAllowTrustOp": 217, + "MessageType_StellarAccountMergeOp": 218, + "MessageType_StellarManageDataOp": 220, + "MessageType_StellarBumpSequenceOp": 221, + "MessageType_StellarManageBuyOfferOp": 222, + "MessageType_StellarPathPaymentStrictSendOp": 223, + "MessageType_StellarSignedTx": 230, + "MessageType_CardanoGetPublicKey": 305, + "MessageType_CardanoPublicKey": 306, + "MessageType_CardanoGetAddress": 307, + "MessageType_CardanoAddress": 308, + "MessageType_CardanoTxItemAck": 313, + "MessageType_CardanoTxAuxiliaryDataSupplement": 314, + "MessageType_CardanoTxWitnessRequest": 315, + "MessageType_CardanoTxWitnessResponse": 316, + "MessageType_CardanoTxHostAck": 317, + "MessageType_CardanoTxBodyHash": 318, + "MessageType_CardanoSignTxFinished": 319, + "MessageType_CardanoSignTxInit": 320, + "MessageType_CardanoTxInput": 321, + "MessageType_CardanoTxOutput": 322, + "MessageType_CardanoAssetGroup": 323, + "MessageType_CardanoToken": 324, + "MessageType_CardanoTxCertificate": 325, + "MessageType_CardanoTxWithdrawal": 326, + "MessageType_CardanoTxAuxiliaryData": 327, + "MessageType_CardanoPoolOwner": 328, + "MessageType_CardanoPoolRelayParameters": 329, + "MessageType_CardanoGetNativeScriptHash": 330, + "MessageType_CardanoNativeScriptHash": 331, + "MessageType_CardanoTxMint": 332, + "MessageType_CardanoTxCollateralInput": 333, + "MessageType_CardanoTxRequiredSigner": 334, + "MessageType_CardanoTxInlineDatumChunk": 335, + "MessageType_CardanoTxReferenceScriptChunk": 336, + "MessageType_CardanoTxReferenceInput": 337, + "MessageType_CardanoSignMessage": 350, + "MessageType_CardanoMessageSignature": 351, + "MessageType_RippleGetAddress": 400, + "MessageType_RippleAddress": 401, + "MessageType_RippleSignTx": 402, + "MessageType_RippleSignedTx": 403, + "MessageType_MoneroTransactionInitRequest": 501, + "MessageType_MoneroTransactionInitAck": 502, + "MessageType_MoneroTransactionSetInputRequest": 503, + "MessageType_MoneroTransactionSetInputAck": 504, + "MessageType_MoneroTransactionInputViniRequest": 507, + "MessageType_MoneroTransactionInputViniAck": 508, + "MessageType_MoneroTransactionAllInputsSetRequest": 509, + "MessageType_MoneroTransactionAllInputsSetAck": 510, + "MessageType_MoneroTransactionSetOutputRequest": 511, + "MessageType_MoneroTransactionSetOutputAck": 512, + "MessageType_MoneroTransactionAllOutSetRequest": 513, + "MessageType_MoneroTransactionAllOutSetAck": 514, + "MessageType_MoneroTransactionSignInputRequest": 515, + "MessageType_MoneroTransactionSignInputAck": 516, + "MessageType_MoneroTransactionFinalRequest": 517, + "MessageType_MoneroTransactionFinalAck": 518, + "MessageType_MoneroKeyImageExportInitRequest": 530, + "MessageType_MoneroKeyImageExportInitAck": 531, + "MessageType_MoneroKeyImageSyncStepRequest": 532, + "MessageType_MoneroKeyImageSyncStepAck": 533, + "MessageType_MoneroKeyImageSyncFinalRequest": 534, + "MessageType_MoneroKeyImageSyncFinalAck": 535, + "MessageType_MoneroGetAddress": 540, + "MessageType_MoneroAddress": 541, + "MessageType_MoneroGetWatchKey": 542, + "MessageType_MoneroWatchKey": 543, + "MessageType_DebugMoneroDiagRequest": 546, + "MessageType_DebugMoneroDiagAck": 547, + "MessageType_MoneroGetTxKeyRequest": 550, + "MessageType_MoneroGetTxKeyAck": 551, + "MessageType_MoneroLiveRefreshStartRequest": 552, + "MessageType_MoneroLiveRefreshStartAck": 553, + "MessageType_MoneroLiveRefreshStepRequest": 554, + "MessageType_MoneroLiveRefreshStepAck": 555, + "MessageType_MoneroLiveRefreshFinalRequest": 556, + "MessageType_MoneroLiveRefreshFinalAck": 557, + "MessageType_EosGetPublicKey": 600, + "MessageType_EosPublicKey": 601, + "MessageType_EosSignTx": 602, + "MessageType_EosTxActionRequest": 603, + "MessageType_EosTxActionAck": 604, + "MessageType_EosSignedTx": 605, + "MessageType_BinanceGetAddress": 700, + "MessageType_BinanceAddress": 701, + "MessageType_BinanceGetPublicKey": 702, + "MessageType_BinancePublicKey": 703, + "MessageType_BinanceSignTx": 704, + "MessageType_BinanceTxRequest": 705, + "MessageType_BinanceTransferMsg": 706, + "MessageType_BinanceOrderMsg": 707, + "MessageType_BinanceCancelMsg": 708, + "MessageType_BinanceSignedTx": 709, + "MessageType_StarcoinGetAddress": 10300, + "MessageType_StarcoinAddress": 10301, + "MessageType_StarcoinGetPublicKey": 10302, + "MessageType_StarcoinPublicKey": 10303, + "MessageType_StarcoinSignTx": 10304, + "MessageType_StarcoinSignedTx": 10305, + "MessageType_StarcoinSignMessage": 10306, + "MessageType_StarcoinMessageSignature": 10307, + "MessageType_StarcoinVerifyMessage": 10308, + "MessageType_ConfluxGetAddress": 10112, + "MessageType_ConfluxAddress": 10113, + "MessageType_ConfluxSignTx": 10114, + "MessageType_ConfluxTxRequest": 10115, + "MessageType_ConfluxTxAck": 10116, + "MessageType_ConfluxSignMessage": 10117, + "MessageType_ConfluxSignMessageCIP23": 10118, + "MessageType_ConfluxMessageSignature": 10119, + "MessageType_TronGetAddress": 10501, + "MessageType_TronAddress": 10502, + "MessageType_TronSignTx": 10503, + "MessageType_TronSignedTx": 10504, + "MessageType_TronSignMessage": 10505, + "MessageType_TronMessageSignature": 10506, + "MessageType_NearGetAddress": 10701, + "MessageType_NearAddress": 10702, + "MessageType_NearSignTx": 10703, + "MessageType_NearSignedTx": 10704, + "MessageType_AptosGetAddress": 10600, + "MessageType_AptosAddress": 10601, + "MessageType_AptosSignTx": 10602, + "MessageType_AptosSignedTx": 10603, + "MessageType_AptosSignMessage": 10604, + "MessageType_AptosMessageSignature": 10605, + "MessageType_AptosSignSIWAMessage": 10606, + "MessageType_WebAuthnListResidentCredentials": 800, + "MessageType_WebAuthnCredentials": 801, + "MessageType_WebAuthnAddResidentCredential": 802, + "MessageType_WebAuthnRemoveResidentCredential": 803, + "MessageType_SolanaGetAddress": 10100, + "MessageType_SolanaAddress": 10101, + "MessageType_SolanaSignTx": 10102, + "MessageType_SolanaSignedTx": 10103, + "MessageType_SolanaSignOffChainMessage": 10104, + "MessageType_SolanaMessageSignature": 10105, + "MessageType_SolanaSignUnsafeMessage": 10106, + "MessageType_CosmosGetAddress": 10800, + "MessageType_CosmosAddress": 10801, + "MessageType_CosmosSignTx": 10802, + "MessageType_CosmosSignedTx": 10803, + "MessageType_AlgorandGetAddress": 10900, + "MessageType_AlgorandAddress": 10901, + "MessageType_AlgorandSignTx": 10902, + "MessageType_AlgorandSignedTx": 10903, + "MessageType_PolkadotGetAddress": 11000, + "MessageType_PolkadotAddress": 11001, + "MessageType_PolkadotSignTx": 11002, + "MessageType_PolkadotSignedTx": 11003, + "MessageType_SuiGetAddress": 11100, + "MessageType_SuiAddress": 11101, + "MessageType_SuiSignTx": 11102, + "MessageType_SuiSignedTx": 11103, + "MessageType_SuiSignMessage": 11104, + "MessageType_SuiMessageSignature": 11105, + "MessageType_SuiTxRequest": 11106, + "MessageType_SuiTxAck": 11107, + "MessageType_FilecoinGetAddress": 11200, + "MessageType_FilecoinAddress": 11201, + "MessageType_FilecoinSignTx": 11202, + "MessageType_FilecoinSignedTx": 11203, + "MessageType_KaspaGetAddress": 11300, + "MessageType_KaspaAddress": 11301, + "MessageType_KaspaSignTx": 11302, + "MessageType_KaspaSignedTx": 11303, + "MessageType_KaspaTxInputRequest": 11304, + "MessageType_KaspaTxInputAck": 11305, + "MessageType_NexaGetAddress": 11400, + "MessageType_NexaAddress": 11401, + "MessageType_NexaSignTx": 11402, + "MessageType_NexaSignedTx": 11403, + "MessageType_NexaTxInputRequest": 11404, + "MessageType_NexaTxInputAck": 11405, + "MessageType_NostrGetPublicKey": 11500, + "MessageType_NostrPublicKey": 11501, + "MessageType_NostrSignEvent": 11502, + "MessageType_NostrSignedEvent": 11503, + "MessageType_NostrEncryptMessage": 11504, + "MessageType_NostrEncryptedMessage": 11505, + "MessageType_NostrDecryptMessage": 11506, + "MessageType_NostrDecryptedMessage": 11507, + "MessageType_NostrSignSchnorr": 11508, + "MessageType_NostrSignedSchnorr": 11509, + "MessageType_LnurlAuth": 11600, + "MessageType_LnurlAuthResp": 11601, + "MessageType_NervosGetAddress": 11701, + "MessageType_NervosAddress": 11702, + "MessageType_NervosSignTx": 11703, + "MessageType_NervosSignedTx": 11704, + "MessageType_NervosTxRequest": 11705, + "MessageType_NervosTxAck": 11706, + "MessageType_TonGetAddress": 11901, + "MessageType_TonAddress": 11902, + "MessageType_TonSignMessage": 11903, + "MessageType_TonSignedMessage": 11904, + "MessageType_TonSignProof": 11905, + "MessageType_TonSignedProof": 11906, + "MessageType_TonTxAck": 11907, + "MessageType_ScdoGetAddress": 12001, + "MessageType_ScdoAddress": 12002, + "MessageType_ScdoSignTx": 12003, + "MessageType_ScdoSignedTx": 12004, + "MessageType_ScdoTxAck": 12005, + "MessageType_ScdoSignMessage": 12006, + "MessageType_ScdoSignedMessage": 12007, + "MessageType_AlephiumGetAddress": 12101, + "MessageType_AlephiumAddress": 12102, + "MessageType_AlephiumSignTx": 12103, + "MessageType_AlephiumSignedTx": 12104, + "MessageType_AlephiumTxRequest": 12105, + "MessageType_AlephiumTxAck": 12106, + "MessageType_AlephiumBytecodeRequest": 12107, + "MessageType_AlephiumBytecodeAck": 12108, + "MessageType_AlephiumSignMessage": 12109, + "MessageType_AlephiumMessageSignature": 12110, + "MessageType_BenfenGetAddress": 12201, + "MessageType_BenfenAddress": 12202, + "MessageType_BenfenSignTx": 12203, + "MessageType_BenfenSignedTx": 12204, + "MessageType_BenfenSignMessage": 12205, + "MessageType_BenfenMessageSignature": 12206, + "MessageType_BenfenTxRequest": 12207, + "MessageType_BenfenTxAck": 12208, + "MessageType_NeoGetAddress": 12301, + "MessageType_NeoAddress": 12302, + "MessageType_NeoSignTx": 12303, + "MessageType_NeoSignedTx": 12304, + "MessageType_UiviewShowAddressRequest": 30200, + "MessageType_UiviewShowPublicKeyRequest": 30201, + "MessageType_UiviewConfirmTxRequest": 30202, + "MessageType_UiviewConfirmSignMessageRequest": 30203, + "MessageType_UiviewResponse": 30204, + "MessageType_DeviceFactoryInfoSet": 60000, + "MessageType_DeviceFactoryInfoGet": 60001, + "MessageType_DeviceFactoryInfo": 60002, + "MessageType_DeviceFactoryPermanentLock": 60003, + "MessageType_DeviceFactoryTest": 60004, + "MessageType_ProtocolInfoRequest": 60200, + "MessageType_ProtocolInfo": 60201, + "MessageType_Ping": 60206, + "MessageType_Success": 60207, + "MessageType_Failure": 60208, + "MessageType_DeviceReboot": 60400, + "MessageType_DeviceSettings": 60410, + "MessageType_DeviceSettingsGet": 60411, + "MessageType_DeviceSettingsSet": 60412, + "MessageType_DeviceSettingsPageShow": 60413, + "MessageType_DeviceCertificate": 60420, + "MessageType_DeviceCertificateWrite": 60421, + "MessageType_DeviceCertificateRead": 60422, + "MessageType_DeviceCertificateSignature": 60423, + "MessageType_DeviceCertificateSign": 60424, + "MessageType_SetWallpaper": 60430, + "MessageType_GetWallpaper": 60431, + "MessageType_Wallpaper": 60432, + "MessageType_DeviceInfoGet": 60600, + "MessageType_DeviceInfo": 60601, + "MessageType_DeviceStatusGet": 60602, + "MessageType_DeviceStatus": 60603, + "MessageType_DevGetOnboardingStatus": 60604, + "MessageType_DevOnboardingStatus": 60605, + "MessageType_DeviceSessionGet": 60606, + "MessageType_DeviceSession": 60607, + "MessageType_DeviceSessionAskPin": 60608, + "MessageType_FilesystemPermissionFix": 60800, + "MessageType_FilesystemPathInfo": 60801, + "MessageType_FilesystemPathInfoQuery": 60802, + "MessageType_FilesystemFile": 60803, + "MessageType_FilesystemFileRead": 60804, + "MessageType_FilesystemFileWrite": 60805, + "MessageType_FilesystemFileDelete": 60806, + "MessageType_FilesystemDir": 60807, + "MessageType_FilesystemDirList": 60808, + "MessageType_FilesystemDirMake": 60809, + "MessageType_FilesystemDirRemove": 60810, + "MessageType_FilesystemFormat": 60811, + "MessageType_DeviceFirmwareUpdateRequest": 61000, + "MessageType_DeviceFirmwareUpdateStatusGet": 61001, + "MessageType_DeviceFirmwareUpdateStatus": 61002, + "MessageType_PortfolioUpdate": 61200 + }, + "reserved": [ + [90, 92], + [114, 122], + [300, 304], + [309, 312] + ] + }, + "AlephiumGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + }, + "include_public_key": { + "type": "bool", + "id": 3 + }, + "target_group": { + "type": "uint32", + "id": 4 + } + } + }, + "AlephiumAddress": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + }, + "public_key": { + "type": "bytes", + "id": 2 + }, + "derived_path": { + "rule": "repeated", + "type": "uint32", + "id": 3, + "options": { + "packed": false + } + } + } + }, + "AlephiumSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "data_initial_chunk": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "data_length": { + "type": "uint32", + "id": 3 + } + } + }, + "AlephiumSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "address": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "AlephiumTxRequest": { + "fields": { + "data_length": { + "type": "uint32", + "id": 1 + }, + "public_key": { + "type": "bytes", + "id": 2 + }, + "signature": { + "type": "bytes", + "id": 3 + } + } + }, + "AlephiumTxAck": { + "fields": { + "data_chunk": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "AlephiumBytecodeRequest": { + "fields": { + "data_length": { + "type": "uint32", + "id": 1 + }, + "public_key": { + "type": "bytes", + "id": 2 + }, + "signature": { + "type": "bytes", + "id": 3 + } + } + }, + "AlephiumBytecodeAck": { + "fields": { + "bytecode_data": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "AlephiumSignMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "type": "bytes", + "id": 2 + }, + "message_type": { + "type": "bytes", + "id": 3 + } + } + }, + "AlephiumMessageSignature": { + "fields": { + "signature": { + "type": "bytes", + "id": 1 + }, + "address": { + "type": "string", + "id": 2 + } + } + }, + "AlgorandGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 3 + } + } + }, + "AlgorandAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + } + } + }, + "AlgorandSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "AlgorandSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "AptosTransactionType": { + "values": { + "STANDARD": 0, + "WITH_DATA": 1 + } + }, + "AptosGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "AptosAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + } + } + }, + "AptosSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "tx_type": { + "type": "AptosTransactionType", + "id": 3, + "options": { + "default": "STANDARD" + } + } + } + }, + "AptosSignedTx": { + "fields": { + "public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "AptosSignMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "payload": { + "rule": "required", + "type": "AptosMessagePayload", + "id": 2 + } + }, + "nested": { + "AptosMessagePayload": { + "fields": { + "address": { + "type": "string", + "id": 2 + }, + "chain_id": { + "type": "string", + "id": 3 + }, + "application": { + "type": "string", + "id": 4 + }, + "nonce": { + "rule": "required", + "type": "string", + "id": 5 + }, + "message": { + "rule": "required", + "type": "string", + "id": 6 + } + } + } + } + }, + "AptosSignSIWAMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "siwa_payload": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "AptosMessageSignature": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "address": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "BenfenGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "BenfenAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + } + } + }, + "BenfenSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "data_initial_chunk": { + "type": "bytes", + "id": 3, + "options": { + "default": "" + } + }, + "coin_type": { + "type": "bytes", + "id": 4 + }, + "data_length": { + "type": "uint32", + "id": 5 + } + } + }, + "BenfenSignedTx": { + "fields": { + "public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "BenfenTxRequest": { + "fields": { + "data_length": { + "type": "uint32", + "id": 1 + }, + "public_key": { + "type": "bytes", + "id": 2 + }, + "signature": { + "type": "bytes", + "id": 3 + } + } + }, + "BenfenTxAck": { + "fields": { + "data_chunk": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "BenfenSignMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "BenfenMessageSignature": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "address": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "BinanceGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "BinanceAddress": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "BinanceGetPublicKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "BinancePublicKey": { + "fields": { + "public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "BinanceSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "msg_count": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "account_number": { + "rule": "required", + "type": "sint64", + "id": 3 + }, + "chain_id": { + "type": "string", + "id": 4 + }, + "memo": { + "type": "string", + "id": 5 + }, + "sequence": { + "rule": "required", + "type": "sint64", + "id": 6 + }, + "source": { + "rule": "required", + "type": "sint64", + "id": 7 + } + } + }, + "BinanceTxRequest": { + "fields": {} + }, + "BinanceTransferMsg": { + "fields": { + "inputs": { + "rule": "repeated", + "type": "BinanceInputOutput", + "id": 1 + }, + "outputs": { + "rule": "repeated", + "type": "BinanceInputOutput", + "id": 2 + } + }, + "nested": { + "BinanceInputOutput": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + }, + "coins": { + "rule": "repeated", + "type": "BinanceCoin", + "id": 2 + } + } + }, + "BinanceCoin": { + "fields": { + "amount": { + "rule": "required", + "type": "sint64", + "id": 1 + }, + "denom": { + "rule": "required", + "type": "string", + "id": 2 + } + } + } + } + }, + "BinanceOrderMsg": { + "fields": { + "id": { + "type": "string", + "id": 1 + }, + "ordertype": { + "rule": "required", + "type": "BinanceOrderType", + "id": 2 + }, + "price": { + "rule": "required", + "type": "sint64", + "id": 3 + }, + "quantity": { + "rule": "required", + "type": "sint64", + "id": 4 + }, + "sender": { + "type": "string", + "id": 5 + }, + "side": { + "rule": "required", + "type": "BinanceOrderSide", + "id": 6 + }, + "symbol": { + "type": "string", + "id": 7 + }, + "timeinforce": { + "rule": "required", + "type": "BinanceTimeInForce", + "id": 8 + } + }, + "nested": { + "BinanceOrderType": { + "values": { + "OT_UNKNOWN": 0, + "MARKET": 1, + "LIMIT": 2, + "OT_RESERVED": 3 + } + }, + "BinanceOrderSide": { + "values": { + "SIDE_UNKNOWN": 0, + "BUY": 1, + "SELL": 2 + } + }, + "BinanceTimeInForce": { + "values": { + "TIF_UNKNOWN": 0, + "GTE": 1, + "TIF_RESERVED": 2, + "IOC": 3 + } + } + } + }, + "BinanceCancelMsg": { + "fields": { + "refid": { + "type": "string", + "id": 1 + }, + "sender": { + "type": "string", + "id": 2 + }, + "symbol": { + "type": "string", + "id": 3 + } + } + }, + "BinanceSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "public_key": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "InputScriptType": { + "values": { + "SPENDADDRESS": 0, + "SPENDMULTISIG": 1, + "EXTERNAL": 2, + "SPENDWITNESS": 3, + "SPENDP2SHWITNESS": 4, + "SPENDTAPROOT": 5 + } + }, + "OutputScriptType": { + "values": { + "PAYTOADDRESS": 0, + "PAYTOSCRIPTHASH": 1, + "PAYTOMULTISIG": 2, + "PAYTOOPRETURN": 3, + "PAYTOWITNESS": 4, + "PAYTOP2SHWITNESS": 5, + "PAYTOTAPROOT": 6 + } + }, + "DecredStakingSpendType": { + "values": { + "SSGen": 0, + "SSRTX": 1 + } + }, + "AmountUnit": { + "values": { + "BITCOIN": 0, + "MILLIBITCOIN": 1, + "MICROBITCOIN": 2, + "SATOSHI": 3 + } + }, + "MultisigRedeemScriptType": { + "fields": { + "pubkeys": { + "rule": "repeated", + "type": "HDNodePathType", + "id": 1 + }, + "signatures": { + "rule": "repeated", + "type": "bytes", + "id": 2 + }, + "m": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "nodes": { + "rule": "repeated", + "type": "HDNodeType", + "id": 4 + }, + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 5, + "options": { + "packed": false + } + } + }, + "nested": { + "HDNodePathType": { + "fields": { + "node": { + "rule": "required", + "type": "HDNodeType", + "id": 1 + }, + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 2, + "options": { + "packed": false + } + } + } + } + } + }, + "GetPublicKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "ecdsa_curve_name": { + "type": "string", + "id": 2 + }, + "show_display": { + "type": "bool", + "id": 3 + }, + "coin_name": { + "type": "string", + "id": 4, + "options": { + "default": "Bitcoin" + } + }, + "script_type": { + "type": "InputScriptType", + "id": 5, + "options": { + "default": "SPENDADDRESS" + } + }, + "ignore_xpub_magic": { + "type": "bool", + "id": 6 + } + } + }, + "PublicKey": { + "fields": { + "node": { + "rule": "required", + "type": "HDNodeType", + "id": 1 + }, + "xpub": { + "rule": "required", + "type": "string", + "id": 2 + }, + "root_fingerprint": { + "type": "uint32", + "id": 3 + } + } + }, + "GetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "coin_name": { + "type": "string", + "id": 2, + "options": { + "default": "Bitcoin" + } + }, + "show_display": { + "type": "bool", + "id": 3 + }, + "multisig": { + "type": "MultisigRedeemScriptType", + "id": 4 + }, + "script_type": { + "type": "InputScriptType", + "id": 5, + "options": { + "default": "SPENDADDRESS" + } + }, + "ignore_xpub_magic": { + "type": "bool", + "id": 6 + } + } + }, + "Address": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + }, + "mac": { + "type": "bytes", + "id": 2 + } + } + }, + "GetOwnershipId": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "coin_name": { + "type": "string", + "id": 2, + "options": { + "default": "Bitcoin" + } + }, + "multisig": { + "type": "MultisigRedeemScriptType", + "id": 3 + }, + "script_type": { + "type": "InputScriptType", + "id": 4, + "options": { + "default": "SPENDADDRESS" + } + } + } + }, + "OwnershipId": { + "fields": { + "ownership_id": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "SignMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "coin_name": { + "type": "string", + "id": 3, + "options": { + "default": "Bitcoin" + } + }, + "script_type": { + "type": "InputScriptType", + "id": 4, + "options": { + "default": "SPENDADDRESS" + } + }, + "no_script_type": { + "type": "bool", + "id": 5 + }, + "is_bip322_simple": { + "type": "bool", + "id": 10, + "options": { + "default": false + } + } + } + }, + "MessageSignature": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "VerifyMessage": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "coin_name": { + "type": "string", + "id": 4, + "options": { + "default": "Bitcoin" + } + } + } + }, + "SignTx": { + "fields": { + "outputs_count": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "inputs_count": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "coin_name": { + "type": "string", + "id": 3, + "options": { + "default": "Bitcoin" + } + }, + "version": { + "type": "uint32", + "id": 4, + "options": { + "default": 1 + } + }, + "lock_time": { + "type": "uint32", + "id": 5, + "options": { + "default": 0 + } + }, + "expiry": { + "type": "uint32", + "id": 6 + }, + "overwintered": { + "type": "bool", + "id": 7, + "options": { + "deprecated": true + } + }, + "version_group_id": { + "type": "uint32", + "id": 8 + }, + "timestamp": { + "type": "uint32", + "id": 9 + }, + "branch_id": { + "type": "uint32", + "id": 10 + }, + "amount_unit": { + "type": "AmountUnit", + "id": 11, + "options": { + "default": "BITCOIN" + } + }, + "decred_staking_ticket": { + "type": "bool", + "id": 12, + "options": { + "default": false + } + }, + "serialize": { + "type": "bool", + "id": 13, + "options": { + "default": true + } + }, + "coinjoin_request": { + "type": "CoinJoinRequest", + "id": 14 + } + }, + "nested": { + "CoinJoinRequest": { + "fields": { + "fee_rate": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "no_fee_threshold": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "min_registrable_amount": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "mask_public_key": { + "rule": "required", + "type": "bytes", + "id": 4 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 5 + } + } + } + } + }, + "TxRequest": { + "fields": { + "request_type": { + "type": "RequestType", + "id": 1 + }, + "details": { + "type": "TxRequestDetailsType", + "id": 2 + }, + "serialized": { + "type": "TxRequestSerializedType", + "id": 3 + } + }, + "nested": { + "RequestType": { + "values": { + "TXINPUT": 0, + "TXOUTPUT": 1, + "TXMETA": 2, + "TXFINISHED": 3, + "TXEXTRADATA": 4, + "TXORIGINPUT": 5, + "TXORIGOUTPUT": 6, + "TXPAYMENTREQ": 7 + } + }, + "TxRequestDetailsType": { + "fields": { + "request_index": { + "type": "uint32", + "id": 1 + }, + "tx_hash": { + "type": "bytes", + "id": 2 + }, + "extra_data_len": { + "type": "uint32", + "id": 3 + }, + "extra_data_offset": { + "type": "uint32", + "id": 4 + } + } + }, + "TxRequestSerializedType": { + "fields": { + "signature_index": { + "type": "uint32", + "id": 1 + }, + "signature": { + "type": "bytes", + "id": 2 + }, + "serialized_tx": { + "type": "bytes", + "id": 3 + } + } + } + } + }, + "TxAck": { + "options": { + "deprecated": true + }, + "fields": { + "tx": { + "type": "TransactionType", + "id": 1 + } + }, + "nested": { + "TransactionType": { + "fields": { + "version": { + "type": "uint32", + "id": 1 + }, + "inputs": { + "rule": "repeated", + "type": "TxInputType", + "id": 2 + }, + "bin_outputs": { + "rule": "repeated", + "type": "TxOutputBinType", + "id": 3 + }, + "lock_time": { + "type": "uint32", + "id": 4 + }, + "outputs": { + "rule": "repeated", + "type": "TxOutputType", + "id": 5 + }, + "inputs_cnt": { + "type": "uint32", + "id": 6 + }, + "outputs_cnt": { + "type": "uint32", + "id": 7 + }, + "extra_data": { + "type": "bytes", + "id": 8 + }, + "extra_data_len": { + "type": "uint32", + "id": 9 + }, + "expiry": { + "type": "uint32", + "id": 10 + }, + "overwintered": { + "type": "bool", + "id": 11, + "options": { + "deprecated": true + } + }, + "version_group_id": { + "type": "uint32", + "id": 12 + }, + "timestamp": { + "type": "uint32", + "id": 13 + }, + "branch_id": { + "type": "uint32", + "id": 14 + } + }, + "nested": { + "TxInputType": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "prev_hash": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "prev_index": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "script_sig": { + "type": "bytes", + "id": 4 + }, + "sequence": { + "type": "uint32", + "id": 5, + "options": { + "default": 4294967295 + } + }, + "script_type": { + "type": "InputScriptType", + "id": 6, + "options": { + "default": "SPENDADDRESS" + } + }, + "multisig": { + "type": "MultisigRedeemScriptType", + "id": 7 + }, + "amount": { + "type": "uint64", + "id": 8 + }, + "decred_tree": { + "type": "uint32", + "id": 9 + }, + "witness": { + "type": "bytes", + "id": 13 + }, + "ownership_proof": { + "type": "bytes", + "id": 14 + }, + "commitment_data": { + "type": "bytes", + "id": 15 + }, + "orig_hash": { + "type": "bytes", + "id": 16 + }, + "orig_index": { + "type": "uint32", + "id": 17 + }, + "decred_staking_spend": { + "type": "DecredStakingSpendType", + "id": 18 + }, + "script_pubkey": { + "type": "bytes", + "id": 19 + }, + "coinjoin_flags": { + "type": "uint32", + "id": 20, + "options": { + "default": 0 + } + } + } + }, + "TxOutputBinType": { + "fields": { + "amount": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "script_pubkey": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "decred_script_version": { + "type": "uint32", + "id": 3 + } + } + }, + "TxOutputType": { + "fields": { + "address": { + "type": "string", + "id": 1 + }, + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 2, + "options": { + "packed": false + } + }, + "amount": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "script_type": { + "type": "OutputScriptType", + "id": 4, + "options": { + "default": "PAYTOADDRESS" + } + }, + "multisig": { + "type": "MultisigRedeemScriptType", + "id": 5 + }, + "op_return_data": { + "type": "bytes", + "id": 6 + }, + "orig_hash": { + "type": "bytes", + "id": 10 + }, + "orig_index": { + "type": "uint32", + "id": 11 + }, + "payment_req_index": { + "type": "uint32", + "id": 12, + "options": { + "(experimental_field)": true + } + } + } + } + } + } + } + }, + "TxInput": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "prev_hash": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "prev_index": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "script_sig": { + "type": "bytes", + "id": 4 + }, + "sequence": { + "type": "uint32", + "id": 5, + "options": { + "default": 4294967295 + } + }, + "script_type": { + "type": "InputScriptType", + "id": 6, + "options": { + "default": "SPENDADDRESS" + } + }, + "multisig": { + "type": "MultisigRedeemScriptType", + "id": 7 + }, + "amount": { + "rule": "required", + "type": "uint64", + "id": 8 + }, + "decred_tree": { + "type": "uint32", + "id": 9 + }, + "witness": { + "type": "bytes", + "id": 13 + }, + "ownership_proof": { + "type": "bytes", + "id": 14 + }, + "commitment_data": { + "type": "bytes", + "id": 15 + }, + "orig_hash": { + "type": "bytes", + "id": 16 + }, + "orig_index": { + "type": "uint32", + "id": 17 + }, + "decred_staking_spend": { + "type": "DecredStakingSpendType", + "id": 18 + }, + "script_pubkey": { + "type": "bytes", + "id": 19 + }, + "coinjoin_flags": { + "type": "uint32", + "id": 20, + "options": { + "default": 0 + } + } + } + }, + "TxOutput": { + "fields": { + "address": { + "type": "string", + "id": 1 + }, + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 2, + "options": { + "packed": false + } + }, + "amount": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "script_type": { + "type": "OutputScriptType", + "id": 4, + "options": { + "default": "PAYTOADDRESS" + } + }, + "multisig": { + "type": "MultisigRedeemScriptType", + "id": 5 + }, + "op_return_data": { + "type": "bytes", + "id": 6 + }, + "orig_hash": { + "type": "bytes", + "id": 10 + }, + "orig_index": { + "type": "uint32", + "id": 11 + }, + "payment_req_index": { + "type": "uint32", + "id": 12, + "options": { + "(experimental_field)": true + } + } + } + }, + "PrevTx": { + "fields": { + "version": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "lock_time": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "inputs_count": { + "rule": "required", + "type": "uint32", + "id": 6 + }, + "outputs_count": { + "rule": "required", + "type": "uint32", + "id": 7 + }, + "extra_data_len": { + "type": "uint32", + "id": 9, + "options": { + "default": 0 + } + }, + "expiry": { + "type": "uint32", + "id": 10 + }, + "version_group_id": { + "type": "uint32", + "id": 12 + }, + "timestamp": { + "type": "uint32", + "id": 13 + }, + "branch_id": { + "type": "uint32", + "id": 14 + } + } + }, + "PrevInput": { + "fields": { + "prev_hash": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "prev_index": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "script_sig": { + "rule": "required", + "type": "bytes", + "id": 4 + }, + "sequence": { + "rule": "required", + "type": "uint32", + "id": 5 + }, + "decred_tree": { + "type": "uint32", + "id": 9 + } + } + }, + "PrevOutput": { + "fields": { + "amount": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "script_pubkey": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "decred_script_version": { + "type": "uint32", + "id": 3 + } + } + }, + "TxAckPaymentRequest": { + "options": { + "(experimental_message)": true + }, + "fields": { + "nonce": { + "type": "bytes", + "id": 1 + }, + "recipient_name": { + "rule": "required", + "type": "string", + "id": 2 + }, + "memos": { + "rule": "repeated", + "type": "PaymentRequestMemo", + "id": 3 + }, + "amount": { + "type": "uint64", + "id": 4 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 5 + } + }, + "nested": { + "PaymentRequestMemo": { + "fields": { + "text_memo": { + "type": "TextMemo", + "id": 1 + }, + "refund_memo": { + "type": "RefundMemo", + "id": 2 + }, + "coin_purchase_memo": { + "type": "CoinPurchaseMemo", + "id": 3 + } + } + }, + "TextMemo": { + "fields": { + "text": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "RefundMemo": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + }, + "mac": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "CoinPurchaseMemo": { + "fields": { + "coin_type": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "amount": { + "rule": "required", + "type": "string", + "id": 2 + }, + "address": { + "rule": "required", + "type": "string", + "id": 3 + }, + "mac": { + "rule": "required", + "type": "bytes", + "id": 4 + } + } + } + } + }, + "TxAckInput": { + "options": { + "(wire_type)": 22 + }, + "fields": { + "tx": { + "rule": "required", + "type": "TxAckInputWrapper", + "id": 1 + } + }, + "nested": { + "TxAckInputWrapper": { + "fields": { + "input": { + "rule": "required", + "type": "TxInput", + "id": 2 + } + } + } + } + }, + "TxAckOutput": { + "options": { + "(wire_type)": 22 + }, + "fields": { + "tx": { + "rule": "required", + "type": "TxAckOutputWrapper", + "id": 1 + } + }, + "nested": { + "TxAckOutputWrapper": { + "fields": { + "output": { + "rule": "required", + "type": "TxOutput", + "id": 5 + } + } + } + } + }, + "TxAckPrevMeta": { + "options": { + "(wire_type)": 22 + }, + "fields": { + "tx": { + "rule": "required", + "type": "PrevTx", + "id": 1 + } + } + }, + "TxAckPrevInput": { + "options": { + "(wire_type)": 22 + }, + "fields": { + "tx": { + "rule": "required", + "type": "TxAckPrevInputWrapper", + "id": 1 + } + }, + "nested": { + "TxAckPrevInputWrapper": { + "fields": { + "input": { + "rule": "required", + "type": "PrevInput", + "id": 2 + } + } + } + } + }, + "TxAckPrevOutput": { + "options": { + "(wire_type)": 22 + }, + "fields": { + "tx": { + "rule": "required", + "type": "TxAckPrevOutputWrapper", + "id": 1 + } + }, + "nested": { + "TxAckPrevOutputWrapper": { + "fields": { + "output": { + "rule": "required", + "type": "PrevOutput", + "id": 3 + } + } + } + } + }, + "TxAckPrevExtraData": { + "options": { + "(wire_type)": 22 + }, + "fields": { + "tx": { + "rule": "required", + "type": "TxAckPrevExtraDataWrapper", + "id": 1 + } + }, + "nested": { + "TxAckPrevExtraDataWrapper": { + "fields": { + "extra_data_chunk": { + "rule": "required", + "type": "bytes", + "id": 8 + } + } + } + } + }, + "GetOwnershipProof": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "coin_name": { + "type": "string", + "id": 2, + "options": { + "default": "Bitcoin" + } + }, + "script_type": { + "type": "InputScriptType", + "id": 3, + "options": { + "default": "SPENDWITNESS" + } + }, + "multisig": { + "type": "MultisigRedeemScriptType", + "id": 4 + }, + "user_confirmation": { + "type": "bool", + "id": 5, + "options": { + "default": false + } + }, + "ownership_ids": { + "rule": "repeated", + "type": "bytes", + "id": 6 + }, + "commitment_data": { + "type": "bytes", + "id": 7, + "options": { + "default": "" + } + } + } + }, + "OwnershipProof": { + "fields": { + "ownership_proof": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "AuthorizeCoinJoin": { + "fields": { + "coordinator": { + "rule": "required", + "type": "string", + "id": 1 + }, + "max_rounds": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "max_coordinator_fee_rate": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "max_fee_per_kvbyte": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 5, + "options": { + "packed": false + } + }, + "coin_name": { + "type": "string", + "id": 6, + "options": { + "default": "Bitcoin" + } + }, + "script_type": { + "type": "InputScriptType", + "id": 7, + "options": { + "default": "SPENDADDRESS" + } + }, + "amount_unit": { + "type": "AmountUnit", + "id": 8, + "options": { + "default": "BITCOIN" + } + } + } + }, + "SignPsbt": { + "fields": { + "psbt": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "coin_name": { + "type": "string", + "id": 2, + "options": { + "default": "Bitcoin" + } + } + } + }, + "SignedPsbt": { + "fields": { + "psbt": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "ButtonRequest": { + "fields": { + "code": { + "type": "ButtonRequestType", + "id": 1 + }, + "pages": { + "type": "uint32", + "id": 2 + } + }, + "nested": { + "ButtonRequestType": { + "values": { + "ButtonRequest_Other": 1, + "ButtonRequest_FeeOverThreshold": 2, + "ButtonRequest_ConfirmOutput": 3, + "ButtonRequest_ResetDevice": 4, + "ButtonRequest_ConfirmWord": 5, + "ButtonRequest_WipeDevice": 6, + "ButtonRequest_ProtectCall": 7, + "ButtonRequest_SignTx": 8, + "ButtonRequest_FirmwareCheck": 9, + "ButtonRequest_Address": 10, + "ButtonRequest_PublicKey": 11, + "ButtonRequest_MnemonicWordCount": 12, + "ButtonRequest_MnemonicInput": 13, + "_Deprecated_ButtonRequest_PassphraseType": 14, + "ButtonRequest_UnknownDerivationPath": 15, + "ButtonRequest_RecoveryHomepage": 16, + "ButtonRequest_Success": 17, + "ButtonRequest_Warning": 18, + "ButtonRequest_PassphraseEntry": 19, + "ButtonRequest_PinEntry": 20, + "ButtonRequest_AttachPin": 8000 + } + } + } + }, + "ButtonAck": { + "fields": {} + }, + "CardanoDerivationType": { + "values": { + "LEDGER": 0, + "ICARUS": 1, + "ICARUS_TREZOR": 2 + } + }, + "CardanoAddressType": { + "values": { + "BASE": 0, + "BASE_SCRIPT_KEY": 1, + "BASE_KEY_SCRIPT": 2, + "BASE_SCRIPT_SCRIPT": 3, + "POINTER": 4, + "POINTER_SCRIPT": 5, + "ENTERPRISE": 6, + "ENTERPRISE_SCRIPT": 7, + "BYRON": 8, + "REWARD": 14, + "REWARD_SCRIPT": 15 + } + }, + "CardanoNativeScriptType": { + "values": { + "PUB_KEY": 0, + "ALL": 1, + "ANY": 2, + "N_OF_K": 3, + "INVALID_BEFORE": 4, + "INVALID_HEREAFTER": 5 + } + }, + "CardanoNativeScriptHashDisplayFormat": { + "values": { + "HIDE": 0, + "BECH32": 1, + "POLICY_ID": 2 + } + }, + "CardanoTxOutputSerializationFormat": { + "values": { + "ARRAY_LEGACY": 0, + "MAP_BABBAGE": 1 + } + }, + "CardanoCertificateType": { + "values": { + "STAKE_REGISTRATION": 0, + "STAKE_DEREGISTRATION": 1, + "STAKE_DELEGATION": 2, + "STAKE_POOL_REGISTRATION": 3, + "STAKE_REGISTRATION_CONWAY": 7, + "STAKE_DEREGISTRATION_CONWAY": 8, + "VOTE_DELEGATION": 9 + } + }, + "CardanoDRepType": { + "values": { + "KEY_HASH": 0, + "SCRIPT_HASH": 1, + "ABSTAIN": 2, + "NO_CONFIDENCE": 3 + } + }, + "CardanoPoolRelayType": { + "values": { + "SINGLE_HOST_IP": 0, + "SINGLE_HOST_NAME": 1, + "MULTIPLE_HOST_NAME": 2 + } + }, + "CardanoTxAuxiliaryDataSupplementType": { + "values": { + "NONE": 0, + "CVOTE_REGISTRATION_SIGNATURE": 1 + } + }, + "CardanoCVoteRegistrationFormat": { + "values": { + "CIP15": 0, + "CIP36": 1 + } + }, + "CardanoTxSigningMode": { + "values": { + "ORDINARY_TRANSACTION": 0, + "POOL_REGISTRATION_AS_OWNER": 1, + "MULTISIG_TRANSACTION": 2, + "PLUTUS_TRANSACTION": 3 + } + }, + "CardanoTxWitnessType": { + "values": { + "BYRON_WITNESS": 0, + "SHELLEY_WITNESS": 1 + } + }, + "CardanoBlockchainPointerType": { + "fields": { + "block_index": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "tx_index": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "certificate_index": { + "rule": "required", + "type": "uint32", + "id": 3 + } + } + }, + "CardanoNativeScript": { + "fields": { + "type": { + "rule": "required", + "type": "CardanoNativeScriptType", + "id": 1 + }, + "scripts": { + "rule": "repeated", + "type": "CardanoNativeScript", + "id": 2 + }, + "key_hash": { + "type": "bytes", + "id": 3 + }, + "key_path": { + "rule": "repeated", + "type": "uint32", + "id": 4, + "options": { + "packed": false + } + }, + "required_signatures_count": { + "type": "uint32", + "id": 5 + }, + "invalid_before": { + "type": "uint64", + "id": 6 + }, + "invalid_hereafter": { + "type": "uint64", + "id": 7 + } + } + }, + "CardanoGetNativeScriptHash": { + "fields": { + "script": { + "rule": "required", + "type": "CardanoNativeScript", + "id": 1 + }, + "display_format": { + "rule": "required", + "type": "CardanoNativeScriptHashDisplayFormat", + "id": 2 + }, + "derivation_type": { + "rule": "required", + "type": "CardanoDerivationType", + "id": 3 + } + } + }, + "CardanoNativeScriptHash": { + "fields": { + "script_hash": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "CardanoAddressParametersType": { + "fields": { + "address_type": { + "rule": "required", + "type": "CardanoAddressType", + "id": 1 + }, + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 2, + "options": { + "packed": false + } + }, + "address_n_staking": { + "rule": "repeated", + "type": "uint32", + "id": 3, + "options": { + "packed": false + } + }, + "staking_key_hash": { + "type": "bytes", + "id": 4 + }, + "certificate_pointer": { + "type": "CardanoBlockchainPointerType", + "id": 5 + }, + "script_payment_hash": { + "type": "bytes", + "id": 6 + }, + "script_staking_hash": { + "type": "bytes", + "id": 7 + } + } + }, + "CardanoGetAddress": { + "fields": { + "show_display": { + "type": "bool", + "id": 2, + "options": { + "default": false + } + }, + "protocol_magic": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "network_id": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "address_parameters": { + "rule": "required", + "type": "CardanoAddressParametersType", + "id": 5 + }, + "derivation_type": { + "rule": "required", + "type": "CardanoDerivationType", + "id": 6 + }, + "chunkify": { + "type": "bool", + "id": 7 + } + } + }, + "CardanoAddress": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "CardanoGetPublicKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + }, + "derivation_type": { + "rule": "required", + "type": "CardanoDerivationType", + "id": 3 + } + } + }, + "CardanoPublicKey": { + "fields": { + "xpub": { + "rule": "required", + "type": "string", + "id": 1 + }, + "node": { + "rule": "required", + "type": "HDNodeType", + "id": 2 + } + } + }, + "CardanoSignTxInit": { + "fields": { + "signing_mode": { + "rule": "required", + "type": "CardanoTxSigningMode", + "id": 1 + }, + "protocol_magic": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "network_id": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "inputs_count": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "outputs_count": { + "rule": "required", + "type": "uint32", + "id": 5 + }, + "fee": { + "rule": "required", + "type": "uint64", + "id": 6 + }, + "ttl": { + "type": "uint64", + "id": 7 + }, + "certificates_count": { + "rule": "required", + "type": "uint32", + "id": 8 + }, + "withdrawals_count": { + "rule": "required", + "type": "uint32", + "id": 9 + }, + "has_auxiliary_data": { + "rule": "required", + "type": "bool", + "id": 10 + }, + "validity_interval_start": { + "type": "uint64", + "id": 11 + }, + "witness_requests_count": { + "rule": "required", + "type": "uint32", + "id": 12 + }, + "minting_asset_groups_count": { + "rule": "required", + "type": "uint32", + "id": 13 + }, + "derivation_type": { + "rule": "required", + "type": "CardanoDerivationType", + "id": 14 + }, + "include_network_id": { + "type": "bool", + "id": 15, + "options": { + "default": false + } + }, + "script_data_hash": { + "type": "bytes", + "id": 16 + }, + "collateral_inputs_count": { + "rule": "required", + "type": "uint32", + "id": 17 + }, + "required_signers_count": { + "rule": "required", + "type": "uint32", + "id": 18 + }, + "has_collateral_return": { + "type": "bool", + "id": 19, + "options": { + "default": false + } + }, + "total_collateral": { + "type": "uint64", + "id": 20 + }, + "reference_inputs_count": { + "type": "uint32", + "id": 21, + "options": { + "default": 0 + } + }, + "chunkify": { + "type": "bool", + "id": 22 + }, + "tag_cbor_sets": { + "type": "bool", + "id": 23, + "options": { + "default": false + } + } + } + }, + "CardanoTxInput": { + "fields": { + "prev_hash": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "prev_index": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + }, + "CardanoTxOutput": { + "fields": { + "address": { + "type": "string", + "id": 1 + }, + "address_parameters": { + "type": "CardanoAddressParametersType", + "id": 2 + }, + "amount": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "asset_groups_count": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "datum_hash": { + "type": "bytes", + "id": 5 + }, + "format": { + "type": "CardanoTxOutputSerializationFormat", + "id": 6, + "options": { + "default": "ARRAY_LEGACY" + } + }, + "inline_datum_size": { + "type": "uint32", + "id": 7, + "options": { + "default": 0 + } + }, + "reference_script_size": { + "type": "uint32", + "id": 8, + "options": { + "default": 0 + } + } + } + }, + "CardanoAssetGroup": { + "fields": { + "policy_id": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "tokens_count": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + }, + "CardanoToken": { + "fields": { + "asset_name_bytes": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "amount": { + "type": "uint64", + "id": 2 + }, + "mint_amount": { + "type": "sint64", + "id": 3 + } + } + }, + "CardanoTxInlineDatumChunk": { + "fields": { + "data": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "CardanoTxReferenceScriptChunk": { + "fields": { + "data": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "CardanoPoolOwner": { + "fields": { + "staking_key_path": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "staking_key_hash": { + "type": "bytes", + "id": 2 + } + } + }, + "CardanoPoolRelayParameters": { + "fields": { + "type": { + "rule": "required", + "type": "CardanoPoolRelayType", + "id": 1 + }, + "ipv4_address": { + "type": "bytes", + "id": 2 + }, + "ipv6_address": { + "type": "bytes", + "id": 3 + }, + "host_name": { + "type": "string", + "id": 4 + }, + "port": { + "type": "uint32", + "id": 5 + } + } + }, + "CardanoPoolMetadataType": { + "fields": { + "url": { + "rule": "required", + "type": "string", + "id": 1 + }, + "hash": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "CardanoPoolParametersType": { + "fields": { + "pool_id": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "vrf_key_hash": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "pledge": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "cost": { + "rule": "required", + "type": "uint64", + "id": 4 + }, + "margin_numerator": { + "rule": "required", + "type": "uint64", + "id": 5 + }, + "margin_denominator": { + "rule": "required", + "type": "uint64", + "id": 6 + }, + "reward_account": { + "rule": "required", + "type": "string", + "id": 7 + }, + "metadata": { + "type": "CardanoPoolMetadataType", + "id": 10 + }, + "owners_count": { + "rule": "required", + "type": "uint32", + "id": 11 + }, + "relays_count": { + "rule": "required", + "type": "uint32", + "id": 12 + } + } + }, + "CardanoDRep": { + "fields": { + "type": { + "rule": "required", + "type": "CardanoDRepType", + "id": 1 + }, + "key_hash": { + "type": "bytes", + "id": 2 + }, + "script_hash": { + "type": "bytes", + "id": 3 + } + } + }, + "CardanoTxCertificate": { + "fields": { + "type": { + "rule": "required", + "type": "CardanoCertificateType", + "id": 1 + }, + "path": { + "rule": "repeated", + "type": "uint32", + "id": 2, + "options": { + "packed": false + } + }, + "pool": { + "type": "bytes", + "id": 3 + }, + "pool_parameters": { + "type": "CardanoPoolParametersType", + "id": 4 + }, + "script_hash": { + "type": "bytes", + "id": 5 + }, + "key_hash": { + "type": "bytes", + "id": 6 + }, + "deposit": { + "type": "uint64", + "id": 7 + }, + "drep": { + "type": "CardanoDRep", + "id": 8 + } + } + }, + "CardanoTxWithdrawal": { + "fields": { + "path": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "amount": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "script_hash": { + "type": "bytes", + "id": 3 + }, + "key_hash": { + "type": "bytes", + "id": 4 + } + } + }, + "CardanoCVoteRegistrationDelegation": { + "fields": { + "vote_public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "weight": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + }, + "CardanoCVoteRegistrationParametersType": { + "fields": { + "vote_public_key": { + "type": "bytes", + "id": 1 + }, + "staking_path": { + "rule": "repeated", + "type": "uint32", + "id": 2, + "options": { + "packed": false + } + }, + "payment_address_parameters": { + "type": "CardanoAddressParametersType", + "id": 3 + }, + "nonce": { + "rule": "required", + "type": "uint64", + "id": 4 + }, + "format": { + "type": "CardanoCVoteRegistrationFormat", + "id": 5, + "options": { + "default": "CIP15" + } + }, + "delegations": { + "rule": "repeated", + "type": "CardanoCVoteRegistrationDelegation", + "id": 6 + }, + "voting_purpose": { + "type": "uint64", + "id": 7 + }, + "payment_address": { + "type": "string", + "id": 8 + } + } + }, + "CardanoTxAuxiliaryData": { + "fields": { + "cvote_registration_parameters": { + "type": "CardanoCVoteRegistrationParametersType", + "id": 1 + }, + "hash": { + "type": "bytes", + "id": 2 + } + } + }, + "CardanoTxMint": { + "fields": { + "asset_groups_count": { + "rule": "required", + "type": "uint32", + "id": 1 + } + } + }, + "CardanoTxCollateralInput": { + "fields": { + "prev_hash": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "prev_index": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + }, + "CardanoTxRequiredSigner": { + "fields": { + "key_hash": { + "type": "bytes", + "id": 1 + }, + "key_path": { + "rule": "repeated", + "type": "uint32", + "id": 2, + "options": { + "packed": false + } + } + } + }, + "CardanoTxReferenceInput": { + "fields": { + "prev_hash": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "prev_index": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + }, + "CardanoTxItemAck": { + "fields": {} + }, + "CardanoTxAuxiliaryDataSupplement": { + "fields": { + "type": { + "rule": "required", + "type": "CardanoTxAuxiliaryDataSupplementType", + "id": 1 + }, + "auxiliary_data_hash": { + "type": "bytes", + "id": 2 + }, + "cvote_registration_signature": { + "type": "bytes", + "id": 3 + } + } + }, + "CardanoTxWitnessRequest": { + "fields": { + "path": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + } + } + }, + "CardanoTxWitnessResponse": { + "fields": { + "type": { + "rule": "required", + "type": "CardanoTxWitnessType", + "id": 1 + }, + "pub_key": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "chain_code": { + "type": "bytes", + "id": 4 + } + } + }, + "CardanoTxHostAck": { + "fields": {} + }, + "CardanoTxBodyHash": { + "fields": { + "tx_hash": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "CardanoSignTxFinished": { + "fields": {} + }, + "CardanoSignMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "derivation_type": { + "rule": "required", + "type": "CardanoDerivationType", + "id": 3 + }, + "network_id": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "address_type": { + "type": "CardanoAddressType", + "id": 5 + } + } + }, + "CardanoMessageSignature": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "key": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "ConfluxGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + }, + "chain_id": { + "type": "uint32", + "id": 3 + } + } + }, + "ConfluxAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + } + } + }, + "ConfluxSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "nonce": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "gas_price": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "gas_limit": { + "rule": "required", + "type": "bytes", + "id": 4 + }, + "to": { + "type": "string", + "id": 5, + "options": { + "default": "" + } + }, + "value": { + "type": "bytes", + "id": 6, + "options": { + "default": "" + } + }, + "epoch_height": { + "type": "bytes", + "id": 7, + "options": { + "default": "" + } + }, + "storage_limit": { + "type": "bytes", + "id": 8, + "options": { + "default": "" + } + }, + "data_initial_chunk": { + "type": "bytes", + "id": 9, + "options": { + "default": "" + } + }, + "data_length": { + "type": "uint32", + "id": 10, + "options": { + "default": 0 + } + }, + "chain_id": { + "type": "uint32", + "id": 11, + "options": { + "default": 1029 + } + } + } + }, + "ConfluxTxRequest": { + "fields": { + "data_length": { + "type": "uint32", + "id": 1 + }, + "signature_v": { + "type": "uint32", + "id": 2 + }, + "signature_r": { + "type": "bytes", + "id": 3 + }, + "signature_s": { + "type": "bytes", + "id": 4 + } + } + }, + "ConfluxTxAck": { + "fields": { + "data_chunk": { + "type": "bytes", + "id": 1 + } + } + }, + "ConfluxSignMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "type": "bytes", + "id": 2 + } + } + }, + "ConfluxMessageSignature": { + "fields": { + "signature": { + "type": "bytes", + "id": 2 + }, + "address": { + "type": "string", + "id": 3 + } + } + }, + "ConfluxSignMessageCIP23": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "domain_hash": { + "type": "bytes", + "id": 2 + }, + "message_hash": { + "type": "bytes", + "id": 3 + } + } + }, + "CosmosGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "hrp": { + "type": "string", + "id": 2 + }, + "show_display": { + "type": "bool", + "id": 3 + } + } + }, + "CosmosAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + } + } + }, + "CosmosSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "CosmosSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "HDNodeType": { + "fields": { + "depth": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "fingerprint": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "child_num": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "chain_code": { + "rule": "required", + "type": "bytes", + "id": 4 + }, + "private_key": { + "type": "bytes", + "id": 5 + }, + "public_key": { + "rule": "required", + "type": "bytes", + "id": 6 + } + } + }, + "CipherKeyValue": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "key": { + "rule": "required", + "type": "string", + "id": 2 + }, + "value": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "encrypt": { + "type": "bool", + "id": 4 + }, + "ask_on_encrypt": { + "type": "bool", + "id": 5 + }, + "ask_on_decrypt": { + "type": "bool", + "id": 6 + }, + "iv": { + "type": "bytes", + "id": 7 + } + } + }, + "CipheredKeyValue": { + "fields": { + "value": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "IdentityType": { + "fields": { + "proto": { + "type": "string", + "id": 1 + }, + "user": { + "type": "string", + "id": 2 + }, + "host": { + "type": "string", + "id": 3 + }, + "port": { + "type": "string", + "id": 4 + }, + "path": { + "type": "string", + "id": 5 + }, + "index": { + "type": "uint32", + "id": 6, + "options": { + "default": 0 + } + } + } + }, + "SignIdentity": { + "fields": { + "identity": { + "rule": "required", + "type": "IdentityType", + "id": 1 + }, + "challenge_hidden": { + "type": "bytes", + "id": 2, + "options": { + "default": "" + } + }, + "challenge_visual": { + "type": "string", + "id": 3, + "options": { + "default": "" + } + }, + "ecdsa_curve_name": { + "type": "string", + "id": 4 + } + } + }, + "SignedIdentity": { + "fields": { + "address": { + "type": "string", + "id": 1 + }, + "public_key": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 3 + } + } + }, + "GetECDHSessionKey": { + "fields": { + "identity": { + "rule": "required", + "type": "IdentityType", + "id": 1 + }, + "peer_public_key": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "ecdsa_curve_name": { + "type": "string", + "id": 3 + } + } + }, + "ECDHSessionKey": { + "fields": { + "session_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "public_key": { + "type": "bytes", + "id": 2 + } + } + }, + "CosiCommit": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "data": { + "type": "bytes", + "id": 2, + "options": { + "deprecated": true + } + } + } + }, + "CosiCommitment": { + "fields": { + "commitment": { + "type": "bytes", + "id": 1 + }, + "pubkey": { + "type": "bytes", + "id": 2 + } + } + }, + "CosiSign": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "data": { + "type": "bytes", + "id": 2 + }, + "global_commitment": { + "type": "bytes", + "id": 3 + }, + "global_pubkey": { + "type": "bytes", + "id": 4 + } + } + }, + "CosiSignature": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "BatchGetPublickeys": { + "fields": { + "ecdsa_curve_name": { + "type": "string", + "id": 1, + "options": { + "default": "ed25519" + } + }, + "paths": { + "rule": "repeated", + "type": "Path", + "id": 2 + }, + "include_node": { + "type": "bool", + "id": 3, + "options": { + "default": false + } + } + }, + "nested": { + "Path": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + } + } + } + } + }, + "EcdsaPublicKeys": { + "fields": { + "public_keys": { + "rule": "repeated", + "type": "bytes", + "id": 1 + }, + "hd_nodes": { + "rule": "repeated", + "type": "HDNodeType", + "id": 2 + }, + "root_fingerprint": { + "type": "uint32", + "id": 3 + } + } + }, + "EosGetPublicKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "EosPublicKey": { + "fields": { + "wif_public_key": { + "rule": "required", + "type": "string", + "id": 1 + }, + "raw_public_key": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "EosSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "chain_id": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "header": { + "rule": "required", + "type": "EosTxHeader", + "id": 3 + }, + "num_actions": { + "rule": "required", + "type": "uint32", + "id": 4 + } + }, + "nested": { + "EosTxHeader": { + "fields": { + "expiration": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "ref_block_num": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "ref_block_prefix": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "max_net_usage_words": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "max_cpu_usage_ms": { + "rule": "required", + "type": "uint32", + "id": 5 + }, + "delay_sec": { + "rule": "required", + "type": "uint32", + "id": 6 + } + } + } + } + }, + "EosTxActionRequest": { + "fields": { + "data_size": { + "type": "uint32", + "id": 1 + } + } + }, + "EosTxActionAck": { + "fields": { + "common": { + "rule": "required", + "type": "EosActionCommon", + "id": 1 + }, + "transfer": { + "type": "EosActionTransfer", + "id": 2 + }, + "delegate": { + "type": "EosActionDelegate", + "id": 3 + }, + "undelegate": { + "type": "EosActionUndelegate", + "id": 4 + }, + "refund": { + "type": "EosActionRefund", + "id": 5 + }, + "buy_ram": { + "type": "EosActionBuyRam", + "id": 6 + }, + "buy_ram_bytes": { + "type": "EosActionBuyRamBytes", + "id": 7 + }, + "sell_ram": { + "type": "EosActionSellRam", + "id": 8 + }, + "vote_producer": { + "type": "EosActionVoteProducer", + "id": 9 + }, + "update_auth": { + "type": "EosActionUpdateAuth", + "id": 10 + }, + "delete_auth": { + "type": "EosActionDeleteAuth", + "id": 11 + }, + "link_auth": { + "type": "EosActionLinkAuth", + "id": 12 + }, + "unlink_auth": { + "type": "EosActionUnlinkAuth", + "id": 13 + }, + "new_account": { + "type": "EosActionNewAccount", + "id": 14 + }, + "unknown": { + "type": "EosActionUnknown", + "id": 15 + } + }, + "nested": { + "EosAsset": { + "fields": { + "amount": { + "rule": "required", + "type": "sint64", + "id": 1 + }, + "symbol": { + "rule": "required", + "type": "uint64", + "id": 2 + } + } + }, + "EosPermissionLevel": { + "fields": { + "actor": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "permission": { + "rule": "required", + "type": "uint64", + "id": 2 + } + } + }, + "EosAuthorizationKey": { + "fields": { + "type": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "key": { + "type": "bytes", + "id": 2 + }, + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 3, + "options": { + "packed": false + } + }, + "weight": { + "rule": "required", + "type": "uint32", + "id": 4 + } + } + }, + "EosAuthorizationAccount": { + "fields": { + "account": { + "rule": "required", + "type": "EosPermissionLevel", + "id": 1 + }, + "weight": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + }, + "EosAuthorizationWait": { + "fields": { + "wait_sec": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "weight": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + }, + "EosAuthorization": { + "fields": { + "threshold": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "keys": { + "rule": "repeated", + "type": "EosAuthorizationKey", + "id": 2 + }, + "accounts": { + "rule": "repeated", + "type": "EosAuthorizationAccount", + "id": 3 + }, + "waits": { + "rule": "repeated", + "type": "EosAuthorizationWait", + "id": 4 + } + } + }, + "EosActionCommon": { + "fields": { + "account": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "name": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "authorization": { + "rule": "repeated", + "type": "EosPermissionLevel", + "id": 3 + } + } + }, + "EosActionTransfer": { + "fields": { + "sender": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "receiver": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "quantity": { + "rule": "required", + "type": "EosAsset", + "id": 3 + }, + "memo": { + "rule": "required", + "type": "string", + "id": 4 + } + } + }, + "EosActionDelegate": { + "fields": { + "sender": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "receiver": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "net_quantity": { + "rule": "required", + "type": "EosAsset", + "id": 3 + }, + "cpu_quantity": { + "rule": "required", + "type": "EosAsset", + "id": 4 + }, + "transfer": { + "rule": "required", + "type": "bool", + "id": 5 + } + } + }, + "EosActionUndelegate": { + "fields": { + "sender": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "receiver": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "net_quantity": { + "rule": "required", + "type": "EosAsset", + "id": 3 + }, + "cpu_quantity": { + "rule": "required", + "type": "EosAsset", + "id": 4 + } + } + }, + "EosActionRefund": { + "fields": { + "owner": { + "rule": "required", + "type": "uint64", + "id": 1 + } + } + }, + "EosActionBuyRam": { + "fields": { + "payer": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "receiver": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "quantity": { + "rule": "required", + "type": "EosAsset", + "id": 3 + } + } + }, + "EosActionBuyRamBytes": { + "fields": { + "payer": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "receiver": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "bytes": { + "rule": "required", + "type": "uint32", + "id": 3 + } + } + }, + "EosActionSellRam": { + "fields": { + "account": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "bytes": { + "rule": "required", + "type": "uint64", + "id": 2 + } + } + }, + "EosActionVoteProducer": { + "fields": { + "voter": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "proxy": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "producers": { + "rule": "repeated", + "type": "uint64", + "id": 3, + "options": { + "packed": false + } + } + } + }, + "EosActionUpdateAuth": { + "fields": { + "account": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "permission": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "parent": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "auth": { + "rule": "required", + "type": "EosAuthorization", + "id": 4 + } + } + }, + "EosActionDeleteAuth": { + "fields": { + "account": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "permission": { + "rule": "required", + "type": "uint64", + "id": 2 + } + } + }, + "EosActionLinkAuth": { + "fields": { + "account": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "code": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "type": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "requirement": { + "rule": "required", + "type": "uint64", + "id": 4 + } + } + }, + "EosActionUnlinkAuth": { + "fields": { + "account": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "code": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "type": { + "rule": "required", + "type": "uint64", + "id": 3 + } + } + }, + "EosActionNewAccount": { + "fields": { + "creator": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "name": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "owner": { + "rule": "required", + "type": "EosAuthorization", + "id": 3 + }, + "active": { + "rule": "required", + "type": "EosAuthorization", + "id": 4 + } + } + }, + "EosActionUnknown": { + "fields": { + "data_size": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "data_chunk": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + } + } + }, + "EosSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "EthereumGetPublicKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "EthereumPublicKey": { + "fields": { + "node": { + "rule": "required", + "type": "HDNodeType", + "id": 1 + }, + "xpub": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "EthereumGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + }, + "encoded_network": { + "type": "bytes", + "id": 3 + } + } + }, + "EthereumAddress": { + "fields": { + "_old_address": { + "type": "bytes", + "id": 1, + "options": { + "deprecated": true + } + }, + "address": { + "type": "string", + "id": 2 + } + } + }, + "EthereumSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "nonce": { + "type": "bytes", + "id": 2, + "options": { + "default": "" + } + }, + "gas_price": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "gas_limit": { + "rule": "required", + "type": "bytes", + "id": 4 + }, + "to": { + "type": "string", + "id": 11, + "options": { + "default": "" + } + }, + "value": { + "type": "bytes", + "id": 6, + "options": { + "default": "" + } + }, + "data_initial_chunk": { + "type": "bytes", + "id": 7, + "options": { + "default": "" + } + }, + "data_length": { + "type": "uint32", + "id": 8, + "options": { + "default": 0 + } + }, + "chain_id": { + "rule": "required", + "type": "uint64", + "id": 9 + }, + "tx_type": { + "type": "uint32", + "id": 10 + }, + "definitions": { + "type": "EthereumDefinitions", + "id": 12 + } + } + }, + "EthereumSignTxEIP1559": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "nonce": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "max_gas_fee": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "max_priority_fee": { + "rule": "required", + "type": "bytes", + "id": 4 + }, + "gas_limit": { + "rule": "required", + "type": "bytes", + "id": 5 + }, + "to": { + "type": "string", + "id": 6, + "options": { + "default": "" + } + }, + "value": { + "rule": "required", + "type": "bytes", + "id": 7 + }, + "data_initial_chunk": { + "type": "bytes", + "id": 8, + "options": { + "default": "" + } + }, + "data_length": { + "rule": "required", + "type": "uint32", + "id": 9 + }, + "chain_id": { + "rule": "required", + "type": "uint64", + "id": 10 + }, + "access_list": { + "rule": "repeated", + "type": "EthereumAccessList", + "id": 11 + }, + "definitions": { + "type": "EthereumDefinitions", + "id": 12 + } + }, + "nested": { + "EthereumAccessList": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + }, + "storage_keys": { + "rule": "repeated", + "type": "bytes", + "id": 2 + } + } + } + } + }, + "EthereumTxRequest": { + "fields": { + "data_length": { + "type": "uint32", + "id": 1 + }, + "signature_v": { + "type": "uint32", + "id": 2 + }, + "signature_r": { + "type": "bytes", + "id": 3 + }, + "signature_s": { + "type": "bytes", + "id": 4 + } + } + }, + "EthereumTxAck": { + "fields": { + "data_chunk": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "EthereumSignMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "encoded_network": { + "type": "bytes", + "id": 3 + } + } + }, + "EthereumMessageSignature": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "address": { + "rule": "required", + "type": "string", + "id": 3 + } + } + }, + "EthereumVerifyMessage": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "address": { + "rule": "required", + "type": "string", + "id": 4 + } + } + }, + "EthereumSignTypedHash": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "domain_separator_hash": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "message_hash": { + "type": "bytes", + "id": 3 + }, + "encoded_network": { + "type": "bytes", + "id": 4 + } + } + }, + "EthereumTypedDataSignature": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "address": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "EthereumDefinitionType": { + "values": { + "NETWORK": 0, + "TOKEN": 1 + } + }, + "EthereumNetworkInfo": { + "fields": { + "chain_id": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "symbol": { + "rule": "required", + "type": "string", + "id": 2 + }, + "slip44": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "name": { + "rule": "required", + "type": "string", + "id": 4 + }, + "icon": { + "type": "string", + "id": 101 + }, + "primary_color": { + "type": "uint64", + "id": 102 + } + } + }, + "EthereumTokenInfo": { + "fields": { + "address": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "chain_id": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "symbol": { + "rule": "required", + "type": "string", + "id": 3 + }, + "decimals": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "name": { + "rule": "required", + "type": "string", + "id": 5 + } + } + }, + "EthereumDefinitions": { + "fields": { + "encoded_network": { + "type": "bytes", + "id": 1 + }, + "encoded_token": { + "type": "bytes", + "id": 2 + } + } + }, + "EthereumSignTypedData": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "primary_type": { + "rule": "required", + "type": "string", + "id": 2 + }, + "metamask_v4_compat": { + "type": "bool", + "id": 3, + "options": { + "default": true + } + }, + "definitions": { + "type": "EthereumDefinitions", + "id": 4 + } + } + }, + "EthereumTypedDataStructRequest": { + "fields": { + "name": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "EthereumTypedDataStructAck": { + "fields": { + "members": { + "rule": "repeated", + "type": "EthereumStructMember", + "id": 1 + } + }, + "nested": { + "EthereumStructMember": { + "fields": { + "type": { + "rule": "required", + "type": "EthereumFieldType", + "id": 1 + }, + "name": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "EthereumFieldType": { + "fields": { + "data_type": { + "rule": "required", + "type": "EthereumDataType", + "id": 1 + }, + "size": { + "type": "uint32", + "id": 2 + }, + "entry_type": { + "type": "EthereumFieldType", + "id": 3 + }, + "struct_name": { + "type": "string", + "id": 4 + } + } + }, + "EthereumDataType": { + "values": { + "UINT": 1, + "INT": 2, + "BYTES": 3, + "STRING": 4, + "BOOL": 5, + "ADDRESS": 6, + "ARRAY": 7, + "STRUCT": 8 + } + } + } + }, + "EthereumTypedDataValueRequest": { + "fields": { + "member_path": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + } + } + }, + "EthereumTypedDataValueAck": { + "fields": { + "value": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "EthereumSignTypedDataOneKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "primary_type": { + "rule": "required", + "type": "string", + "id": 2 + }, + "metamask_v4_compat": { + "type": "bool", + "id": 3, + "options": { + "default": true + } + }, + "chain_id": { + "type": "uint64", + "id": 4 + } + } + }, + "EthereumTypedDataStructRequestOneKey": { + "fields": { + "name": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "EthereumTypedDataStructAckOneKey": { + "fields": { + "members": { + "rule": "repeated", + "type": "EthereumStructMemberOneKey", + "id": 1 + } + }, + "nested": { + "EthereumStructMemberOneKey": { + "fields": { + "type": { + "rule": "required", + "type": "EthereumFieldTypeOneKey", + "id": 1 + }, + "name": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "EthereumFieldTypeOneKey": { + "fields": { + "data_type": { + "rule": "required", + "type": "EthereumDataTypeOneKey", + "id": 1 + }, + "size": { + "type": "uint32", + "id": 2 + }, + "entry_type": { + "type": "EthereumFieldTypeOneKey", + "id": 3 + }, + "struct_name": { + "type": "string", + "id": 4 + } + } + }, + "EthereumDataTypeOneKey": { + "values": { + "UINT": 1, + "INT": 2, + "BYTES": 3, + "STRING": 4, + "BOOL": 5, + "ADDRESS": 6, + "ARRAY": 7, + "STRUCT": 8 + } + } + } + }, + "EthereumTypedDataValueRequestOneKey": { + "fields": { + "member_path": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + } + } + }, + "EthereumTypedDataValueAckOneKey": { + "fields": { + "value": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "EthereumGnosisSafeTxOperation": { + "values": { + "CALL": 0, + "DELEGATE_CALL": 1 + } + }, + "EthereumGnosisSafeTxRequest": { + "fields": {} + }, + "EthereumGnosisSafeTxAck": { + "fields": { + "to": { + "rule": "required", + "type": "string", + "id": 1 + }, + "value": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "data": { + "type": "bytes", + "id": 3 + }, + "operation": { + "rule": "required", + "type": "EthereumGnosisSafeTxOperation", + "id": 4 + }, + "safeTxGas": { + "rule": "required", + "type": "bytes", + "id": 5 + }, + "baseGas": { + "rule": "required", + "type": "bytes", + "id": 6 + }, + "gasPrice": { + "rule": "required", + "type": "bytes", + "id": 7 + }, + "gasToken": { + "rule": "required", + "type": "string", + "id": 8 + }, + "refundReceiver": { + "rule": "required", + "type": "string", + "id": 9 + }, + "nonce": { + "rule": "required", + "type": "bytes", + "id": 10 + }, + "chain_id": { + "rule": "required", + "type": "uint64", + "id": 11 + }, + "verifyingContract": { + "rule": "required", + "type": "string", + "id": 12 + } + } + }, + "EthereumSignTypedDataQR": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "json_data": { + "type": "bytes", + "id": 2 + }, + "chain_id": { + "type": "uint64", + "id": 3 + }, + "metamask_v4_compat": { + "type": "bool", + "id": 4, + "options": { + "default": true + } + }, + "request_id": { + "type": "bytes", + "id": 5 + } + } + }, + "EthereumGetPublicKeyOneKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + }, + "chain_id": { + "type": "uint64", + "id": 3 + } + } + }, + "EthereumPublicKeyOneKey": { + "fields": { + "node": { + "rule": "required", + "type": "HDNodeType", + "id": 1 + }, + "xpub": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "EthereumGetAddressOneKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + }, + "chain_id": { + "type": "uint64", + "id": 3 + } + } + }, + "EthereumAddressOneKey": { + "fields": { + "_old_address": { + "type": "bytes", + "id": 1, + "options": { + "deprecated": true + } + }, + "address": { + "type": "string", + "id": 2 + } + } + }, + "EthereumSignTxOneKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "nonce": { + "type": "bytes", + "id": 2, + "options": { + "default": "" + } + }, + "gas_price": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "gas_limit": { + "rule": "required", + "type": "bytes", + "id": 4 + }, + "to": { + "type": "string", + "id": 11, + "options": { + "default": "" + } + }, + "value": { + "type": "bytes", + "id": 6, + "options": { + "default": "" + } + }, + "data_initial_chunk": { + "type": "bytes", + "id": 7, + "options": { + "default": "" + } + }, + "data_length": { + "type": "uint32", + "id": 8, + "options": { + "default": 0 + } + }, + "chain_id": { + "rule": "required", + "type": "uint64", + "id": 9 + }, + "tx_type": { + "type": "uint32", + "id": 10 + } + } + }, + "EthereumAccessListOneKey": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + }, + "storage_keys": { + "rule": "repeated", + "type": "bytes", + "id": 2 + } + } + }, + "EthereumSignTxEIP1559OneKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "nonce": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "max_gas_fee": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "max_priority_fee": { + "rule": "required", + "type": "bytes", + "id": 4 + }, + "gas_limit": { + "rule": "required", + "type": "bytes", + "id": 5 + }, + "to": { + "type": "string", + "id": 6, + "options": { + "default": "" + } + }, + "value": { + "rule": "required", + "type": "bytes", + "id": 7 + }, + "data_initial_chunk": { + "type": "bytes", + "id": 8, + "options": { + "default": "" + } + }, + "data_length": { + "rule": "required", + "type": "uint32", + "id": 9 + }, + "chain_id": { + "rule": "required", + "type": "uint64", + "id": 10 + }, + "access_list": { + "rule": "repeated", + "type": "EthereumAccessListOneKey", + "id": 11 + } + } + }, + "EthereumAuthorizationSignature": { + "fields": { + "y_parity": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "r": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "s": { + "rule": "required", + "type": "bytes", + "id": 3 + } + } + }, + "EthereumSignTxEIP7702OneKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "nonce": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "max_gas_fee": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "max_priority_fee": { + "rule": "required", + "type": "bytes", + "id": 4 + }, + "gas_limit": { + "rule": "required", + "type": "bytes", + "id": 5 + }, + "to": { + "rule": "required", + "type": "string", + "id": 6 + }, + "value": { + "rule": "required", + "type": "bytes", + "id": 7 + }, + "data_initial_chunk": { + "type": "bytes", + "id": 8, + "options": { + "default": "" + } + }, + "data_length": { + "rule": "required", + "type": "uint32", + "id": 9 + }, + "chain_id": { + "rule": "required", + "type": "uint64", + "id": 10 + }, + "access_list": { + "rule": "repeated", + "type": "EthereumAccessListOneKey", + "id": 11 + }, + "authorization_list": { + "rule": "repeated", + "type": "EthereumAuthorizationOneKey", + "id": 12 + } + }, + "nested": { + "EthereumAuthorizationOneKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "chain_id": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "address": { + "rule": "required", + "type": "string", + "id": 3 + }, + "nonce": { + "rule": "required", + "type": "bytes", + "id": 4 + }, + "signature": { + "type": "EthereumAuthorizationSignature", + "id": 5 + } + } + } + } + }, + "EthereumTxRequestOneKey": { + "fields": { + "data_length": { + "type": "uint32", + "id": 1 + }, + "signature_v": { + "type": "uint32", + "id": 2 + }, + "signature_r": { + "type": "bytes", + "id": 3 + }, + "signature_s": { + "type": "bytes", + "id": 4 + }, + "authorization_signatures": { + "rule": "repeated", + "type": "EthereumAuthorizationSignature", + "id": 10 + } + } + }, + "EthereumTxAckOneKey": { + "fields": { + "data_chunk": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "EthereumSignMessageOneKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "chain_id": { + "type": "uint64", + "id": 3 + } + } + }, + "EthereumMessageSignatureOneKey": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "address": { + "rule": "required", + "type": "string", + "id": 3 + } + } + }, + "EthereumVerifyMessageOneKey": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "address": { + "rule": "required", + "type": "string", + "id": 4 + }, + "chain_id": { + "type": "uint64", + "id": 5 + } + } + }, + "EthereumSignTypedHashOneKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "domain_separator_hash": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "message_hash": { + "type": "bytes", + "id": 3 + }, + "chain_id": { + "type": "uint64", + "id": 4 + } + } + }, + "EthereumTypedDataSignatureOneKey": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "address": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "FilecoinGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + }, + "testnet": { + "type": "bool", + "id": 3 + } + } + }, + "FilecoinAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + } + } + }, + "FilecoinSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "testnet": { + "type": "bool", + "id": 3 + } + } + }, + "FilecoinSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "InternalMyAddressRequest": { + "fields": { + "coin_type": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "chain_id": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "account_index": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "derive_type": { + "rule": "required", + "type": "uint32", + "id": 4 + } + } + }, + "KaspaGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + }, + "prefix": { + "type": "string", + "id": 3, + "options": { + "default": "kaspa" + } + }, + "scheme": { + "type": "string", + "id": 4, + "options": { + "default": "schnorr" + } + }, + "use_tweak": { + "type": "bool", + "id": 5, + "options": { + "default": true + } + } + } + }, + "KaspaAddress": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "KaspaSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_message": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "scheme": { + "type": "string", + "id": 3, + "options": { + "default": "schnorr" + } + }, + "prefix": { + "type": "string", + "id": 4, + "options": { + "default": "kaspa" + } + }, + "input_count": { + "type": "uint32", + "id": 5, + "options": { + "default": 1 + } + }, + "use_tweak": { + "type": "bool", + "id": 6, + "options": { + "default": true + } + } + } + }, + "KaspaTxInputRequest": { + "fields": { + "request_index": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "signature": { + "type": "bytes", + "id": 2 + } + } + }, + "KaspaTxInputAck": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_message": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "KaspaSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "LnurlAuth": { + "fields": { + "domain": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "data": { + "rule": "required", + "type": "bytes", + "id": 3 + } + } + }, + "LnurlAuthResp": { + "fields": { + "publickey": { + "type": "string", + "id": 1 + }, + "path": { + "type": "string", + "id": 2 + }, + "signature": { + "type": "bytes", + "id": 3 + } + } + }, + "BackupType": { + "values": { + "Bip39": 0, + "Slip39_Basic": 1, + "Slip39_Advanced": 2, + "Slip39_Single_Extendable": 3, + "Slip39_Basic_Extendable": 4, + "Slip39_Advanced_Extendable": 5 + } + }, + "SafetyCheckLevel": { + "values": { + "Strict": 0, + "PromptAlways": 1, + "PromptTemporarily": 2 + } + }, + "Initialize": { + "fields": { + "session_id": { + "type": "bytes", + "id": 1 + }, + "_skip_passphrase": { + "type": "bool", + "id": 2, + "options": { + "deprecated": true + } + }, + "derive_cardano": { + "type": "bool", + "id": 3 + }, + "passphrase_state": { + "type": "string", + "id": 8000 + }, + "is_contains_attach": { + "type": "bool", + "id": 8001 + } + } + }, + "StartSession": { + "fields": { + "session_id": { + "type": "bytes", + "id": 1 + }, + "_skip_passphrase": { + "type": "bool", + "id": 2, + "options": { + "deprecated": true + } + }, + "derive_cardano": { + "type": "bool", + "id": 3 + } + } + }, + "EndSession": { + "fields": {} + }, + "GetFeatures": { + "fields": {} + }, + "OnekeyGetFeatures": { + "fields": {} + }, + "OneKeyDeviceType": { + "values": { + "CLASSIC": 0, + "CLASSIC1S": 1, + "MINI": 2, + "TOUCH": 3, + "PRO": 5 + } + }, + "OneKeySeType": { + "values": { + "THD89": 0, + "SE608A": 1 + } + }, + "OneKeySEState": { + "values": { + "BOOT": 0, + "APP": 1 + } + }, + "Features": { + "fields": { + "vendor": { + "type": "string", + "id": 1 + }, + "major_version": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "minor_version": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "patch_version": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "bootloader_mode": { + "type": "bool", + "id": 5 + }, + "device_id": { + "type": "string", + "id": 6 + }, + "pin_protection": { + "type": "bool", + "id": 7 + }, + "passphrase_protection": { + "type": "bool", + "id": 8 + }, + "language": { + "type": "string", + "id": 9 + }, + "label": { + "type": "string", + "id": 10 + }, + "initialized": { + "type": "bool", + "id": 12 + }, + "revision": { + "type": "bytes", + "id": 13 + }, + "bootloader_hash": { + "type": "bytes", + "id": 14 + }, + "imported": { + "type": "bool", + "id": 15 + }, + "unlocked": { + "type": "bool", + "id": 16 + }, + "_passphrase_cached": { + "type": "bool", + "id": 17, + "options": { + "deprecated": true + } + }, + "firmware_present": { + "type": "bool", + "id": 18 + }, + "needs_backup": { + "type": "bool", + "id": 19 + }, + "flags": { + "type": "uint32", + "id": 20 + }, + "model": { + "type": "string", + "id": 21 + }, + "fw_major": { + "type": "uint32", + "id": 22 + }, + "fw_minor": { + "type": "uint32", + "id": 23 + }, + "fw_patch": { + "type": "uint32", + "id": 24 + }, + "fw_vendor": { + "type": "string", + "id": 25 + }, + "unfinished_backup": { + "type": "bool", + "id": 27 + }, + "no_backup": { + "type": "bool", + "id": 28 + }, + "recovery_mode": { + "type": "bool", + "id": 29 + }, + "capabilities": { + "rule": "repeated", + "type": "Capability", + "id": 30, + "options": { + "packed": false + } + }, + "backup_type": { + "type": "BackupType", + "id": 31 + }, + "sd_card_present": { + "type": "bool", + "id": 32 + }, + "sd_protection": { + "type": "bool", + "id": 33 + }, + "wipe_code_protection": { + "type": "bool", + "id": 34 + }, + "session_id": { + "type": "bytes", + "id": 35 + }, + "passphrase_always_on_device": { + "type": "bool", + "id": 36 + }, + "safety_checks": { + "type": "SafetyCheckLevel", + "id": 37 + }, + "auto_lock_delay_ms": { + "type": "uint32", + "id": 38 + }, + "display_rotation": { + "type": "uint32", + "id": 39 + }, + "experimental_features": { + "type": "bool", + "id": 40 + }, + "offset": { + "type": "uint32", + "id": 500 + }, + "coprocessor_bt_name": { + "type": "string", + "id": 501 + }, + "coprocessor_version": { + "type": "string", + "id": 502 + }, + "coprocessor_bt_enable": { + "type": "bool", + "id": 503 + }, + "se_enable": { + "type": "bool", + "id": 504 + }, + "se_ver": { + "type": "string", + "id": 506 + }, + "backup_only": { + "type": "bool", + "id": 507 + }, + "onekey_version": { + "type": "string", + "id": 508 + }, + "onekey_serial": { + "type": "string", + "id": 509 + }, + "bootloader_version": { + "type": "string", + "id": 510 + }, + "serial_no": { + "type": "string", + "id": 511 + }, + "spi_flash": { + "type": "string", + "id": 512 + }, + "initstates": { + "type": "uint32", + "id": 513 + }, + "NFT_voucher": { + "type": "bytes", + "id": 514 + }, + "cpu_info": { + "type": "string", + "id": 515 + }, + "pre_firmware": { + "type": "string", + "id": 516 + }, + "coin_switch": { + "type": "uint32", + "id": 517 + }, + "build_id": { + "type": "bytes", + "id": 518 + }, + "romloader_version": { + "type": "string", + "id": 519 + }, + "busy": { + "type": "bool", + "id": 41 + }, + "onekey_device_type": { + "type": "OneKeyDeviceType", + "id": 600 + }, + "onekey_se_type": { + "type": "OneKeySeType", + "id": 601 + }, + "onekey_romloader_version": { + "type": "string", + "id": 602 + }, + "onekey_romloader_hash": { + "type": "bytes", + "id": 603 + }, + "onekey_bootloader_version": { + "type": "string", + "id": 604 + }, + "onekey_bootloader_hash": { + "type": "bytes", + "id": 605 + }, + "onekey_se01_version": { + "type": "string", + "id": 606 + }, + "onekey_se01_hash": { + "type": "bytes", + "id": 607 + }, + "onekey_se01_build_id": { + "type": "string", + "id": 608 + }, + "onekey_firmware_version": { + "type": "string", + "id": 609 + }, + "onekey_firmware_hash": { + "type": "bytes", + "id": 610 + }, + "onekey_firmware_build_id": { + "type": "string", + "id": 611 + }, + "onekey_serial_no": { + "type": "string", + "id": 612 + }, + "onekey_bootloader_build_id": { + "type": "string", + "id": 613 + }, + "onekey_coprocessor_bt_name": { + "type": "string", + "id": 614 + }, + "onekey_coprocessor_version": { + "type": "string", + "id": 615 + }, + "onekey_coprocessor_build_id": { + "type": "string", + "id": 616 + }, + "onekey_coprocessor_hash": { + "type": "bytes", + "id": 617 + }, + "onekey_se02_version": { + "type": "string", + "id": 618 + }, + "onekey_se03_version": { + "type": "string", + "id": 619 + }, + "onekey_se04_version": { + "type": "string", + "id": 620 + }, + "onekey_se01_state": { + "type": "OneKeySEState", + "id": 621 + }, + "onekey_se02_state": { + "type": "OneKeySEState", + "id": 622 + }, + "onekey_se03_state": { + "type": "OneKeySEState", + "id": 623 + }, + "onekey_se04_state": { + "type": "OneKeySEState", + "id": 624 + }, + "attach_to_pin_user": { + "type": "bool", + "id": 625 + }, + "unlocked_attach_pin": { + "type": "bool", + "id": 626 + } + }, + "nested": { + "Capability": { + "options": { + "(has_bitcoin_only_values)": true + }, + "values": { + "Capability_Bitcoin": 1, + "Capability_Bitcoin_like": 2, + "Capability_Binance": 3, + "Capability_Cardano": 4, + "Capability_Crypto": 5, + "Capability_EOS": 6, + "Capability_Ethereum": 7, + "Capability_Lisk": 8, + "Capability_Monero": 9, + "Capability_NEM": 10, + "Capability_Ripple": 11, + "Capability_Stellar": 12, + "Capability_Tezos": 13, + "Capability_U2F": 14, + "Capability_Shamir": 15, + "Capability_ShamirGroups": 16, + "Capability_PassphraseEntry": 17, + "Capability_AttachToPin": 18, + "Capability_EthereumTypedData": 1000 + } + } + } + }, + "OnekeyFeatures": { + "fields": { + "onekey_device_type": { + "type": "OneKeyDeviceType", + "id": 1 + }, + "onekey_romloader_version": { + "type": "string", + "id": 2 + }, + "onekey_bootloader_version": { + "type": "string", + "id": 3 + }, + "onekey_firmware_version": { + "type": "string", + "id": 4 + }, + "onekey_romloader_hash": { + "type": "bytes", + "id": 5 + }, + "onekey_bootloader_hash": { + "type": "bytes", + "id": 6 + }, + "onekey_firmware_hash": { + "type": "bytes", + "id": 7 + }, + "onekey_romloader_build_id": { + "type": "string", + "id": 8 + }, + "onekey_bootloader_build_id": { + "type": "string", + "id": 9 + }, + "onekey_firmware_build_id": { + "type": "string", + "id": 10 + }, + "onekey_serial_no": { + "type": "string", + "id": 11 + }, + "onekey_coprocessor_bt_name": { + "type": "string", + "id": 12 + }, + "onekey_coprocessor_version": { + "type": "string", + "id": 13 + }, + "onekey_coprocessor_build_id": { + "type": "string", + "id": 14 + }, + "onekey_coprocessor_hash": { + "type": "bytes", + "id": 15 + }, + "onekey_se_type": { + "type": "OneKeySeType", + "id": 16 + }, + "onekey_se01_state": { + "type": "OneKeySEState", + "id": 17 + }, + "onekey_se02_state": { + "type": "OneKeySEState", + "id": 18 + }, + "onekey_se03_state": { + "type": "OneKeySEState", + "id": 19 + }, + "onekey_se04_state": { + "type": "OneKeySEState", + "id": 20 + }, + "onekey_se01_version": { + "type": "string", + "id": 21 + }, + "onekey_se02_version": { + "type": "string", + "id": 22 + }, + "onekey_se03_version": { + "type": "string", + "id": 23 + }, + "onekey_se04_version": { + "type": "string", + "id": 24 + }, + "onekey_se01_hash": { + "type": "bytes", + "id": 25 + }, + "onekey_se02_hash": { + "type": "bytes", + "id": 26 + }, + "onekey_se03_hash": { + "type": "bytes", + "id": 27 + }, + "onekey_se04_hash": { + "type": "bytes", + "id": 28 + }, + "onekey_se01_build_id": { + "type": "string", + "id": 29 + }, + "onekey_se02_build_id": { + "type": "string", + "id": 30 + }, + "onekey_se03_build_id": { + "type": "string", + "id": 31 + }, + "onekey_se04_build_id": { + "type": "string", + "id": 32 + }, + "onekey_se01_bootloader_version": { + "type": "string", + "id": 33 + }, + "onekey_se02_bootloader_version": { + "type": "string", + "id": 34 + }, + "onekey_se03_bootloader_version": { + "type": "string", + "id": 35 + }, + "onekey_se04_bootloader_version": { + "type": "string", + "id": 36 + }, + "onekey_se01_bootloader_hash": { + "type": "bytes", + "id": 37 + }, + "onekey_se02_bootloader_hash": { + "type": "bytes", + "id": 38 + }, + "onekey_se03_bootloader_hash": { + "type": "bytes", + "id": 39 + }, + "onekey_se04_bootloader_hash": { + "type": "bytes", + "id": 40 + }, + "onekey_se01_bootloader_build_id": { + "type": "string", + "id": 41 + }, + "onekey_se02_bootloader_build_id": { + "type": "string", + "id": 42 + }, + "onekey_se03_bootloader_build_id": { + "type": "string", + "id": 43 + }, + "onekey_se04_bootloader_build_id": { + "type": "string", + "id": 44 + } + } + }, + "LockDevice": { + "fields": {} + }, + "SetBusy": { + "fields": { + "expiry_ms": { + "type": "uint32", + "id": 1 + } + } + }, + "ApplySettings": { + "fields": { + "language": { + "type": "string", + "id": 1 + }, + "label": { + "type": "string", + "id": 2 + }, + "use_passphrase": { + "type": "bool", + "id": 3 + }, + "homescreen": { + "type": "bytes", + "id": 4 + }, + "_passphrase_source": { + "type": "uint32", + "id": 5, + "options": { + "deprecated": true + } + }, + "auto_lock_delay_ms": { + "type": "uint32", + "id": 6 + }, + "display_rotation": { + "type": "uint32", + "id": 7 + }, + "passphrase_always_on_device": { + "type": "bool", + "id": 8 + }, + "safety_checks": { + "type": "SafetyCheckLevel", + "id": 9 + }, + "experimental_features": { + "type": "bool", + "id": 10 + } + } + }, + "ApplyFlags": { + "fields": { + "flags": { + "rule": "required", + "type": "uint32", + "id": 1 + } + } + }, + "ChangePin": { + "fields": { + "remove": { + "type": "bool", + "id": 1 + } + } + }, + "ChangeWipeCode": { + "fields": { + "remove": { + "type": "bool", + "id": 1 + } + } + }, + "SdProtect": { + "fields": { + "operation": { + "rule": "required", + "type": "SdProtectOperationType", + "id": 1 + } + }, + "nested": { + "SdProtectOperationType": { + "values": { + "DISABLE": 0, + "ENABLE": 1, + "REFRESH": 2 + } + } + } + }, + "Cancel": { + "fields": {} + }, + "GetEntropy": { + "fields": { + "size": { + "rule": "required", + "type": "uint32", + "id": 1 + } + } + }, + "Entropy": { + "fields": { + "entropy": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "GetFirmwareHash": { + "fields": { + "challenge": { + "type": "bytes", + "id": 1 + } + } + }, + "FirmwareHash": { + "fields": { + "hash": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "WipeDevice": { + "fields": {} + }, + "LoadDevice": { + "fields": { + "mnemonics": { + "rule": "repeated", + "type": "string", + "id": 1 + }, + "pin": { + "type": "string", + "id": 3 + }, + "passphrase_protection": { + "type": "bool", + "id": 4 + }, + "language": { + "type": "string", + "id": 5, + "options": { + "default": "en-US" + } + }, + "label": { + "type": "string", + "id": 6 + }, + "skip_checksum": { + "type": "bool", + "id": 7 + }, + "u2f_counter": { + "type": "uint32", + "id": 8 + }, + "needs_backup": { + "type": "bool", + "id": 9 + }, + "no_backup": { + "type": "bool", + "id": 10 + } + } + }, + "ResetDevice": { + "fields": { + "display_random": { + "type": "bool", + "id": 1 + }, + "strength": { + "type": "uint32", + "id": 2, + "options": { + "default": 256 + } + }, + "passphrase_protection": { + "type": "bool", + "id": 3 + }, + "pin_protection": { + "type": "bool", + "id": 4 + }, + "language": { + "type": "string", + "id": 5, + "options": { + "default": "en-US" + } + }, + "label": { + "type": "string", + "id": 6 + }, + "u2f_counter": { + "type": "uint32", + "id": 7 + }, + "skip_backup": { + "type": "bool", + "id": 8 + }, + "no_backup": { + "type": "bool", + "id": 9 + }, + "backup_type": { + "type": "BackupType", + "id": 10, + "options": { + "default": "Bip39" + } + } + } + }, + "BackupDevice": { + "fields": {} + }, + "EntropyRequest": { + "fields": {} + }, + "EntropyAck": { + "fields": { + "entropy": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "RecoveryDevice": { + "fields": { + "word_count": { + "type": "uint32", + "id": 1 + }, + "passphrase_protection": { + "type": "bool", + "id": 2 + }, + "pin_protection": { + "type": "bool", + "id": 3 + }, + "language": { + "type": "string", + "id": 4 + }, + "label": { + "type": "string", + "id": 5 + }, + "enforce_wordlist": { + "type": "bool", + "id": 6 + }, + "type": { + "type": "RecoveryDeviceType", + "id": 8 + }, + "u2f_counter": { + "type": "uint32", + "id": 9 + }, + "dry_run": { + "type": "bool", + "id": 10 + } + }, + "nested": { + "RecoveryDeviceType": { + "values": { + "RecoveryDeviceType_ScrambledWords": 0, + "RecoveryDeviceType_Matrix": 1 + } + } + } + }, + "WordRequest": { + "fields": { + "type": { + "rule": "required", + "type": "WordRequestType", + "id": 1 + } + }, + "nested": { + "WordRequestType": { + "values": { + "WordRequestType_Plain": 0, + "WordRequestType_Matrix9": 1, + "WordRequestType_Matrix6": 2 + } + } + } + }, + "WordAck": { + "fields": { + "word": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "SetU2FCounter": { + "fields": { + "u2f_counter": { + "rule": "required", + "type": "uint32", + "id": 1 + } + } + }, + "GetNextU2FCounter": { + "fields": {} + }, + "NextU2FCounter": { + "fields": { + "u2f_counter": { + "rule": "required", + "type": "uint32", + "id": 1 + } + } + }, + "DoPreauthorized": { + "fields": {} + }, + "PreauthorizedRequest": { + "fields": {} + }, + "CancelAuthorization": { + "fields": {} + }, + "GetNonce": { + "options": { + "(experimental_message)": true + }, + "fields": {} + }, + "Nonce": { + "options": { + "(experimental_message)": true + }, + "fields": { + "nonce": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "WriteSEPrivateKey": { + "fields": { + "private_key": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "ReadSEPublicKey": { + "fields": {} + }, + "SEPublicKey": { + "fields": { + "public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "WriteSEPublicCert": { + "fields": { + "public_cert": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "ReadSEPublicCert": { + "fields": {} + }, + "SEPublicCert": { + "fields": { + "public_cert": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "SESignMessage": { + "fields": { + "message": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "SEMessageSignature": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "ResourceUpload": { + "fields": { + "extension": { + "rule": "required", + "type": "string", + "id": 1 + }, + "data_length": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "res_type": { + "rule": "required", + "type": "ResourceType", + "id": 3 + }, + "nft_meta_data": { + "type": "bytes", + "id": 4 + }, + "zoom_data_length": { + "rule": "required", + "type": "uint32", + "id": 5 + }, + "file_name_no_ext": { + "type": "string", + "id": 6 + }, + "blur_data_length": { + "type": "uint32", + "id": 7 + } + }, + "nested": { + "ResourceType": { + "values": { + "WallPaper": 0, + "Nft": 1 + } + } + } + }, + "ZoomRequest": { + "fields": { + "offset": { + "type": "uint32", + "id": 1 + }, + "data_length": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + }, + "BlurRequest": { + "fields": { + "offset": { + "type": "uint32", + "id": 1 + }, + "data_length": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + }, + "ResourceRequest": { + "fields": { + "offset": { + "type": "uint32", + "id": 1 + }, + "data_length": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + }, + "ResourceAck": { + "fields": { + "data_chunk": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "hash": { + "type": "bytes", + "id": 2 + } + } + }, + "WallpaperTarget": { + "values": { + "Home": 0, + "Lock": 1 + } + }, + "SetWallpaper": { + "fields": { + "target": { + "rule": "required", + "type": "WallpaperTarget", + "id": 1 + }, + "path": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "GetWallpaper": { + "fields": { + "target": { + "rule": "required", + "type": "WallpaperTarget", + "id": 1 + } + } + }, + "Wallpaper": { + "fields": { + "target": { + "rule": "required", + "type": "WallpaperTarget", + "id": 1 + }, + "path": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "ResourceUpdate": { + "fields": { + "file_name": { + "rule": "required", + "type": "string", + "id": 1 + }, + "data_length": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "initial_data_chunk": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "hash": { + "type": "bytes", + "id": 4 + } + } + }, + "ListResDir": { + "fields": { + "path": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "FileInfoList": { + "fields": { + "files": { + "rule": "repeated", + "type": "FileInfo", + "id": 1 + } + }, + "nested": { + "FileInfo": { + "fields": { + "name": { + "rule": "required", + "type": "string", + "id": 1 + }, + "size": { + "rule": "required", + "type": "uint64", + "id": 2 + } + } + } + } + }, + "UnlockPath": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "mac": { + "type": "bytes", + "id": 2 + } + } + }, + "UnlockedPathRequest": { + "fields": { + "mac": { + "type": "bytes", + "id": 1 + } + } + }, + "GetPassphraseState": { + "fields": { + "passphrase_state": { + "type": "string", + "id": 1 + } + } + }, + "PassphraseState": { + "fields": { + "passphrase_state": { + "type": "string", + "id": 1 + }, + "session_id": { + "type": "bytes", + "id": 2 + }, + "unlocked_attach_pin": { + "type": "bool", + "id": 3 + } + } + }, + "UnLockDevice": { + "fields": {} + }, + "UnLockDeviceResponse": { + "fields": { + "unlocked": { + "type": "bool", + "id": 1 + }, + "unlocked_attach_pin": { + "type": "bool", + "id": 2 + }, + "passphrase_protection": { + "type": "bool", + "id": 3 + } + } + }, + "MoneroNetworkType": { + "values": { + "MAINNET": 0, + "TESTNET": 1, + "STAGENET": 2, + "FAKECHAIN": 3 + } + }, + "MoneroTransactionSourceEntry": { + "fields": { + "outputs": { + "rule": "repeated", + "type": "MoneroOutputEntry", + "id": 1 + }, + "real_output": { + "type": "uint64", + "id": 2 + }, + "real_out_tx_key": { + "type": "bytes", + "id": 3 + }, + "real_out_additional_tx_keys": { + "rule": "repeated", + "type": "bytes", + "id": 4 + }, + "real_output_in_tx_index": { + "type": "uint64", + "id": 5 + }, + "amount": { + "type": "uint64", + "id": 6 + }, + "rct": { + "type": "bool", + "id": 7 + }, + "mask": { + "type": "bytes", + "id": 8 + }, + "multisig_kLRki": { + "type": "MoneroMultisigKLRki", + "id": 9 + }, + "subaddr_minor": { + "type": "uint32", + "id": 10 + } + }, + "nested": { + "MoneroOutputEntry": { + "fields": { + "idx": { + "type": "uint64", + "id": 1 + }, + "key": { + "type": "MoneroRctKeyPublic", + "id": 2 + } + }, + "nested": { + "MoneroRctKeyPublic": { + "fields": { + "dest": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "commitment": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + } + } + }, + "MoneroMultisigKLRki": { + "fields": { + "K": { + "type": "bytes", + "id": 1 + }, + "L": { + "type": "bytes", + "id": 2 + }, + "R": { + "type": "bytes", + "id": 3 + }, + "ki": { + "type": "bytes", + "id": 4 + } + } + } + } + }, + "MoneroTransactionDestinationEntry": { + "fields": { + "amount": { + "type": "uint64", + "id": 1 + }, + "addr": { + "type": "MoneroAccountPublicAddress", + "id": 2 + }, + "is_subaddress": { + "type": "bool", + "id": 3 + }, + "original": { + "type": "bytes", + "id": 4 + }, + "is_integrated": { + "type": "bool", + "id": 5 + } + }, + "nested": { + "MoneroAccountPublicAddress": { + "fields": { + "spend_public_key": { + "type": "bytes", + "id": 1 + }, + "view_public_key": { + "type": "bytes", + "id": 2 + } + } + } + } + }, + "MoneroTransactionRsigData": { + "fields": { + "rsig_type": { + "type": "uint32", + "id": 1 + }, + "offload_type": { + "type": "uint32", + "id": 2 + }, + "grouping": { + "rule": "repeated", + "type": "uint64", + "id": 3, + "options": { + "packed": false + } + }, + "mask": { + "type": "bytes", + "id": 4 + }, + "rsig": { + "type": "bytes", + "id": 5 + }, + "rsig_parts": { + "rule": "repeated", + "type": "bytes", + "id": 6 + }, + "bp_version": { + "type": "uint32", + "id": 7 + } + } + }, + "MoneroGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + }, + "network_type": { + "type": "MoneroNetworkType", + "id": 3, + "options": { + "default": "MAINNET" + } + }, + "account": { + "type": "uint32", + "id": 4 + }, + "minor": { + "type": "uint32", + "id": 5 + }, + "payment_id": { + "type": "bytes", + "id": 6 + } + } + }, + "MoneroAddress": { + "fields": { + "address": { + "type": "bytes", + "id": 1 + } + } + }, + "MoneroGetWatchKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "network_type": { + "type": "MoneroNetworkType", + "id": 2, + "options": { + "default": "MAINNET" + } + } + } + }, + "MoneroWatchKey": { + "fields": { + "watch_key": { + "type": "bytes", + "id": 1 + }, + "address": { + "type": "bytes", + "id": 2 + } + } + }, + "MoneroTransactionInitRequest": { + "fields": { + "version": { + "type": "uint32", + "id": 1 + }, + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 2, + "options": { + "packed": false + } + }, + "network_type": { + "type": "MoneroNetworkType", + "id": 3, + "options": { + "default": "MAINNET" + } + }, + "tsx_data": { + "type": "MoneroTransactionData", + "id": 4 + } + }, + "nested": { + "MoneroTransactionData": { + "fields": { + "version": { + "type": "uint32", + "id": 1 + }, + "payment_id": { + "type": "bytes", + "id": 2 + }, + "unlock_time": { + "type": "uint64", + "id": 3 + }, + "outputs": { + "rule": "repeated", + "type": "MoneroTransactionDestinationEntry", + "id": 4 + }, + "change_dts": { + "type": "MoneroTransactionDestinationEntry", + "id": 5 + }, + "num_inputs": { + "type": "uint32", + "id": 6 + }, + "mixin": { + "type": "uint32", + "id": 7 + }, + "fee": { + "type": "uint64", + "id": 8 + }, + "account": { + "type": "uint32", + "id": 9 + }, + "minor_indices": { + "rule": "repeated", + "type": "uint32", + "id": 10, + "options": { + "packed": false + } + }, + "rsig_data": { + "type": "MoneroTransactionRsigData", + "id": 11 + }, + "integrated_indices": { + "rule": "repeated", + "type": "uint32", + "id": 12, + "options": { + "packed": false + } + }, + "client_version": { + "type": "uint32", + "id": 13 + }, + "hard_fork": { + "type": "uint32", + "id": 14 + }, + "monero_version": { + "type": "bytes", + "id": 15 + } + } + } + } + }, + "MoneroTransactionInitAck": { + "fields": { + "hmacs": { + "rule": "repeated", + "type": "bytes", + "id": 1 + }, + "rsig_data": { + "type": "MoneroTransactionRsigData", + "id": 2 + } + } + }, + "MoneroTransactionSetInputRequest": { + "fields": { + "src_entr": { + "type": "MoneroTransactionSourceEntry", + "id": 1 + } + } + }, + "MoneroTransactionSetInputAck": { + "fields": { + "vini": { + "type": "bytes", + "id": 1 + }, + "vini_hmac": { + "type": "bytes", + "id": 2 + }, + "pseudo_out": { + "type": "bytes", + "id": 3 + }, + "pseudo_out_hmac": { + "type": "bytes", + "id": 4 + }, + "pseudo_out_alpha": { + "type": "bytes", + "id": 5 + }, + "spend_key": { + "type": "bytes", + "id": 6 + } + } + }, + "MoneroTransactionInputViniRequest": { + "fields": { + "src_entr": { + "type": "MoneroTransactionSourceEntry", + "id": 1 + }, + "vini": { + "type": "bytes", + "id": 2 + }, + "vini_hmac": { + "type": "bytes", + "id": 3 + }, + "pseudo_out": { + "type": "bytes", + "id": 4 + }, + "pseudo_out_hmac": { + "type": "bytes", + "id": 5 + }, + "orig_idx": { + "type": "uint32", + "id": 6 + } + } + }, + "MoneroTransactionInputViniAck": { + "fields": {} + }, + "MoneroTransactionAllInputsSetRequest": { + "fields": {} + }, + "MoneroTransactionAllInputsSetAck": { + "fields": { + "rsig_data": { + "type": "MoneroTransactionRsigData", + "id": 1 + } + } + }, + "MoneroTransactionSetOutputRequest": { + "fields": { + "dst_entr": { + "type": "MoneroTransactionDestinationEntry", + "id": 1 + }, + "dst_entr_hmac": { + "type": "bytes", + "id": 2 + }, + "rsig_data": { + "type": "MoneroTransactionRsigData", + "id": 3 + }, + "is_offloaded_bp": { + "type": "bool", + "id": 4 + } + } + }, + "MoneroTransactionSetOutputAck": { + "fields": { + "tx_out": { + "type": "bytes", + "id": 1 + }, + "vouti_hmac": { + "type": "bytes", + "id": 2 + }, + "rsig_data": { + "type": "MoneroTransactionRsigData", + "id": 3 + }, + "out_pk": { + "type": "bytes", + "id": 4 + }, + "ecdh_info": { + "type": "bytes", + "id": 5 + } + } + }, + "MoneroTransactionAllOutSetRequest": { + "fields": { + "rsig_data": { + "type": "MoneroTransactionRsigData", + "id": 1 + } + } + }, + "MoneroTransactionAllOutSetAck": { + "fields": { + "extra": { + "type": "bytes", + "id": 1 + }, + "tx_prefix_hash": { + "type": "bytes", + "id": 2 + }, + "rv": { + "type": "MoneroRingCtSig", + "id": 4 + }, + "full_message_hash": { + "type": "bytes", + "id": 5 + } + }, + "nested": { + "MoneroRingCtSig": { + "fields": { + "txn_fee": { + "type": "uint64", + "id": 1 + }, + "message": { + "type": "bytes", + "id": 2 + }, + "rv_type": { + "type": "uint32", + "id": 3 + } + } + } + } + }, + "MoneroTransactionSignInputRequest": { + "fields": { + "src_entr": { + "type": "MoneroTransactionSourceEntry", + "id": 1 + }, + "vini": { + "type": "bytes", + "id": 2 + }, + "vini_hmac": { + "type": "bytes", + "id": 3 + }, + "pseudo_out": { + "type": "bytes", + "id": 4 + }, + "pseudo_out_hmac": { + "type": "bytes", + "id": 5 + }, + "pseudo_out_alpha": { + "type": "bytes", + "id": 6 + }, + "spend_key": { + "type": "bytes", + "id": 7 + }, + "orig_idx": { + "type": "uint32", + "id": 8 + } + } + }, + "MoneroTransactionSignInputAck": { + "fields": { + "signature": { + "type": "bytes", + "id": 1 + }, + "pseudo_out": { + "type": "bytes", + "id": 2 + } + } + }, + "MoneroTransactionFinalRequest": { + "fields": {} + }, + "MoneroTransactionFinalAck": { + "fields": { + "cout_key": { + "type": "bytes", + "id": 1 + }, + "salt": { + "type": "bytes", + "id": 2 + }, + "rand_mult": { + "type": "bytes", + "id": 3 + }, + "tx_enc_keys": { + "type": "bytes", + "id": 4 + }, + "opening_key": { + "type": "bytes", + "id": 5 + } + } + }, + "MoneroKeyImageExportInitRequest": { + "fields": { + "num": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "hash": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 3, + "options": { + "packed": false + } + }, + "network_type": { + "type": "MoneroNetworkType", + "id": 4, + "options": { + "default": "MAINNET" + } + }, + "subs": { + "rule": "repeated", + "type": "MoneroSubAddressIndicesList", + "id": 5 + } + }, + "nested": { + "MoneroSubAddressIndicesList": { + "fields": { + "account": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "minor_indices": { + "rule": "repeated", + "type": "uint32", + "id": 2, + "options": { + "packed": false + } + } + } + } + } + }, + "MoneroKeyImageExportInitAck": { + "fields": {} + }, + "MoneroKeyImageSyncStepRequest": { + "fields": { + "tdis": { + "rule": "repeated", + "type": "MoneroTransferDetails", + "id": 1 + } + }, + "nested": { + "MoneroTransferDetails": { + "fields": { + "out_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "tx_pub_key": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "additional_tx_pub_keys": { + "rule": "repeated", + "type": "bytes", + "id": 3 + }, + "internal_output_index": { + "rule": "required", + "type": "uint64", + "id": 4 + }, + "sub_addr_major": { + "type": "uint32", + "id": 5 + }, + "sub_addr_minor": { + "type": "uint32", + "id": 6 + } + } + } + } + }, + "MoneroKeyImageSyncStepAck": { + "fields": { + "kis": { + "rule": "repeated", + "type": "MoneroExportedKeyImage", + "id": 1 + } + }, + "nested": { + "MoneroExportedKeyImage": { + "fields": { + "iv": { + "type": "bytes", + "id": 1 + }, + "blob": { + "type": "bytes", + "id": 3 + } + } + } + } + }, + "MoneroKeyImageSyncFinalRequest": { + "fields": {} + }, + "MoneroKeyImageSyncFinalAck": { + "fields": { + "enc_key": { + "type": "bytes", + "id": 1 + } + } + }, + "MoneroGetTxKeyRequest": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "network_type": { + "type": "MoneroNetworkType", + "id": 2, + "options": { + "default": "MAINNET" + } + }, + "salt1": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "salt2": { + "rule": "required", + "type": "bytes", + "id": 4 + }, + "tx_enc_keys": { + "rule": "required", + "type": "bytes", + "id": 5 + }, + "tx_prefix_hash": { + "rule": "required", + "type": "bytes", + "id": 6 + }, + "reason": { + "type": "uint32", + "id": 7 + }, + "view_public_key": { + "type": "bytes", + "id": 8 + } + } + }, + "MoneroGetTxKeyAck": { + "fields": { + "salt": { + "type": "bytes", + "id": 1 + }, + "tx_keys": { + "type": "bytes", + "id": 2 + }, + "tx_derivations": { + "type": "bytes", + "id": 3 + } + } + }, + "MoneroLiveRefreshStartRequest": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "network_type": { + "type": "MoneroNetworkType", + "id": 2, + "options": { + "default": "MAINNET" + } + } + } + }, + "MoneroLiveRefreshStartAck": { + "fields": {} + }, + "MoneroLiveRefreshStepRequest": { + "fields": { + "out_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "recv_deriv": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "real_out_idx": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "sub_addr_major": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "sub_addr_minor": { + "rule": "required", + "type": "uint32", + "id": 5 + } + } + }, + "MoneroLiveRefreshStepAck": { + "fields": { + "salt": { + "type": "bytes", + "id": 1 + }, + "key_image": { + "type": "bytes", + "id": 2 + } + } + }, + "MoneroLiveRefreshFinalRequest": { + "fields": {} + }, + "MoneroLiveRefreshFinalAck": { + "fields": {} + }, + "DebugMoneroDiagRequest": { + "fields": { + "ins": { + "type": "uint64", + "id": 1 + }, + "p1": { + "type": "uint64", + "id": 2 + }, + "p2": { + "type": "uint64", + "id": 3 + }, + "pd": { + "rule": "repeated", + "type": "uint64", + "id": 4, + "options": { + "packed": false + } + }, + "data1": { + "type": "bytes", + "id": 5 + }, + "data2": { + "type": "bytes", + "id": 6 + } + } + }, + "DebugMoneroDiagAck": { + "fields": { + "ins": { + "type": "uint64", + "id": 1 + }, + "p1": { + "type": "uint64", + "id": 2 + }, + "p2": { + "type": "uint64", + "id": 3 + }, + "pd": { + "rule": "repeated", + "type": "uint64", + "id": 4, + "options": { + "packed": false + } + }, + "data1": { + "type": "bytes", + "id": 5 + }, + "data2": { + "type": "bytes", + "id": 6 + } + } + }, + "NearGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "NearAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + } + } + }, + "NearSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "NearSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "NEMGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "network": { + "type": "uint32", + "id": 2, + "options": { + "default": 104 + } + }, + "show_display": { + "type": "bool", + "id": 3 + } + } + }, + "NEMAddress": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "NEMSignTx": { + "fields": { + "transaction": { + "rule": "required", + "type": "NEMTransactionCommon", + "id": 1 + }, + "multisig": { + "type": "NEMTransactionCommon", + "id": 2 + }, + "transfer": { + "type": "NEMTransfer", + "id": 3 + }, + "cosigning": { + "type": "bool", + "id": 4 + }, + "provision_namespace": { + "type": "NEMProvisionNamespace", + "id": 5 + }, + "mosaic_creation": { + "type": "NEMMosaicCreation", + "id": 6 + }, + "supply_change": { + "type": "NEMMosaicSupplyChange", + "id": 7 + }, + "aggregate_modification": { + "type": "NEMAggregateModification", + "id": 8 + }, + "importance_transfer": { + "type": "NEMImportanceTransfer", + "id": 9 + } + }, + "nested": { + "NEMTransactionCommon": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "network": { + "type": "uint32", + "id": 2, + "options": { + "default": 104 + } + }, + "timestamp": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "fee": { + "rule": "required", + "type": "uint64", + "id": 4 + }, + "deadline": { + "rule": "required", + "type": "uint32", + "id": 5 + }, + "signer": { + "type": "bytes", + "id": 6 + } + } + }, + "NEMTransfer": { + "fields": { + "recipient": { + "rule": "required", + "type": "string", + "id": 1 + }, + "amount": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "payload": { + "type": "bytes", + "id": 3, + "options": { + "default": "" + } + }, + "public_key": { + "type": "bytes", + "id": 4 + }, + "mosaics": { + "rule": "repeated", + "type": "NEMMosaic", + "id": 5 + } + }, + "nested": { + "NEMMosaic": { + "fields": { + "namespace": { + "rule": "required", + "type": "string", + "id": 1 + }, + "mosaic": { + "rule": "required", + "type": "string", + "id": 2 + }, + "quantity": { + "rule": "required", + "type": "uint64", + "id": 3 + } + } + } + } + }, + "NEMProvisionNamespace": { + "fields": { + "namespace": { + "rule": "required", + "type": "string", + "id": 1 + }, + "parent": { + "type": "string", + "id": 2 + }, + "sink": { + "rule": "required", + "type": "string", + "id": 3 + }, + "fee": { + "rule": "required", + "type": "uint64", + "id": 4 + } + } + }, + "NEMMosaicCreation": { + "fields": { + "definition": { + "rule": "required", + "type": "NEMMosaicDefinition", + "id": 1 + }, + "sink": { + "rule": "required", + "type": "string", + "id": 2 + }, + "fee": { + "rule": "required", + "type": "uint64", + "id": 3 + } + }, + "nested": { + "NEMMosaicDefinition": { + "fields": { + "name": { + "type": "string", + "id": 1 + }, + "ticker": { + "type": "string", + "id": 2 + }, + "namespace": { + "rule": "required", + "type": "string", + "id": 3 + }, + "mosaic": { + "rule": "required", + "type": "string", + "id": 4 + }, + "divisibility": { + "type": "uint32", + "id": 5 + }, + "levy": { + "type": "NEMMosaicLevy", + "id": 6 + }, + "fee": { + "type": "uint64", + "id": 7 + }, + "levy_address": { + "type": "string", + "id": 8 + }, + "levy_namespace": { + "type": "string", + "id": 9 + }, + "levy_mosaic": { + "type": "string", + "id": 10 + }, + "supply": { + "type": "uint64", + "id": 11 + }, + "mutable_supply": { + "type": "bool", + "id": 12 + }, + "transferable": { + "type": "bool", + "id": 13 + }, + "description": { + "rule": "required", + "type": "string", + "id": 14 + }, + "networks": { + "rule": "repeated", + "type": "uint32", + "id": 15, + "options": { + "packed": false + } + } + }, + "nested": { + "NEMMosaicLevy": { + "values": { + "MosaicLevy_Absolute": 1, + "MosaicLevy_Percentile": 2 + } + } + } + } + } + }, + "NEMMosaicSupplyChange": { + "fields": { + "namespace": { + "rule": "required", + "type": "string", + "id": 1 + }, + "mosaic": { + "rule": "required", + "type": "string", + "id": 2 + }, + "type": { + "rule": "required", + "type": "NEMSupplyChangeType", + "id": 3 + }, + "delta": { + "rule": "required", + "type": "uint64", + "id": 4 + } + }, + "nested": { + "NEMSupplyChangeType": { + "values": { + "SupplyChange_Increase": 1, + "SupplyChange_Decrease": 2 + } + } + } + }, + "NEMAggregateModification": { + "fields": { + "modifications": { + "rule": "repeated", + "type": "NEMCosignatoryModification", + "id": 1 + }, + "relative_change": { + "type": "sint32", + "id": 2 + } + }, + "nested": { + "NEMCosignatoryModification": { + "fields": { + "type": { + "rule": "required", + "type": "NEMModificationType", + "id": 1 + }, + "public_key": { + "rule": "required", + "type": "bytes", + "id": 2 + } + }, + "nested": { + "NEMModificationType": { + "values": { + "CosignatoryModification_Add": 1, + "CosignatoryModification_Delete": 2 + } + } + } + } + } + }, + "NEMImportanceTransfer": { + "fields": { + "mode": { + "rule": "required", + "type": "NEMImportanceTransferMode", + "id": 1 + }, + "public_key": { + "rule": "required", + "type": "bytes", + "id": 2 + } + }, + "nested": { + "NEMImportanceTransferMode": { + "values": { + "ImportanceTransfer_Activate": 1, + "ImportanceTransfer_Deactivate": 2 + } + } + } + } + } + }, + "NEMSignedTx": { + "fields": { + "data": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "NEMDecryptMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "network": { + "type": "uint32", + "id": 2 + }, + "public_key": { + "type": "bytes", + "id": 3 + }, + "payload": { + "type": "bytes", + "id": 4 + } + } + }, + "NEMDecryptedMessage": { + "fields": { + "payload": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "NeoGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "NeoAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + }, + "public_key": { + "type": "bytes", + "id": 2 + } + } + }, + "NeoSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "network_magic": { + "type": "uint32", + "id": 3, + "options": { + "default": 860833102 + } + } + } + }, + "NeoSignedTx": { + "fields": { + "public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "NervosGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "network": { + "rule": "required", + "type": "string", + "id": 2 + }, + "show_display": { + "type": "bool", + "id": 3 + } + } + }, + "NervosAddress": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "NervosSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "data_initial_chunk": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "witness_buffer": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "network": { + "rule": "required", + "type": "string", + "id": 4 + }, + "data_length": { + "type": "uint32", + "id": 5 + } + } + }, + "NervosSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "address": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "NervosTxRequest": { + "fields": { + "data_length": { + "type": "uint32", + "id": 1 + }, + "public_key": { + "type": "bytes", + "id": 2 + }, + "signature": { + "type": "bytes", + "id": 3 + } + } + }, + "NervosTxAck": { + "fields": { + "data_chunk": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "NexaGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + }, + "prefix": { + "type": "string", + "id": 3, + "options": { + "default": "nexa" + } + } + } + }, + "NexaAddress": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + }, + "public_key": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "NexaSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_message": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "prefix": { + "type": "string", + "id": 3, + "options": { + "default": "nexa" + } + }, + "input_count": { + "type": "uint32", + "id": 4, + "options": { + "default": 1 + } + } + } + }, + "NexaTxInputRequest": { + "fields": { + "request_index": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "signature": { + "type": "bytes", + "id": 2 + } + } + }, + "NexaTxInputAck": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_message": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "NexaSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "NostrGetPublicKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "NostrPublicKey": { + "fields": { + "publickey": { + "type": "string", + "id": 1 + }, + "npub": { + "type": "string", + "id": 2 + } + } + }, + "NostrSignEvent": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "event": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "NostrSignedEvent": { + "fields": { + "event": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "NostrSignSchnorr": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "hash": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "NostrSignedSchnorr": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "NostrEncryptMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "pubkey": { + "rule": "required", + "type": "string", + "id": 2 + }, + "msg": { + "rule": "required", + "type": "string", + "id": 3 + }, + "show_display": { + "type": "bool", + "id": 4 + } + } + }, + "NostrEncryptedMessage": { + "fields": { + "msg": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "NostrDecryptMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "pubkey": { + "rule": "required", + "type": "string", + "id": 2 + }, + "msg": { + "rule": "required", + "type": "string", + "id": 3 + }, + "show_display": { + "type": "bool", + "id": 4 + } + } + }, + "NostrDecryptedMessage": { + "fields": { + "msg": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "PassphraseRequest": { + "fields": { + "_on_device": { + "type": "bool", + "id": 1, + "options": { + "deprecated": true + } + }, + "exists_attach_pin_user": { + "type": "bool", + "id": 8000 + } + } + }, + "PassphraseAck": { + "fields": { + "passphrase": { + "type": "string", + "id": 1 + }, + "_state": { + "type": "bytes", + "id": 2, + "options": { + "deprecated": true + } + }, + "on_device": { + "type": "bool", + "id": 3 + }, + "on_device_attach_pin": { + "type": "bool", + "id": 8000 + } + } + }, + "PolkadotGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "prefix": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "network": { + "rule": "required", + "type": "string", + "id": 3 + }, + "show_display": { + "type": "bool", + "id": 4 + } + } + }, + "PolkadotAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + }, + "public_key": { + "type": "string", + "id": 2 + } + } + }, + "PolkadotSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "network": { + "rule": "required", + "type": "string", + "id": 3 + } + } + }, + "PolkadotSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "RippleGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "RippleAddress": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "RippleSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "fee": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "flags": { + "type": "uint32", + "id": 3, + "options": { + "default": 0 + } + }, + "sequence": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "last_ledger_sequence": { + "type": "uint32", + "id": 5 + }, + "payment": { + "rule": "required", + "type": "RipplePayment", + "id": 6 + } + }, + "nested": { + "RipplePayment": { + "fields": { + "amount": { + "rule": "required", + "type": "uint64", + "id": 1 + }, + "destination": { + "rule": "required", + "type": "string", + "id": 2 + }, + "destination_tag": { + "type": "uint32", + "id": 3 + } + } + } + } + }, + "RippleSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "serialized_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "SolanaOffChainMessageVersion": { + "values": { + "MESSAGE_VERSION_0": 0 + } + }, + "SolanaOffChainMessageFormat": { + "values": { + "V0_RESTRICTED_ASCII": 0, + "V0_LIMITED_UTF8": 1 + } + }, + "SolanaGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "SolanaAddress": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "SolanaSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "SolanaSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "SolanaSignOffChainMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "message_version": { + "type": "SolanaOffChainMessageVersion", + "id": 3, + "options": { + "default": "MESSAGE_VERSION_0" + } + }, + "message_format": { + "type": "SolanaOffChainMessageFormat", + "id": 4, + "options": { + "default": "V0_RESTRICTED_ASCII" + } + }, + "application_domain": { + "type": "bytes", + "id": 5 + } + } + }, + "SolanaSignUnsafeMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "SolanaMessageSignature": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "public_key": { + "type": "bytes", + "id": 2 + } + } + }, + "StarcoinGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "StarcoinAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + } + } + }, + "StarcoinGetPublicKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "StarcoinPublicKey": { + "fields": { + "public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "StarcoinSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "StarcoinSignedTx": { + "fields": { + "public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "StarcoinSignMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "type": "bytes", + "id": 2 + } + } + }, + "StarcoinMessageSignature": { + "fields": { + "public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "StarcoinVerifyMessage": { + "fields": { + "public_key": { + "type": "bytes", + "id": 1 + }, + "signature": { + "type": "bytes", + "id": 2 + }, + "message": { + "type": "bytes", + "id": 3 + } + } + }, + "StellarAssetType": { + "values": { + "NATIVE": 0, + "ALPHANUM4": 1, + "ALPHANUM12": 2 + } + }, + "StellarAsset": { + "fields": { + "type": { + "rule": "required", + "type": "StellarAssetType", + "id": 1 + }, + "code": { + "type": "string", + "id": 2 + }, + "issuer": { + "type": "string", + "id": 3 + } + } + }, + "StellarGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "StellarAddress": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "StellarSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 2, + "options": { + "packed": false + } + }, + "network_passphrase": { + "rule": "required", + "type": "string", + "id": 3 + }, + "source_account": { + "rule": "required", + "type": "string", + "id": 4 + }, + "fee": { + "rule": "required", + "type": "uint32", + "id": 5 + }, + "sequence_number": { + "rule": "required", + "type": "uint64", + "id": 6 + }, + "timebounds_start": { + "rule": "required", + "type": "uint32", + "id": 8 + }, + "timebounds_end": { + "rule": "required", + "type": "uint32", + "id": 9 + }, + "memo_type": { + "rule": "required", + "type": "StellarMemoType", + "id": 10 + }, + "memo_text": { + "type": "string", + "id": 11 + }, + "memo_id": { + "type": "uint64", + "id": 12 + }, + "memo_hash": { + "type": "bytes", + "id": 13 + }, + "num_operations": { + "rule": "required", + "type": "uint32", + "id": 14 + } + }, + "nested": { + "StellarMemoType": { + "values": { + "NONE": 0, + "TEXT": 1, + "ID": 2, + "HASH": 3, + "RETURN": 4 + } + } + } + }, + "StellarTxOpRequest": { + "fields": {} + }, + "StellarPaymentOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "destination_account": { + "rule": "required", + "type": "string", + "id": 2 + }, + "asset": { + "rule": "required", + "type": "StellarAsset", + "id": 3 + }, + "amount": { + "rule": "required", + "type": "sint64", + "id": 4 + } + } + }, + "StellarCreateAccountOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "new_account": { + "rule": "required", + "type": "string", + "id": 2 + }, + "starting_balance": { + "rule": "required", + "type": "sint64", + "id": 3 + } + } + }, + "StellarPathPaymentStrictReceiveOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "send_asset": { + "rule": "required", + "type": "StellarAsset", + "id": 2 + }, + "send_max": { + "rule": "required", + "type": "sint64", + "id": 3 + }, + "destination_account": { + "rule": "required", + "type": "string", + "id": 4 + }, + "destination_asset": { + "rule": "required", + "type": "StellarAsset", + "id": 5 + }, + "destination_amount": { + "rule": "required", + "type": "sint64", + "id": 6 + }, + "paths": { + "rule": "repeated", + "type": "StellarAsset", + "id": 7 + } + } + }, + "StellarPathPaymentStrictSendOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "send_asset": { + "rule": "required", + "type": "StellarAsset", + "id": 2 + }, + "send_amount": { + "rule": "required", + "type": "sint64", + "id": 3 + }, + "destination_account": { + "rule": "required", + "type": "string", + "id": 4 + }, + "destination_asset": { + "rule": "required", + "type": "StellarAsset", + "id": 5 + }, + "destination_min": { + "rule": "required", + "type": "sint64", + "id": 6 + }, + "paths": { + "rule": "repeated", + "type": "StellarAsset", + "id": 7 + } + } + }, + "StellarManageSellOfferOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "selling_asset": { + "rule": "required", + "type": "StellarAsset", + "id": 2 + }, + "buying_asset": { + "rule": "required", + "type": "StellarAsset", + "id": 3 + }, + "amount": { + "rule": "required", + "type": "sint64", + "id": 4 + }, + "price_n": { + "rule": "required", + "type": "uint32", + "id": 5 + }, + "price_d": { + "rule": "required", + "type": "uint32", + "id": 6 + }, + "offer_id": { + "rule": "required", + "type": "uint64", + "id": 7 + } + } + }, + "StellarManageBuyOfferOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "selling_asset": { + "rule": "required", + "type": "StellarAsset", + "id": 2 + }, + "buying_asset": { + "rule": "required", + "type": "StellarAsset", + "id": 3 + }, + "amount": { + "rule": "required", + "type": "sint64", + "id": 4 + }, + "price_n": { + "rule": "required", + "type": "uint32", + "id": 5 + }, + "price_d": { + "rule": "required", + "type": "uint32", + "id": 6 + }, + "offer_id": { + "rule": "required", + "type": "uint64", + "id": 7 + } + } + }, + "StellarCreatePassiveSellOfferOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "selling_asset": { + "rule": "required", + "type": "StellarAsset", + "id": 2 + }, + "buying_asset": { + "rule": "required", + "type": "StellarAsset", + "id": 3 + }, + "amount": { + "rule": "required", + "type": "sint64", + "id": 4 + }, + "price_n": { + "rule": "required", + "type": "uint32", + "id": 5 + }, + "price_d": { + "rule": "required", + "type": "uint32", + "id": 6 + } + } + }, + "StellarSetOptionsOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "inflation_destination_account": { + "type": "string", + "id": 2 + }, + "clear_flags": { + "type": "uint32", + "id": 3 + }, + "set_flags": { + "type": "uint32", + "id": 4 + }, + "master_weight": { + "type": "uint32", + "id": 5 + }, + "low_threshold": { + "type": "uint32", + "id": 6 + }, + "medium_threshold": { + "type": "uint32", + "id": 7 + }, + "high_threshold": { + "type": "uint32", + "id": 8 + }, + "home_domain": { + "type": "string", + "id": 9 + }, + "signer_type": { + "type": "StellarSignerType", + "id": 10 + }, + "signer_key": { + "type": "bytes", + "id": 11 + }, + "signer_weight": { + "type": "uint32", + "id": 12 + } + }, + "nested": { + "StellarSignerType": { + "values": { + "ACCOUNT": 0, + "PRE_AUTH": 1, + "HASH": 2 + } + } + } + }, + "StellarChangeTrustOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "asset": { + "rule": "required", + "type": "StellarAsset", + "id": 2 + }, + "limit": { + "rule": "required", + "type": "uint64", + "id": 3 + } + } + }, + "StellarAllowTrustOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "trusted_account": { + "rule": "required", + "type": "string", + "id": 2 + }, + "asset_type": { + "rule": "required", + "type": "StellarAssetType", + "id": 3 + }, + "asset_code": { + "type": "string", + "id": 4 + }, + "is_authorized": { + "rule": "required", + "type": "bool", + "id": 5 + } + } + }, + "StellarAccountMergeOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "destination_account": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "StellarManageDataOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "key": { + "rule": "required", + "type": "string", + "id": 2 + }, + "value": { + "type": "bytes", + "id": 3 + } + } + }, + "StellarBumpSequenceOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "bump_to": { + "rule": "required", + "type": "uint64", + "id": 2 + } + } + }, + "StellarSignedTx": { + "fields": { + "public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "SuiGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "SuiAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + } + } + }, + "SuiSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "raw_tx": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "data_initial_chunk": { + "type": "bytes", + "id": 3, + "options": { + "default": "" + } + }, + "data_length": { + "type": "uint32", + "id": 4 + } + } + }, + "SuiSignedTx": { + "fields": { + "public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "SuiTxRequest": { + "fields": { + "data_length": { + "type": "uint32", + "id": 1 + }, + "public_key": { + "type": "bytes", + "id": 2 + }, + "signature": { + "type": "bytes", + "id": 3 + } + } + }, + "SuiTxAck": { + "fields": { + "data_chunk": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "SuiSignMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "SuiMessageSignature": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "address": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "TezosGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "TezosAddress": { + "fields": { + "address": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "TezosGetPublicKey": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "TezosPublicKey": { + "fields": { + "public_key": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "TezosSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "branch": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "reveal": { + "type": "TezosRevealOp", + "id": 3 + }, + "transaction": { + "type": "TezosTransactionOp", + "id": 4 + }, + "origination": { + "type": "TezosOriginationOp", + "id": 5 + }, + "delegation": { + "type": "TezosDelegationOp", + "id": 6 + }, + "proposal": { + "type": "TezosProposalOp", + "id": 7 + }, + "ballot": { + "type": "TezosBallotOp", + "id": 8 + } + }, + "nested": { + "TezosContractID": { + "fields": { + "tag": { + "rule": "required", + "type": "TezosContractType", + "id": 1 + }, + "hash": { + "rule": "required", + "type": "bytes", + "id": 2 + } + }, + "nested": { + "TezosContractType": { + "values": { + "Implicit": 0, + "Originated": 1 + } + } + } + }, + "TezosRevealOp": { + "fields": { + "source": { + "rule": "required", + "type": "bytes", + "id": 7 + }, + "fee": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "counter": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "gas_limit": { + "rule": "required", + "type": "uint64", + "id": 4 + }, + "storage_limit": { + "rule": "required", + "type": "uint64", + "id": 5 + }, + "public_key": { + "rule": "required", + "type": "bytes", + "id": 6 + } + } + }, + "TezosTransactionOp": { + "fields": { + "source": { + "rule": "required", + "type": "bytes", + "id": 9 + }, + "fee": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "counter": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "gas_limit": { + "rule": "required", + "type": "uint64", + "id": 4 + }, + "storage_limit": { + "rule": "required", + "type": "uint64", + "id": 5 + }, + "amount": { + "rule": "required", + "type": "uint64", + "id": 6 + }, + "destination": { + "rule": "required", + "type": "TezosContractID", + "id": 7 + }, + "parameters": { + "type": "bytes", + "id": 8 + }, + "parameters_manager": { + "type": "TezosParametersManager", + "id": 10 + } + }, + "nested": { + "TezosParametersManager": { + "fields": { + "set_delegate": { + "type": "bytes", + "id": 1 + }, + "cancel_delegate": { + "type": "bool", + "id": 2 + }, + "transfer": { + "type": "TezosManagerTransfer", + "id": 3 + } + }, + "nested": { + "TezosManagerTransfer": { + "fields": { + "destination": { + "rule": "required", + "type": "TezosContractID", + "id": 1 + }, + "amount": { + "rule": "required", + "type": "uint64", + "id": 2 + } + } + } + } + } + } + }, + "TezosOriginationOp": { + "fields": { + "source": { + "rule": "required", + "type": "bytes", + "id": 12 + }, + "fee": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "counter": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "gas_limit": { + "rule": "required", + "type": "uint64", + "id": 4 + }, + "storage_limit": { + "rule": "required", + "type": "uint64", + "id": 5 + }, + "manager_pubkey": { + "type": "bytes", + "id": 6 + }, + "balance": { + "rule": "required", + "type": "uint64", + "id": 7 + }, + "spendable": { + "type": "bool", + "id": 8 + }, + "delegatable": { + "type": "bool", + "id": 9 + }, + "delegate": { + "type": "bytes", + "id": 10 + }, + "script": { + "rule": "required", + "type": "bytes", + "id": 11 + } + } + }, + "TezosDelegationOp": { + "fields": { + "source": { + "rule": "required", + "type": "bytes", + "id": 7 + }, + "fee": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "counter": { + "rule": "required", + "type": "uint64", + "id": 3 + }, + "gas_limit": { + "rule": "required", + "type": "uint64", + "id": 4 + }, + "storage_limit": { + "rule": "required", + "type": "uint64", + "id": 5 + }, + "delegate": { + "rule": "required", + "type": "bytes", + "id": 6 + } + } + }, + "TezosProposalOp": { + "fields": { + "source": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "period": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "proposals": { + "rule": "repeated", + "type": "bytes", + "id": 4 + } + } + }, + "TezosBallotOp": { + "fields": { + "source": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "period": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "proposal": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "ballot": { + "rule": "required", + "type": "TezosBallotType", + "id": 4 + } + }, + "nested": { + "TezosBallotType": { + "values": { + "Yay": 0, + "Nay": 1, + "Pass": 2 + } + } + } + } + } + }, + "TezosSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "string", + "id": 1 + }, + "sig_op_contents": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "operation_hash": { + "rule": "required", + "type": "string", + "id": 3 + } + } + }, + "TonWalletVersion": { + "values": { + "V4R2": 3 + } + }, + "TonWorkChain": { + "values": { + "BASECHAIN": 0, + "MASTERCHAIN": 1 + } + }, + "TonGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + }, + "wallet_version": { + "type": "TonWalletVersion", + "id": 3, + "options": { + "default": "V4R2" + } + }, + "is_bounceable": { + "type": "bool", + "id": 4, + "options": { + "default": false + } + }, + "is_testnet_only": { + "type": "bool", + "id": 5, + "options": { + "default": false + } + }, + "workchain": { + "type": "TonWorkChain", + "id": 6, + "options": { + "default": "BASECHAIN" + } + }, + "wallet_id": { + "type": "uint32", + "id": 7, + "options": { + "default": 698983191 + } + } + } + }, + "TonAddress": { + "fields": { + "public_key": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "address": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "TonSignMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "destination": { + "rule": "required", + "type": "string", + "id": 2 + }, + "jetton_master_address": { + "type": "string", + "id": 3 + }, + "jetton_wallet_address": { + "type": "string", + "id": 4 + }, + "ton_amount": { + "rule": "required", + "type": "uint64", + "id": 5 + }, + "jetton_amount": { + "type": "uint64", + "id": 6 + }, + "fwd_fee": { + "type": "uint64", + "id": 7, + "options": { + "default": 0 + } + }, + "comment": { + "type": "string", + "id": 8 + }, + "is_raw_data": { + "type": "bool", + "id": 9, + "options": { + "default": false + } + }, + "mode": { + "type": "uint32", + "id": 10, + "options": { + "default": 3 + } + }, + "seqno": { + "rule": "required", + "type": "uint32", + "id": 11 + }, + "expire_at": { + "rule": "required", + "type": "uint64", + "id": 12 + }, + "wallet_version": { + "type": "TonWalletVersion", + "id": 13, + "options": { + "default": "V4R2" + } + }, + "wallet_id": { + "type": "uint32", + "id": 14, + "options": { + "default": 698983191 + } + }, + "workchain": { + "type": "TonWorkChain", + "id": 15, + "options": { + "default": "BASECHAIN" + } + }, + "is_bounceable": { + "type": "bool", + "id": 16, + "options": { + "default": false + } + }, + "is_testnet_only": { + "type": "bool", + "id": 17, + "options": { + "default": false + } + }, + "ext_destination": { + "rule": "repeated", + "type": "string", + "id": 18 + }, + "ext_ton_amount": { + "rule": "repeated", + "type": "uint64", + "id": 19, + "options": { + "packed": false + } + }, + "ext_payload": { + "rule": "repeated", + "type": "string", + "id": 20 + }, + "jetton_amount_bytes": { + "type": "bytes", + "id": 21 + }, + "init_data_initial_chunk": { + "type": "bytes", + "id": 22 + }, + "init_data_length": { + "type": "uint32", + "id": 23 + }, + "signing_message_repr": { + "type": "bytes", + "id": 24 + } + } + }, + "TonSignedMessage": { + "fields": { + "signature": { + "type": "bytes", + "id": 1 + }, + "signing_message": { + "type": "bytes", + "id": 2 + }, + "init_data_length": { + "type": "uint32", + "id": 3 + } + } + }, + "TonTxAck": { + "fields": { + "init_data_chunk": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "TonSignProof": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "appdomain": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "comment": { + "type": "bytes", + "id": 3 + }, + "expire_at": { + "rule": "required", + "type": "uint64", + "id": 4 + }, + "wallet_version": { + "type": "TonWalletVersion", + "id": 5, + "options": { + "default": "V4R2" + } + }, + "wallet_id": { + "type": "uint32", + "id": 6, + "options": { + "default": 698983191 + } + }, + "workchain": { + "type": "TonWorkChain", + "id": 7, + "options": { + "default": "BASECHAIN" + } + }, + "is_bounceable": { + "type": "bool", + "id": 8, + "options": { + "default": false + } + }, + "is_testnet_only": { + "type": "bool", + "id": 9, + "options": { + "default": false + } + } + } + }, + "TonSignedProof": { + "fields": { + "signature": { + "type": "bytes", + "id": 1 + } + } + }, + "TronGetAddress": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "show_display": { + "type": "bool", + "id": 2 + } + } + }, + "TronAddress": { + "fields": { + "address": { + "type": "string", + "id": 1 + } + } + }, + "TronSignTx": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "ref_block_bytes": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "ref_block_hash": { + "rule": "required", + "type": "bytes", + "id": 3 + }, + "expiration": { + "rule": "required", + "type": "uint64", + "id": 4 + }, + "data": { + "type": "bytes", + "id": 5 + }, + "contract": { + "rule": "required", + "type": "TronContract", + "id": 6 + }, + "timestamp": { + "rule": "required", + "type": "uint64", + "id": 7 + }, + "fee_limit": { + "type": "uint64", + "id": 8 + } + }, + "nested": { + "TronContract": { + "fields": { + "transfer_contract": { + "type": "TronTransferContract", + "id": 2 + }, + "vote_witness_contract": { + "type": "TronVoteWitnessContract", + "id": 4 + }, + "freeze_balance_contract": { + "type": "TronFreezeBalanceContract", + "id": 11 + }, + "unfreeze_balance_contract": { + "type": "TronUnfreezeBalanceContract", + "id": 12 + }, + "withdraw_balance_contract": { + "type": "TronWithdrawBalanceContract", + "id": 13 + }, + "trigger_smart_contract": { + "type": "TronTriggerSmartContract", + "id": 31 + }, + "freeze_balance_v2_contract": { + "type": "TronFreezeBalanceV2Contract", + "id": 54 + }, + "unfreeze_balance_v2_contract": { + "type": "TronUnfreezeBalanceV2Contract", + "id": 55 + }, + "withdraw_expire_unfreeze_contract": { + "type": "TronWithdrawExpireUnfreezeContract", + "id": 56 + }, + "delegate_resource_contract": { + "type": "TronDelegateResourceContract", + "id": 57 + }, + "undelegate_resource_contract": { + "type": "TronUnDelegateResourceContract", + "id": 58 + }, + "cancel_all_unfreeze_v2_contract": { + "type": "TronCancelAllUnfreezeV2Contract", + "id": 59 + }, + "provider": { + "type": "bytes", + "id": 3 + }, + "contract_name": { + "type": "bytes", + "id": 5 + }, + "permission_id": { + "type": "uint32", + "id": 6 + } + }, + "nested": { + "TronTransferContract": { + "fields": { + "to_address": { + "type": "string", + "id": 2 + }, + "amount": { + "type": "uint64", + "id": 3 + } + } + }, + "TronTriggerSmartContract": { + "fields": { + "contract_address": { + "type": "string", + "id": 2 + }, + "call_value": { + "type": "uint64", + "id": 3 + }, + "data": { + "type": "bytes", + "id": 4 + }, + "call_token_value": { + "type": "uint64", + "id": 5 + }, + "asset_id": { + "type": "uint64", + "id": 6 + } + } + }, + "TronResourceCode": { + "values": { + "BANDWIDTH": 0, + "ENERGY": 1, + "TRON_POWER": 2 + } + }, + "TronFreezeBalanceContract": { + "fields": { + "frozen_balance": { + "type": "uint64", + "id": 1 + }, + "frozen_duration": { + "type": "uint64", + "id": 2 + }, + "resource": { + "type": "TronResourceCode", + "id": 3 + }, + "receiver_address": { + "type": "string", + "id": 4 + } + } + }, + "TronUnfreezeBalanceContract": { + "fields": { + "resource": { + "type": "TronResourceCode", + "id": 1 + }, + "receiver_address": { + "type": "string", + "id": 2 + } + } + }, + "TronWithdrawBalanceContract": { + "fields": { + "owner_address": { + "type": "bytes", + "id": 1 + } + } + }, + "TronFreezeBalanceV2Contract": { + "fields": { + "frozen_balance": { + "type": "uint64", + "id": 2 + }, + "resource": { + "type": "TronResourceCode", + "id": 3 + } + } + }, + "TronUnfreezeBalanceV2Contract": { + "fields": { + "unfreeze_balance": { + "type": "uint64", + "id": 2 + }, + "resource": { + "type": "TronResourceCode", + "id": 3 + } + } + }, + "TronWithdrawExpireUnfreezeContract": { + "fields": {} + }, + "TronDelegateResourceContract": { + "fields": { + "resource": { + "type": "TronResourceCode", + "id": 2 + }, + "balance": { + "type": "uint64", + "id": 3 + }, + "receiver_address": { + "type": "string", + "id": 4 + }, + "lock": { + "type": "bool", + "id": 5 + }, + "lock_period": { + "type": "uint64", + "id": 6 + } + } + }, + "TronUnDelegateResourceContract": { + "fields": { + "resource": { + "type": "TronResourceCode", + "id": 2 + }, + "balance": { + "type": "uint64", + "id": 3 + }, + "receiver_address": { + "type": "string", + "id": 4 + } + } + }, + "TronCancelAllUnfreezeV2Contract": { + "fields": {} + }, + "TronVoteWitnessContract": { + "fields": { + "votes": { + "rule": "repeated", + "type": "Vote", + "id": 2 + }, + "support": { + "type": "bool", + "id": 3 + } + }, + "nested": { + "Vote": { + "fields": { + "vote_address": { + "rule": "required", + "type": "string", + "id": 1 + }, + "vote_count": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + } + } + } + } + } + } + }, + "TronSignedTx": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "serialized_tx": { + "type": "bytes", + "id": 2 + } + } + }, + "TronMessageType": { + "values": { + "V1": 1, + "V2": 2 + } + }, + "TronSignMessage": { + "fields": { + "address_n": { + "rule": "repeated", + "type": "uint32", + "id": 1, + "options": { + "packed": false + } + }, + "message": { + "rule": "required", + "type": "bytes", + "id": 2 + }, + "message_type": { + "type": "TronMessageType", + "id": 3, + "options": { + "default": "V1" + } + } + } + }, + "TronMessageSignature": { + "fields": { + "address": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "signature": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "ViewAmount": { + "fields": { + "is_unlimited": { + "rule": "required", + "type": "bool", + "id": 1 + }, + "num": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "ViewDetail": { + "fields": { + "key": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "value": { + "rule": "required", + "type": "string", + "id": 2 + }, + "is_overview": { + "rule": "required", + "type": "bool", + "id": 3 + }, + "has_icon": { + "rule": "required", + "type": "bool", + "id": 4 + } + } + }, + "ViewTipType": { + "values": { + "Default": 0, + "Highlight": 1, + "Recommend": 2, + "Warning": 3, + "Danger": 4 + } + }, + "ViewTip": { + "fields": { + "type": { + "rule": "required", + "type": "ViewTipType", + "id": 1 + }, + "text": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "ViewRawData": { + "fields": { + "initial_data": { + "rule": "required", + "type": "string", + "id": 1 + }, + "placeholder": { + "rule": "required", + "type": "uint32", + "id": 2 + } + } + }, + "ViewSignLayout": { + "values": { + "LayoutDefault": 0, + "LayoutSafeTxCreate": 1, + "LayoutFinalConfirm": 2, + "Layout7702": 3, + "LayoutFlat": 4 + } + }, + "ViewSignPage": { + "fields": { + "title": { + "rule": "required", + "type": "string", + "id": 1 + }, + "amount": { + "type": "ViewAmount", + "id": 2 + }, + "general": { + "rule": "repeated", + "type": "ViewDetail", + "id": 3 + }, + "tip": { + "type": "ViewTip", + "id": 4 + }, + "raw_data": { + "type": "ViewRawData", + "id": 5 + }, + "slide_to_confirm": { + "type": "bool", + "id": 6, + "options": { + "default": true + } + }, + "layout": { + "type": "ViewSignLayout", + "id": 7, + "options": { + "default": "LayoutDefault" + } + } + } + }, + "ViewVerifyPage": { + "fields": { + "title": { + "rule": "required", + "type": "string", + "id": 1 + }, + "address": { + "rule": "required", + "type": "string", + "id": 2 + }, + "path": { + "rule": "required", + "type": "string", + "id": 3 + } + } + }, + "WebAuthnListResidentCredentials": { + "fields": {} + }, + "WebAuthnAddResidentCredential": { + "fields": { + "credential_id": { + "type": "bytes", + "id": 1 + } + } + }, + "WebAuthnRemoveResidentCredential": { + "fields": { + "index": { + "type": "uint32", + "id": 1 + } + } + }, + "WebAuthnCredentials": { + "fields": { + "credentials": { + "rule": "repeated", + "type": "WebAuthnCredential", + "id": 1 + } + }, + "nested": { + "WebAuthnCredential": { + "fields": { + "index": { + "type": "uint32", + "id": 1 + }, + "id": { + "type": "bytes", + "id": 2 + }, + "rp_id": { + "type": "string", + "id": 3 + }, + "rp_name": { + "type": "string", + "id": 4 + }, + "user_id": { + "type": "bytes", + "id": 5 + }, + "user_name": { + "type": "string", + "id": 6 + }, + "user_display_name": { + "type": "string", + "id": 7 + }, + "creation_time": { + "type": "uint32", + "id": 8 + }, + "hmac_secret": { + "type": "bool", + "id": 9 + }, + "use_sign_count": { + "type": "bool", + "id": 10 + }, + "algorithm": { + "type": "sint32", + "id": 11 + }, + "curve": { + "type": "sint32", + "id": 12 + } + } + } + } + }, + "ProtocolInfoRequest": { + "fields": {} + }, + "ProtocolInfo": { + "fields": { + "version": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "supported_messages": { + "rule": "repeated", + "type": "uint32", + "id": 2, + "options": { + "packed": false + } + }, + "protobuf_definition": { + "type": "string", + "id": 3 + } + } + }, + "Ping": { + "fields": { + "message": { + "type": "string", + "id": 1, + "options": { + "default": "" + } + } + } + }, + "Success": { + "fields": { + "message": { + "type": "string", + "id": 1, + "options": { + "default": "" + } + } + } + }, + "Failure": { + "fields": { + "code": { + "rule": "required", + "type": "FailureType", + "id": 1 + }, + "subcode": { + "type": "uint32", + "id": 2 + }, + "message": { + "type": "string", + "id": 3 + } + }, + "nested": { + "FailureType": { + "values": { + "Failure_InvalidMessage": 1, + "Failure_UndefinedError": 2, + "Failure_UsageError": 3, + "Failure_DataError": 4, + "Failure_ProcessError": 5 + } + } + } + }, + "DeviceErrorCode": { + "values": { + "DeviceError_None": 0, + "DeviceError_Busy": 1, + "DeviceError_NotInitialized": 2, + "DeviceError_ActionCancelled": 3, + "DeviceError_PinAlreadyUsed": 4, + "DeviceError_PersistFailed": 5, + "DeviceError_SeError": 6, + "DeviceError_InvalidLanguage": 7, + "DeviceError_WallpaperNotUsable": 8, + "DeviceError_DeviceLocked": 9 + } + }, + "DeviceRebootType": { + "values": { + "Normal": 0, + "Romloader": 1, + "Bootloader": 2 + } + }, + "DeviceReboot": { + "fields": { + "reboot_type": { + "rule": "required", + "type": "DeviceRebootType", + "id": 1 + } + } + }, + "DeviceSettings": { + "fields": { + "label": { + "type": "string", + "id": 1 + }, + "bt_enable": { + "type": "bool", + "id": 2 + }, + "language": { + "type": "string", + "id": 3 + }, + "wallpaper_path": { + "type": "string", + "id": 4 + }, + "passphrase_enable": { + "type": "bool", + "id": 6 + }, + "brightness": { + "type": "uint32", + "id": 7 + }, + "autolock_delay_ms": { + "type": "uint32", + "id": 8 + }, + "autoshutdown_delay_ms": { + "type": "uint32", + "id": 9 + }, + "animation_enable": { + "type": "bool", + "id": 10 + }, + "tap_to_wake": { + "type": "bool", + "id": 11 + }, + "haptic_feedback": { + "type": "bool", + "id": 12 + }, + "device_name_display_enabled": { + "type": "bool", + "id": 13 + }, + "airgap_mode": { + "type": "bool", + "id": 14 + }, + "fido_enabled": { + "type": "bool", + "id": 15 + }, + "experimental_features": { + "type": "bool", + "id": 16 + }, + "usb_lock_enable": { + "type": "bool", + "id": 17 + }, + "random_keypad": { + "type": "bool", + "id": 18 + } + } + }, + "DeviceSettingsGet": { + "fields": {} + }, + "DeviceSettingsSet": { + "fields": { + "settings": { + "rule": "required", + "type": "DeviceSettings", + "id": 1 + } + } + }, + "DeviceSettingsPage": { + "values": { + "DeviceReset": 0, + "DevicePinChange": 1, + "DevicePassphrase": 2, + "DeviceAirgap": 3 + } + }, + "DeviceSettingsPageShow": { + "fields": { + "page": { + "rule": "required", + "type": "DeviceSettingsPage", + "id": 1 + }, + "field_name": { + "type": "string", + "id": 2 + } + } + }, + "DeviceCertificate": { + "fields": { + "cert_and_pubkey": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "private_key": { + "type": "bytes", + "id": 2 + } + } + }, + "DeviceCertificateWrite": { + "fields": { + "cert": { + "rule": "required", + "type": "DeviceCertificate", + "id": 1 + } + } + }, + "DeviceCertificateRead": { + "fields": {} + }, + "DeviceCertificateSignature": { + "fields": { + "data": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "DeviceCertificateSign": { + "fields": { + "data": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "DeviceFirmwareTargetType": { + "values": { + "FW_MGMT_TARGET_INVALID": 0, + "FW_MGMT_TARGET_CRATE": 1, + "FW_MGMT_TARGET_ROMLOADER": 2, + "FW_MGMT_TARGET_BOOTLOADER": 3, + "FW_MGMT_TARGET_APPLICATION_P1": 4, + "FW_MGMT_TARGET_APPLICATION_P2": 5, + "FW_MGMT_TARGET_COPROCESSOR": 6, + "FW_MGMT_TARGET_SE01": 7, + "FW_MGMT_TARGET_SE02": 8, + "FW_MGMT_TARGET_SE03": 9, + "FW_MGMT_TARGET_SE04": 10 + } + }, + "DeviceFirmwareUpdateTaskStatus": { + "values": { + "FW_MGMT_UPDATER_TASK_STATUS_PENDING": 0, + "FW_MGMT_UPDATER_TASK_STATUS_IN_PROGRESS": 1, + "FW_MGMT_UPDATER_TASK_STATUS_FINISHED": 2, + "FW_MGMT_UPDATER_TASK_STATUS_FAILED_FILE_NOT_FOUND": 3, + "FW_MGMT_UPDATER_TASK_STATUS_FAILED_FILE_READ": 4, + "FW_MGMT_UPDATER_TASK_STATUS_FAILED_FILE_WRITE": 5, + "FW_MGMT_UPDATER_TASK_STATUS_FAILED_VERIFY": 6, + "FW_MGMT_UPDATER_TASK_STATUS_FAILED_INSTALL": 7, + "FW_MGMT_UPDATER_TASK_STATUS_FAILED_ABORT": 8, + "FW_MGMT_UPDATER_TASK_STATUS_FAILED_BUSY": 9, + "FW_MGMT_UPDATER_TASK_STATUS_FAILED_ENTRY_OUT_OF_BOUNDS": 10 + } + }, + "DeviceFirmwareTarget": { + "fields": { + "target_id": { + "rule": "required", + "type": "DeviceFirmwareTargetType", + "id": 1 + }, + "path": { + "rule": "required", + "type": "string", + "id": 2 + } + } + }, + "DeviceFirmwareUpdateRequest": { + "fields": { + "targets": { + "rule": "repeated", + "type": "DeviceFirmwareTarget", + "id": 1 + } + } + }, + "DeviceFirmwareUpdateRecord": { + "fields": { + "target_id": { + "rule": "required", + "type": "DeviceFirmwareTargetType", + "id": 1 + }, + "status": { + "type": "DeviceFirmwareUpdateTaskStatus", + "id": 10 + }, + "payload_version": { + "type": "uint32", + "id": 20 + }, + "path": { + "type": "string", + "id": 30 + } + } + }, + "DeviceFirmwareUpdateRecordFields": { + "fields": { + "status": { + "type": "bool", + "id": 10 + }, + "payload_version": { + "type": "bool", + "id": 20 + }, + "path": { + "type": "bool", + "id": 30 + } + } + }, + "DeviceFirmwareUpdateStatusGet": { + "fields": { + "fields": { + "type": "DeviceFirmwareUpdateRecordFields", + "id": 1 + } + } + }, + "DeviceFirmwareUpdateStatus": { + "fields": { + "records": { + "rule": "repeated", + "type": "DeviceFirmwareUpdateRecord", + "id": 1 + } + } + }, + "DeviceFactoryAck": { + "values": { + "FACTORY_ACK_SUCCESS": 0, + "FACTORY_ACK_FAIL": 1 + } + }, + "DeviceFactoryInfoManufactureTime": { + "fields": { + "year": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "month": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "day": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "hour": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "minute": { + "rule": "required", + "type": "uint32", + "id": 5 + }, + "second": { + "rule": "required", + "type": "uint32", + "id": 6 + } + } + }, + "DeviceFactoryInfo": { + "fields": { + "version": { + "type": "uint32", + "id": 1 + }, + "serial_number": { + "type": "string", + "id": 2 + }, + "burn_in_completed": { + "type": "bool", + "id": 3 + }, + "factory_test_completed": { + "type": "bool", + "id": 4 + }, + "manufacture_time": { + "type": "DeviceFactoryInfoManufactureTime", + "id": 5 + } + } + }, + "DeviceFactoryInfoSet": { + "fields": { + "info": { + "rule": "required", + "type": "DeviceFactoryInfo", + "id": 1 + } + } + }, + "DeviceFactoryInfoGet": { + "fields": {} + }, + "DeviceFactoryPermanentLock": { + "fields": { + "check_a": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "check_b": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, + "DeviceFactoryTest": { + "fields": { + "burn_in_test": { + "rule": "required", + "type": "bool", + "id": 1 + } + } + }, + "DeviceType": { + "values": { + "CLASSIC1": 0, + "CLASSIC1S": 1, + "MINI": 2, + "TOUCH": 3, + "PRO": 5, + "CLASSIC1S_PURE": 6, + "PRO2": 7, + "NEO": 8 + } + }, + "DeviceSeType": { + "values": { + "THD89": 0, + "SE608A": 1 + } + }, + "DeviceSEState": { + "values": { + "BOOT": 0, + "APP_FACTORY": 51, + "APP": 85 + } + }, + "DeviceFirmwareImageInfo": { + "fields": { + "version": { + "type": "string", + "id": 10 + }, + "build_id": { + "type": "string", + "id": 20 + }, + "hash": { + "type": "bytes", + "id": 30 + } + } + }, + "DeviceHardwareInfo": { + "fields": { + "Device_type": { + "type": "DeviceType", + "id": 10 + }, + "serial_no": { + "type": "string", + "id": 20 + }, + "hardware_version": { + "type": "string", + "id": 100 + }, + "hardware_version_raw_adc": { + "type": "uint32", + "id": 110 + } + } + }, + "DeviceMainMcuInfo": { + "fields": { + "romloader": { + "type": "DeviceFirmwareImageInfo", + "id": 10 + }, + "bootloader": { + "type": "DeviceFirmwareImageInfo", + "id": 20 + }, + "application": { + "type": "DeviceFirmwareImageInfo", + "id": 30 + }, + "application_data": { + "type": "DeviceFirmwareImageInfo", + "id": 40 + } + } + }, + "DeviceCoprocessorInfo": { + "fields": { + "bootloader": { + "type": "DeviceFirmwareImageInfo", + "id": 20 + }, + "application": { + "type": "DeviceFirmwareImageInfo", + "id": 30 + }, + "bt_adv_name": { + "type": "string", + "id": 100 + }, + "bt_mac": { + "type": "bytes", + "id": 110 + } + } + }, + "DeviceSEInfo": { + "fields": { + "bootloader": { + "type": "DeviceFirmwareImageInfo", + "id": 20 + }, + "application": { + "type": "DeviceFirmwareImageInfo", + "id": 30 + }, + "type": { + "type": "DeviceSeType", + "id": 100 + }, + "state": { + "type": "DeviceSEState", + "id": 110 + } + } + }, + "DeviceInfoTargets": { + "fields": { + "hw": { + "type": "bool", + "id": 100 + }, + "fw": { + "type": "bool", + "id": 200 + }, + "coprocessor": { + "type": "bool", + "id": 300 + }, + "se1": { + "type": "bool", + "id": 400 + }, + "se2": { + "type": "bool", + "id": 410 + }, + "se3": { + "type": "bool", + "id": 420 + }, + "se4": { + "type": "bool", + "id": 430 + }, + "status": { + "type": "bool", + "id": 10000 + } + } + }, + "DeviceInfoTypes": { + "fields": { + "version": { + "type": "bool", + "id": 10 + }, + "build_id": { + "type": "bool", + "id": 20 + }, + "hash": { + "type": "bool", + "id": 30 + }, + "specific": { + "type": "bool", + "id": 40 + } + } + }, + "DeviceInfoGet": { + "fields": { + "targets": { + "type": "DeviceInfoTargets", + "id": 1 + }, + "types": { + "type": "DeviceInfoTypes", + "id": 2 + } + } + }, + "DeviceInfo": { + "fields": { + "protocol_version": { + "rule": "required", + "type": "uint32", + "id": 1 + }, + "hw": { + "type": "DeviceHardwareInfo", + "id": 100 + }, + "fw": { + "type": "DeviceMainMcuInfo", + "id": 200 + }, + "coprocessor": { + "type": "DeviceCoprocessorInfo", + "id": 300 + }, + "se1": { + "type": "DeviceSEInfo", + "id": 400 + }, + "se2": { + "type": "DeviceSEInfo", + "id": 410 + }, + "se3": { + "type": "DeviceSEInfo", + "id": 420 + }, + "se4": { + "type": "DeviceSEInfo", + "id": 430 + }, + "status": { + "type": "DeviceStatus", + "id": 10000 + } + } + }, + "DeviceSessionGet": { + "fields": { + "session_id": { + "type": "bytes", + "id": 1 + } + } + }, + "DeviceSession": { + "fields": { + "session_id": { + "type": "bytes", + "id": 1 + }, + "btc_test_address": { + "type": "string", + "id": 2 + } + } + }, + "DeviceSessionAskPin": { + "fields": {} + }, + "DeviceSessionAskPin_FailureSubCodes": { + "values": { + "UserCancel": 1 + } + }, + "DeviceStatus": { + "fields": { + "device_id": { + "type": "string", + "id": 1 + }, + "unlocked": { + "type": "bool", + "id": 2 + }, + "init_states": { + "type": "bool", + "id": 3 + }, + "backup_required": { + "type": "bool", + "id": 4 + }, + "passphrase_enabled": { + "type": "bool", + "id": 10 + }, + "attach_to_pin_enabled": { + "type": "bool", + "id": 11 + }, + "unlocked_by_attach_to_pin": { + "type": "bool", + "id": 12 + } + } + }, + "DeviceStatusGet": { + "fields": {} + }, + "DevOnboardingStage": { + "values": { + "DEV_ONBOARDING_STAGE_UNKNOWN": 0, + "DEV_ONBOARDING_STAGE_SAFETY_CHECK": 1, + "DEV_ONBOARDING_STAGE_PERSONALIZATION": 2, + "DEV_ONBOARDING_STAGE_SELECT_SETUP_METHOD": 3, + "DEV_ONBOARDING_STAGE_NEW_DEVICE": 4, + "DEV_ONBOARDING_STAGE_SELECT_RESTORE_METHOD": 5, + "DEV_ONBOARDING_STAGE_RESTORE_MNEMONIC": 6, + "DEV_ONBOARDING_STAGE_RESTORE_SEEDCARD": 7, + "DEV_ONBOARDING_STAGE_WALLET_READY": 8, + "DEV_ONBOARDING_STAGE_SEEDCARD_BACKUP_PROMPT": 9, + "DEV_ONBOARDING_STAGE_SELECT_SEEDCARD_BACKUP_METHOD": 10, + "DEV_ONBOARDING_STAGE_SEEDCARD_BACKUP": 11, + "DEV_ONBOARDING_STAGE_DONE": 12 + } + }, + "DevGetOnboardingStatus": { + "fields": {} + }, + "DevOnboardingStatus": { + "fields": { + "stage": { + "rule": "required", + "type": "DevOnboardingStage", + "id": 1 + }, + "status_code": { + "type": "uint32", + "id": 2 + }, + "detail_code": { + "type": "uint32", + "id": 3 + } + } + }, + "FilesystemPermissionFix": { + "fields": {} + }, + "FilesystemPathInfo": { + "fields": { + "exist": { + "rule": "required", + "type": "bool", + "id": 1 + }, + "size": { + "rule": "required", + "type": "uint64", + "id": 2 + }, + "year": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "month": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "day": { + "rule": "required", + "type": "uint32", + "id": 5 + }, + "hour": { + "rule": "required", + "type": "uint32", + "id": 6 + }, + "minute": { + "rule": "required", + "type": "uint32", + "id": 7 + }, + "second": { + "rule": "required", + "type": "uint32", + "id": 8 + }, + "readonly": { + "rule": "required", + "type": "bool", + "id": 9 + }, + "hidden": { + "rule": "required", + "type": "bool", + "id": 10 + }, + "system": { + "rule": "required", + "type": "bool", + "id": 11 + }, + "archive": { + "rule": "required", + "type": "bool", + "id": 12 + }, + "directory": { + "rule": "required", + "type": "bool", + "id": 13 + } + } + }, + "FilesystemPathInfoQuery": { + "fields": { + "path": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "FilesystemFile": { + "fields": { + "path": { + "rule": "required", + "type": "string", + "id": 1 + }, + "offset": { + "rule": "required", + "type": "uint32", + "id": 2 + }, + "total_size": { + "rule": "required", + "type": "uint32", + "id": 3 + }, + "data": { + "type": "bytes", + "id": 4 + }, + "data_hash": { + "type": "uint32", + "id": 5 + }, + "processed_byte": { + "type": "uint32", + "id": 6 + } + } + }, + "FilesystemFileRead": { + "fields": { + "file": { + "rule": "required", + "type": "FilesystemFile", + "id": 1 + }, + "chunk_len": { + "type": "uint32", + "id": 2 + }, + "ui_percentage": { + "type": "uint32", + "id": 3 + } + } + }, + "FilesystemFileWrite": { + "fields": { + "file": { + "rule": "required", + "type": "FilesystemFile", + "id": 1 + }, + "overwrite": { + "rule": "required", + "type": "bool", + "id": 2 + }, + "append": { + "rule": "required", + "type": "bool", + "id": 3 + }, + "ui_percentage": { + "type": "uint32", + "id": 4 + } + } + }, + "FilesystemFileDelete": { + "fields": { + "path": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "FilesystemDir": { + "fields": { + "path": { + "rule": "required", + "type": "string", + "id": 1 + }, + "child_dirs": { + "type": "string", + "id": 2 + }, + "child_files": { + "type": "string", + "id": 3 + } + } + }, + "FilesystemDirList": { + "fields": { + "path": { + "rule": "required", + "type": "string", + "id": 1 + }, + "depth": { + "type": "uint32", + "id": 2, + "options": { + "default": 0 + } + } + } + }, + "FilesystemDirMake": { + "fields": { + "path": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "FilesystemDirRemove": { + "fields": { + "path": { + "rule": "required", + "type": "string", + "id": 1 + } + } + }, + "FilesystemFormat": { + "fields": { + "data": { + "rule": "required", + "type": "bool", + "id": 1 + }, + "user": { + "rule": "required", + "type": "bool", + "id": 2 + } + } + }, + "PortfolioUpdate": { + "fields": {} + }, + "google": { + "nested": { + "protobuf": { + "nested": { + "FileDescriptorSet": { + "fields": { + "file": { + "rule": "repeated", + "type": "FileDescriptorProto", + "id": 1 + } + } + }, + "FileDescriptorProto": { + "fields": { + "name": { + "type": "string", + "id": 1 + }, + "package": { + "type": "string", + "id": 2 + }, + "dependency": { + "rule": "repeated", + "type": "string", + "id": 3 + }, + "public_dependency": { + "rule": "repeated", + "type": "int32", + "id": 10, + "options": { + "packed": false + } + }, + "weak_dependency": { + "rule": "repeated", + "type": "int32", + "id": 11, + "options": { + "packed": false + } + }, + "message_type": { + "rule": "repeated", + "type": "DescriptorProto", + "id": 4 + }, + "enum_type": { + "rule": "repeated", + "type": "EnumDescriptorProto", + "id": 5 + }, + "service": { + "rule": "repeated", + "type": "ServiceDescriptorProto", + "id": 6 + }, + "extension": { + "rule": "repeated", + "type": "FieldDescriptorProto", + "id": 7 + }, + "options": { + "type": "FileOptions", + "id": 8 + }, + "source_code_info": { + "type": "SourceCodeInfo", + "id": 9 + }, + "syntax": { + "type": "string", + "id": 12 + } + } + }, + "DescriptorProto": { + "fields": { + "name": { + "type": "string", + "id": 1 + }, + "field": { + "rule": "repeated", + "type": "FieldDescriptorProto", + "id": 2 + }, + "extension": { + "rule": "repeated", + "type": "FieldDescriptorProto", + "id": 6 + }, + "nested_type": { + "rule": "repeated", + "type": "DescriptorProto", + "id": 3 + }, + "enum_type": { + "rule": "repeated", + "type": "EnumDescriptorProto", + "id": 4 + }, + "extension_range": { + "rule": "repeated", + "type": "ExtensionRange", + "id": 5 + }, + "oneof_decl": { + "rule": "repeated", + "type": "OneofDescriptorProto", + "id": 8 + }, + "options": { + "type": "MessageOptions", + "id": 7 + }, + "reserved_range": { + "rule": "repeated", + "type": "ReservedRange", + "id": 9 + }, + "reserved_name": { + "rule": "repeated", + "type": "string", + "id": 10 + } + }, + "nested": { + "ExtensionRange": { + "fields": { + "start": { + "type": "int32", + "id": 1 + }, + "end": { + "type": "int32", + "id": 2 + } + } + }, + "ReservedRange": { + "fields": { + "start": { + "type": "int32", + "id": 1 + }, + "end": { + "type": "int32", + "id": 2 + } + } + } + } + }, + "FieldDescriptorProto": { + "fields": { + "name": { + "type": "string", + "id": 1 + }, + "number": { + "type": "int32", + "id": 3 + }, + "label": { + "type": "Label", + "id": 4 + }, + "type": { + "type": "Type", + "id": 5 + }, + "type_name": { + "type": "string", + "id": 6 + }, + "extendee": { + "type": "string", + "id": 2 + }, + "default_value": { + "type": "string", + "id": 7 + }, + "oneof_index": { + "type": "int32", + "id": 9 + }, + "json_name": { + "type": "string", + "id": 10 + }, + "options": { + "type": "FieldOptions", + "id": 8 + } + }, + "nested": { + "Type": { + "values": { + "TYPE_DOUBLE": 1, + "TYPE_FLOAT": 2, + "TYPE_INT64": 3, + "TYPE_UINT64": 4, + "TYPE_INT32": 5, + "TYPE_FIXED64": 6, + "TYPE_FIXED32": 7, + "TYPE_BOOL": 8, + "TYPE_STRING": 9, + "TYPE_GROUP": 10, + "TYPE_MESSAGE": 11, + "TYPE_BYTES": 12, + "TYPE_UINT32": 13, + "TYPE_ENUM": 14, + "TYPE_SFIXED32": 15, + "TYPE_SFIXED64": 16, + "TYPE_SINT32": 17, + "TYPE_SINT64": 18 + } + }, + "Label": { + "values": { + "LABEL_OPTIONAL": 1, + "LABEL_REQUIRED": 2, + "LABEL_REPEATED": 3 + } + } + } + }, + "OneofDescriptorProto": { + "fields": { + "name": { + "type": "string", + "id": 1 + }, + "options": { + "type": "OneofOptions", + "id": 2 + } + } + }, + "EnumDescriptorProto": { + "fields": { + "name": { + "type": "string", + "id": 1 + }, + "value": { + "rule": "repeated", + "type": "EnumValueDescriptorProto", + "id": 2 + }, + "options": { + "type": "EnumOptions", + "id": 3 + } + } + }, + "EnumValueDescriptorProto": { + "fields": { + "name": { + "type": "string", + "id": 1 + }, + "number": { + "type": "int32", + "id": 2 + }, + "options": { + "type": "EnumValueOptions", + "id": 3 + } + } + }, + "ServiceDescriptorProto": { + "fields": { + "name": { + "type": "string", + "id": 1 + }, + "method": { + "rule": "repeated", + "type": "MethodDescriptorProto", + "id": 2 + }, + "options": { + "type": "ServiceOptions", + "id": 3 + } + } + }, + "MethodDescriptorProto": { + "fields": { + "name": { + "type": "string", + "id": 1 + }, + "input_type": { + "type": "string", + "id": 2 + }, + "output_type": { + "type": "string", + "id": 3 + }, + "options": { + "type": "MethodOptions", + "id": 4 + }, + "client_streaming": { + "type": "bool", + "id": 5 + }, + "server_streaming": { + "type": "bool", + "id": 6 + } + } + }, + "FileOptions": { + "fields": { + "java_package": { + "type": "string", + "id": 1 + }, + "java_outer_classname": { + "type": "string", + "id": 8 + }, + "java_multiple_files": { + "type": "bool", + "id": 10 + }, + "java_generate_equals_and_hash": { + "type": "bool", + "id": 20, + "options": { + "deprecated": true + } + }, + "java_string_check_utf8": { + "type": "bool", + "id": 27 + }, + "optimize_for": { + "type": "OptimizeMode", + "id": 9, + "options": { + "default": "SPEED" + } + }, + "go_package": { + "type": "string", + "id": 11 + }, + "cc_generic_services": { + "type": "bool", + "id": 16 + }, + "java_generic_services": { + "type": "bool", + "id": 17 + }, + "py_generic_services": { + "type": "bool", + "id": 18 + }, + "deprecated": { + "type": "bool", + "id": 23 + }, + "cc_enable_arenas": { + "type": "bool", + "id": 31 + }, + "objc_class_prefix": { + "type": "string", + "id": 36 + }, + "csharp_namespace": { + "type": "string", + "id": 37 + }, + "uninterpreted_option": { + "rule": "repeated", + "type": "UninterpretedOption", + "id": 999 + } + }, + "extensions": [[1000, 536870911]], + "reserved": [[38, 38]], + "nested": { + "OptimizeMode": { + "values": { + "SPEED": 1, + "CODE_SIZE": 2, + "LITE_RUNTIME": 3 + } + } + } + }, + "MessageOptions": { + "fields": { + "message_set_wire_format": { + "type": "bool", + "id": 1 + }, + "no_standard_descriptor_accessor": { + "type": "bool", + "id": 2 + }, + "deprecated": { + "type": "bool", + "id": 3 + }, + "map_entry": { + "type": "bool", + "id": 7 + }, + "uninterpreted_option": { + "rule": "repeated", + "type": "UninterpretedOption", + "id": 999 + } + }, + "extensions": [[1000, 536870911]], + "reserved": [[8, 8]] + }, + "FieldOptions": { + "fields": { + "ctype": { + "type": "CType", + "id": 1, + "options": { + "default": "STRING" + } + }, + "packed": { + "type": "bool", + "id": 2 + }, + "jstype": { + "type": "JSType", + "id": 6, + "options": { + "default": "JS_NORMAL" + } + }, + "lazy": { + "type": "bool", + "id": 5 + }, + "deprecated": { + "type": "bool", + "id": 3 + }, + "weak": { + "type": "bool", + "id": 10 + }, + "uninterpreted_option": { + "rule": "repeated", + "type": "UninterpretedOption", + "id": 999 + } + }, + "extensions": [[1000, 536870911]], + "reserved": [[4, 4]], + "nested": { + "CType": { + "values": { + "STRING": 0, + "CORD": 1, + "STRING_PIECE": 2 + } + }, + "JSType": { + "values": { + "JS_NORMAL": 0, + "JS_STRING": 1, + "JS_NUMBER": 2 + } + } + } + }, + "OneofOptions": { + "fields": { + "uninterpreted_option": { + "rule": "repeated", + "type": "UninterpretedOption", + "id": 999 + } + }, + "extensions": [[1000, 536870911]] + }, + "EnumOptions": { + "fields": { + "allow_alias": { + "type": "bool", + "id": 2 + }, + "deprecated": { + "type": "bool", + "id": 3 + }, + "uninterpreted_option": { + "rule": "repeated", + "type": "UninterpretedOption", + "id": 999 + } + }, + "extensions": [[1000, 536870911]] + }, + "EnumValueOptions": { + "fields": { + "deprecated": { + "type": "bool", + "id": 1 + }, + "uninterpreted_option": { + "rule": "repeated", + "type": "UninterpretedOption", + "id": 999 + } + }, + "extensions": [[1000, 536870911]] + }, + "ServiceOptions": { + "fields": { + "deprecated": { + "type": "bool", + "id": 33 + }, + "uninterpreted_option": { + "rule": "repeated", + "type": "UninterpretedOption", + "id": 999 + } + }, + "extensions": [[1000, 536870911]] + }, + "MethodOptions": { + "fields": { + "deprecated": { + "type": "bool", + "id": 33 + }, + "uninterpreted_option": { + "rule": "repeated", + "type": "UninterpretedOption", + "id": 999 + } + }, + "extensions": [[1000, 536870911]] + }, + "UninterpretedOption": { + "fields": { + "name": { + "rule": "repeated", + "type": "NamePart", + "id": 2 + }, + "identifier_value": { + "type": "string", + "id": 3 + }, + "positive_int_value": { + "type": "uint64", + "id": 4 + }, + "negative_int_value": { + "type": "int64", + "id": 5 + }, + "double_value": { + "type": "double", + "id": 6 + }, + "string_value": { + "type": "bytes", + "id": 7 + }, + "aggregate_value": { + "type": "string", + "id": 8 + } + }, + "nested": { + "NamePart": { + "fields": { + "name_part": { + "rule": "required", + "type": "string", + "id": 1 + }, + "is_extension": { + "rule": "required", + "type": "bool", + "id": 2 + } + } + } + } + }, + "SourceCodeInfo": { + "fields": { + "location": { + "rule": "repeated", + "type": "Location", + "id": 1 + } + }, + "nested": { + "Location": { + "fields": { + "path": { + "rule": "repeated", + "type": "int32", + "id": 1 + }, + "span": { + "rule": "repeated", + "type": "int32", + "id": 2 + }, + "leading_comments": { + "type": "string", + "id": 3 + }, + "trailing_comments": { + "type": "string", + "id": 4 + }, + "leading_detached_comments": { + "rule": "repeated", + "type": "string", + "id": 6 + } + } + } + } + }, + "GeneratedCodeInfo": { + "fields": { + "annotation": { + "rule": "repeated", + "type": "Annotation", + "id": 1 + } + }, + "nested": { + "Annotation": { + "fields": { + "path": { + "rule": "repeated", + "type": "int32", + "id": 1 + }, + "source_file": { + "type": "string", + "id": 2 + }, + "begin": { + "type": "int32", + "id": 3 + }, + "end": { + "type": "int32", + "id": 4 + } + } + } + } + } + } + } + } + } + } +} diff --git a/packages/hd-transport/package.json b/packages/hd-transport/package.json index c39709e94..8921aceee 100644 --- a/packages/hd-transport/package.json +++ b/packages/hd-transport/package.json @@ -16,7 +16,7 @@ "test": "jest", "lint": "eslint .", "lint:fix": "eslint . --fix", - "update:protobuf": "./scripts/protobuf-build.sh" + "update-protobuf": "./scripts/protobuf-build.sh" }, "devDependencies": { "@types/bytebuffer": "^5.0.42", diff --git a/packages/hd-transport/scripts/protobuf-build.sh b/packages/hd-transport/scripts/protobuf-build.sh index 853f91d2d..b8f36da37 100755 --- a/packages/hd-transport/scripts/protobuf-build.sh +++ b/packages/hd-transport/scripts/protobuf-build.sh @@ -5,10 +5,14 @@ set -euxo pipefail echo $# PARENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) +PACKAGE_ROOT="$PARENT_PATH/.." SRC="../../submodules/firmware/common/protob" DIST="." LANG="typescript" +# Absolute paths — resolved relative to this script's directory +REPO_ROOT="$PARENT_PATH/../../.." +CORE_MESSAGES_DIR="$REPO_ROOT/packages/core/src/data/messages" if [[ $# -ne 0 && $# -ne 3 ]] then @@ -29,30 +33,144 @@ if [[ "$LANG" != "typescript" && "$LANG" != "flow" ]]; exit 1 fi +if [[ "$SRC" = /* ]]; then + SRC_PATH="$SRC" +else + SRC_PATH="$PACKAGE_ROOT/$SRC" +fi + +if [[ "$DIST" = /* ]]; then + DIST_PATH="$DIST" +else + DIST_PATH="$PACKAGE_ROOT/$DIST" +fi + +# Remove temp proto files on any exit, including failure paths +trap 'rm -f "$DIST_PATH/messages-tmp.proto" "$PARENT_PATH/messages-protocol-v2-tmp.proto"' EXIT + -# BUILD combined messages.proto file from protobuf files -# this code was copied from ./submodules/firmware/protob Makekile -# clear protobuf syntax and remove unknown values to be able to work with proto2js -echo 'syntax = "proto2";' > $DIST/messages.proto -echo 'import "google/protobuf/descriptor.proto";' >> $DIST/messages.proto -echo "Build proto file from $SRC" -grep -hv -e '^import ' -e '^syntax' -e '^package' -e 'option java_' $SRC/messages*.proto \ -| sed 's/ hw\.trezor\.messages\.common\./ /' \ -| sed 's/ common\./ /' \ -| sed 's/ ethereum_definitions\./ /' \ -| sed 's/ management\./ /' \ -| sed 's/^option /\/\/ option /' \ -| grep -v ' reserved '>> $DIST/messages.proto +# ============================================================ +# BUILD Pro1 messages.json (requires firmware submodule) +# ============================================================ +# Combines all messages*.proto files from firmware submodule into +# messages.json, then copies to core package. +if [ -d "$SRC_PATH" ] && ls "$SRC_PATH"/messages*.proto 1>/dev/null 2>&1; then + echo "=== Building Pro1 (legacy) protobuf messages ===" + TMP_PROTO="$DIST_PATH/messages-tmp.proto" + echo 'syntax = "proto2";' > "$TMP_PROTO" + echo 'import "google/protobuf/descriptor.proto";' >> "$TMP_PROTO" + echo "Build proto file from $SRC_PATH" + grep -hv -e '^import ' -e '^syntax' -e '^package' -e 'option java_' "$SRC_PATH"/messages*.proto \ + | sed 's/ hw\.trezor\.messages\.common\./ /' \ + | sed 's/ common\./ /' \ + | sed 's/ ethereum_definitions\./ /' \ + | sed 's/ management\./ /' \ + | sed 's/^option /\/\/ option /' \ + | grep -v ' reserved '>> "$TMP_PROTO" -# BUILD messages.json from message.proto -npx pbjs -t json -p $DIST -o $DIST/messages.json --keep-case messages.proto -rm $DIST/messages.proto + npx pbjs -t json -p "$DIST_PATH" -o "$DIST_PATH/messages.json" --keep-case "$TMP_PROTO" + rm "$TMP_PROTO" -echo "generating type definitions for: $LANG" + # 固件仓库的 V1 proto 尚未同步 Attach-to-PIN 请求参数;SDK 需要继续编码这两个兼容字段。 + node - "$DIST_PATH/messages.json" <<'NODE' +const fs = require('fs'); +const messagesPath = process.argv[2]; +const schema = JSON.parse(fs.readFileSync(messagesPath, 'utf8')); +const fields = schema.nested.GetPassphraseState.fields; +fields._only_main_pin = { type: 'bool', id: 2 }; +fields.allow_create_attach_pin = { type: 'bool', id: 3 }; +fs.writeFileSync(messagesPath, JSON.stringify(schema)); +NODE + # Copy to core package + cp "$DIST_PATH/messages.json" "$CORE_MESSAGES_DIR/messages.json" + echo "Pro1 messages.json copied to core" + + yarn --cwd "$PACKAGE_ROOT" prettier --write "$DIST_PATH/messages.json" + yarn --cwd "$PACKAGE_ROOT" prettier --write "$CORE_MESSAGES_DIR/messages.json" + # Type generation (protobuf-types.js) runs once at the end of the Protocol V2 + # section below, after both schemas are available. +else + # Intentional asymmetry with the Protocol V2 section below: the legacy firmware + # submodule is optional (the committed messages.json stays in use when it is + # absent), while firmware-pro2 is the only source of the Protocol V2 schema, + # so its absence is a hard error (exit 1). + echo "⚠️ firmware submodule not found at $SRC_PATH" + echo " Skipping Pro1 protobuf build. To enable:" + echo " git submodule update --init submodules/firmware" +fi + + +# ============================================================ +# BUILD Protocol V2 messages-protocol-v2.json +# ============================================================ +# Source of truth: submodules/firmware-pro2/sys/protobuf/onekey_protocol/. +# Protocol V2 keeps chain/app protocols under legacy/ and system protocols under latest/. +# The SDK flattens them into one protobuf schema for transport runtime, but it must keep +# firmware message names and enum values intact. SDK-facing aliases belong in core/API code, +# not in the protobuf schema. +# ============================================================ cd "$PARENT_PATH" -node ./protobuf-types.js $LANG +SRC_PRO2_LEGACY="$REPO_ROOT/submodules/firmware-pro2/sys/protobuf/onekey_protocol/legacy" +SRC_PRO2_LATEST="$REPO_ROOT/submodules/firmware-pro2/sys/protobuf/onekey_protocol/latest" + +if [ -d "$SRC_PRO2_LATEST" ] && ls "$SRC_PRO2_LATEST"/messages*.proto 1>/dev/null 2>&1; then + echo "=== Building Protocol V2 messages from firmware-pro2 legacy + latest protobuf schema ===" + TMP_PROTO="$PARENT_PATH/messages-protocol-v2-tmp.proto" -yarn prettier --write messages.json -yarn prettier --write **/messages.ts + { + echo 'syntax = "proto2";' + echo 'import "google/protobuf/descriptor.proto";' + echo '' + + # Pro2 firmware keeps chain/app protocols under legacy/, and Protocol V2 + # device/filesystem/firmware protocols under latest/. Build one flat + # schema so Protocol V2 framing can also encode legacy public-chain calls. + grep -hv \ + -e '^import ' -e '^syntax' -e '^package' -e 'option java_' \ + "$SRC_PRO2_LEGACY"/messages*.proto \ + | sed 's/ hw\.onekey\.messages\.[a-zA-Z_]*\./ /g' \ + | sed 's/ crypto\./ /g' \ + | sed 's/ ethereum_definitions\./ /g' \ + | sed 's/ management\./ /g' \ + | sed 's/^option /\/\/ option /' \ + | grep -v ' reserved ' + + echo '' + echo '// --- Protocol V2 system messages ---' + grep -hv \ + -e '^import ' -e '^syntax' -e '^package' -e 'option java_' \ + -e '^option ' \ + "$SRC_PRO2_LATEST"/messages*.proto \ + | grep -v ' reserved ' + + } > "$TMP_PROTO" + + npx pbjs -t json \ + -p "$PARENT_PATH" \ + -o "$PARENT_PATH/../messages-protocol-v2.json" \ + --keep-case \ + "$(basename "$TMP_PROTO")" + + rm -f "$TMP_PROTO" + + cp "$PARENT_PATH/../messages-protocol-v2.json" "$CORE_MESSAGES_DIR/messages-protocol-v2.json" + echo "Protocol V2 messages-protocol-v2.json generated from firmware-pro2 legacy + latest schema and copied to core" + + yarn --cwd "$PACKAGE_ROOT" prettier --write "$PARENT_PATH/../messages-protocol-v2.json" + yarn --cwd "$PACKAGE_ROOT" prettier --write "$CORE_MESSAGES_DIR/messages-protocol-v2.json" + + echo "generating type definitions for: $LANG" + node ./protobuf-types.js $LANG + yarn --cwd "$PACKAGE_ROOT" prettier --write "$PACKAGE_ROOT/src/types/messages.ts" + echo "=== Protocol V2 messages build complete ===" +else + # Unlike the optional Pro1 (legacy) section above, the firmware-pro2 submodule is + # the only source of the Protocol V2 schema, so a missing checkout is a hard error. + echo "firmware-pro2 latest protobuf schema not found at $SRC_PRO2_LATEST" + echo "The Protocol V2 schema requires firmware-pro2 on branch dev." + echo "Run: git submodule update --init submodules/firmware-pro2" + echo "Then: git -C submodules/firmware-pro2 fetch origin dev && git -C submodules/firmware-pro2 checkout origin/dev" + exit 1 +fi diff --git a/packages/hd-transport/scripts/protobuf-patches/TxInputType.js b/packages/hd-transport/scripts/protobuf-patches/TxInputType.js index 0649185cf..de501b0c7 100644 --- a/packages/hd-transport/scripts/protobuf-patches/TxInputType.js +++ b/packages/hd-transport/scripts/protobuf-patches/TxInputType.js @@ -29,6 +29,7 @@ type CommonTxInputType = {| witness?: string, // used by EXTERNAL, depending on script_pubkey ownership_proof?: string, // used by EXTERNAL, depending on script_pubkey commitment_data?: string, // used by EXTERNAL, depending on ownership_proof + coinjoin_flags?: number, // Protocol V2 only (CoinJoin) |}; export type TxInputType = diff --git a/packages/hd-transport/scripts/protobuf-patches/index.js b/packages/hd-transport/scripts/protobuf-patches/index.js index 843c6632a..244e87af3 100644 --- a/packages/hd-transport/scripts/protobuf-patches/index.js +++ b/packages/hd-transport/scripts/protobuf-patches/index.js @@ -116,6 +116,7 @@ const TYPE_PATCH = { 'Features.onekey_se03_state': 'string | null', 'Features.onekey_se04_state': 'string | null', 'HDNodePathType.node': 'HDNodeType | string', + 'FilesystemFile.data': 'Buffer | ArrayBuffer | Uint8Array | string', 'FirmwareUpload.payload': 'Buffer | ArrayBuffer', 'EthereumGetAddress.encoded_network': 'ArrayBuffer', 'EthereumDefinitions.encoded_network': 'ArrayBuffer', @@ -207,6 +208,7 @@ const TYPE_PATCH = { 'TonSignMessage.fwd_fee': UINT_TYPE, 'TonSignMessage.expire_at': UINT_TYPE, 'TonSignMessage.ext_ton_amount': UINT_TYPE, + 'TonSignData.timestamp': UINT_TYPE, 'TonSignProof.expire_at': UINT_TYPE, }; diff --git a/packages/hd-transport/scripts/protobuf-types.js b/packages/hd-transport/scripts/protobuf-types.js index 640d307e3..0d315a7bf 100644 --- a/packages/hd-transport/scripts/protobuf-types.js +++ b/packages/hd-transport/scripts/protobuf-types.js @@ -6,9 +6,29 @@ const fs = require('fs'); const path = require('path'); -const json = require('../messages.json'); const { RULE_PATCH, TYPE_PATCH, DEFINITION_PATCH, SKIP, UINT_TYPE } = require('./protobuf-patches'); +const readJson = filePath => JSON.parse(fs.readFileSync(filePath, 'utf8')); + +const localMessagesJsonPath = path.join(__dirname, '../messages.json'); +const coreMessagesJsonPath = path.join(__dirname, '../../core/src/data/messages/messages.json'); +const json = readJson( + fs.existsSync(localMessagesJsonPath) ? localMessagesJsonPath : coreMessagesJsonPath +); +const optionalJsonFiles = ['../messages-protocol-v2.json']; +const OPTIONAL_DUPLICATE_TYPE_ALIASES = { + DeviceInfo: 'ProtocolV2DeviceInfo', +}; +const messageTypeAliases = {}; +const skipMessageTypeKeys = new Set(Object.values(OPTIONAL_DUPLICATE_TYPE_ALIASES)); + +// V1/V2 duplicate-type merge bookkeeping. +// When the same top-level type exists in both schemas with a different shape we emit +// the union of both sides instead of silently keeping the V1 definition only. +const mergedRuleOverrides = {}; // `${Type}.${field}` -> 'optional' +const mergedTypeOverrides = {}; // `${Type}.${field}` -> 'v1JsType | v2JsType' +const duplicateTypeMergeReport = []; // { name, diffs: string[] } + const args = process.argv.slice(2); const isTypescript = args.includes('typescript'); @@ -26,6 +46,8 @@ const FIELD_TYPES = { const types = []; // { type: 'enum | message', name: string, value: string[], exact?: boolean }; +const hasParsedType = name => types.some(t => t && t.name === name); + // enums used as keys (string), used as values (number) by default const ENUM_KEYS = [ 'InputScriptType', @@ -111,8 +133,9 @@ const useDefinition = def => { return clean.replace(/\/\/ @typescript-variant(.*)/, '').replace(/\/\/ @flowtype-variant:/, ''); }; -const parseMessage = (messageName, message, depth = 0) => { +const parseMessage = (messageName, message, depth = 0, skipExisting = false) => { if (messageName === 'google') return; + if (!depth && skipExisting && hasParsedType(messageName)) return; const value = []; // add comment line if (!depth) value.push(`// ${messageName}`); @@ -120,7 +143,9 @@ const parseMessage = (messageName, message, depth = 0) => { // declare nested values if (message.nested) { - Object.keys(message.nested).map(item => parseMessage(item, message.nested[item], depth + 1)); + Object.keys(message.nested).map(item => + parseMessage(item, message.nested[item], depth + 1, skipExisting) + ); } if (message.values) { @@ -142,11 +167,15 @@ const parseMessage = (messageName, message, depth = 0) => { Object.keys(message.fields).forEach(fieldName => { const field = message.fields[fieldName]; const fieldKey = `${messageName}.${fieldName}`; - // find patch for "rule" - const fieldRule = RULE_PATCH[fieldKey] || field.rule; + // find patch for "rule" (hand-written patches win over V1/V2 merge overrides) + const fieldRule = RULE_PATCH[fieldKey] || mergedRuleOverrides[fieldKey] || field.rule; const rule = fieldRule === 'required' || fieldRule === 'repeated' ? ': ' : '?: '; - // find patch for "type" - let type = TYPE_PATCH[fieldKey] || FIELD_TYPES[field.type] || field.type; + // find patch for "type" (hand-written patches win over V1/V2 merge overrides) + let type = + TYPE_PATCH[fieldKey] || + mergedTypeOverrides[fieldKey] || + FIELD_TYPES[field.type] || + field.type; // automatically convert all amount and fee fields to UINT_TYPE if (['amount', 'fee'].includes(fieldName)) { type = UINT_TYPE; @@ -173,8 +202,159 @@ const parseMessage = (messageName, message, depth = 0) => { }); }; +const jsFieldType = protoType => FIELD_TYPES[protoType] || protoType; + +// Merge a duplicate Protocol V2 definition into its V1 counterpart (in-memory only, +// messages.json / messages-protocol-v2.json on disk stay untouched). +// Strategy: +// - messages: emit the union of both sides' fields; a field present on one side only +// becomes optional +// - field type conflict: emit a union type and warn +// - field rule conflict (required/optional/repeated): keep the V1 rule and warn +// - enums: emit the union of members; member value conflicts keep the V1 value and warn +// - nested types are merged recursively +const mergeDuplicateDefinition = (typeName, base, extra, diffs) => { + if (base.values || extra.values) { + if (!base.values || !extra.values) { + diffs.push('! kind conflict (enum vs message), keeping V1 definition'); + console.warn( + `[protobuf-types] duplicate type "${typeName}" is an enum on one side and a message on the other, keeping V1` + ); + return; + } + Object.entries(extra.values).forEach(([memberName, memberValue]) => { + if (!(memberName in base.values)) { + base.values[memberName] = memberValue; + diffs.push(`+ enum member ${memberName} = ${memberValue} (V2 only)`); + } else if (base.values[memberName] !== memberValue) { + diffs.push( + `! enum member ${memberName} value conflict: V1=${base.values[memberName]} V2=${memberValue}, keeping V1` + ); + console.warn( + `[protobuf-types] enum "${typeName}.${memberName}" value differs between V1 (${base.values[memberName]}) and V2 (${memberValue}), keeping V1` + ); + } + }); + return; + } + + if (extra.nested) { + base.nested = base.nested || {}; + Object.keys(extra.nested).forEach(nestedName => { + if (base.nested[nestedName]) { + mergeDuplicateDefinition( + nestedName, + base.nested[nestedName], + extra.nested[nestedName], + diffs + ); + } else { + base.nested[nestedName] = extra.nested[nestedName]; + diffs.push(`+ nested type ${nestedName} (V2 only)`); + } + }); + } + + const extraFields = extra.fields || {}; + if (!Object.keys(extraFields).length && !base.fields) return; + base.fields = base.fields || {}; + const baseFields = base.fields; + + Object.entries(extraFields).forEach(([fieldName, field]) => { + const fieldKey = `${typeName}.${fieldName}`; + if (!(fieldName in baseFields)) { + baseFields[fieldName] = field; + // field exists in V2 only -> always optional in the merged interface + if (field.rule) mergedRuleOverrides[fieldKey] = 'optional'; + diffs.push( + `+ field ${fieldName}?: ${jsFieldType(field.type)}${ + field.rule === 'repeated' ? '[]' : '' + } (V2 only, marked optional)` + ); + return; + } + const baseField = baseFields[fieldName]; + if (baseField.type !== field.type) { + const baseJsType = jsFieldType(baseField.type); + const extraJsType = jsFieldType(field.type); + if (baseJsType !== extraJsType) { + mergedTypeOverrides[fieldKey] = `${baseJsType} | ${extraJsType}`; + diffs.push( + `! field ${fieldName} type conflict: V1=${baseField.type} V2=${field.type}, emitting union ${baseJsType} | ${extraJsType}` + ); + console.warn( + `[protobuf-types] field "${fieldKey}" type differs between V1 (${baseField.type}) and V2 (${field.type}), emitting union type` + ); + } + } + if ((baseField.rule || 'optional') !== (field.rule || 'optional')) { + diffs.push( + `! field ${fieldName} rule conflict: V1=${baseField.rule || 'optional'} V2=${ + field.rule || 'optional' + }, keeping V1` + ); + } + }); + + Object.entries(baseFields).forEach(([fieldName, field]) => { + if (fieldName in extraFields) return; + const fieldKey = `${typeName}.${fieldName}`; + // field exists in V1 only -> always optional in the merged interface + if (field.rule && !mergedRuleOverrides[fieldKey]) { + mergedRuleOverrides[fieldKey] = 'optional'; + diffs.push(`~ field ${fieldName} (V1 only): ${field.rule} -> optional`); + } else { + diffs.push(`~ field ${fieldName} (V1 only, already optional)`); + } + }); +}; + +// Pass 1 (before parsing): merge duplicate V2 definitions into the V1 schema so the +// generated interfaces contain the union of both protocols' fields. Previously the V2 +// side of a duplicate type was silently dropped, hiding V2-only fields/enum members. +const optionalJsons = []; +optionalJsonFiles.forEach(jsonFile => { + const jsonPath = path.join(__dirname, jsonFile); + if (!fs.existsSync(jsonPath)) return; + const optionalJson = readJson(jsonPath); + optionalJsons.push(optionalJson); + Object.keys(optionalJson.nested).forEach(e => { + if (!json.nested[e]) return; // V2-only type, parsed after the V1 pass below + if (OPTIONAL_DUPLICATE_TYPE_ALIASES[e]) return; // kept as a separate aliased type + if (SKIP.includes(e)) return; // custom/hand-written definitions (MessageType, TxInput, ...) + const diffs = []; + mergeDuplicateDefinition(e, json.nested[e], optionalJson.nested[e], diffs); + if (diffs.length) duplicateTypeMergeReport.push({ name: e, diffs }); + }); +}); + +// Explicit drift report: every merged duplicate type and its V1/V2 differences. +if (duplicateTypeMergeReport.length) { + console.log( + `[protobuf-types] ${duplicateTypeMergeReport.length} duplicate V1/V2 type(s) differ and were merged (field union):` + ); + duplicateTypeMergeReport.forEach(({ name, diffs }) => { + console.log(` - ${name}`); + diffs.forEach(diff => console.log(` ${diff}`)); + }); +} + // top level messages and nested messages -Object.keys(json.nested).map(e => parseMessage(e, json.nested[e])); +Object.keys(json.nested).forEach(e => parseMessage(e, json.nested[e])); + +// Pass 2: aliased duplicates (e.g. DeviceInfo -> ProtocolV2DeviceInfo) and V2-only types +optionalJsons.forEach(optionalJson => { + Object.keys(optionalJson.nested).forEach(e => { + const alias = hasParsedType(e) ? OPTIONAL_DUPLICATE_TYPE_ALIASES[e] : undefined; + if (alias) { + parseMessage(alias, optionalJson.nested[e]); + messageTypeAliases[e] = [e, alias]; + return; + } + if (json.nested[e]) return; // duplicate type, already merged into the V1 definition + parseMessage(e, optionalJson.nested[e], 0, true); + }); +}); // types needs reordering (used before defined) const ORDER = { @@ -223,10 +403,14 @@ if (!isTypescript) { types .flatMap(t => (t && t.type === 'message' ? [t] : [])) .forEach(t => { + if (skipMessageTypeKeys.has(t.name)) return; + const messageTypeValue = messageTypeAliases[t.name] + ? messageTypeAliases[t.name].join(' | ') + : t.name; if (t.exact) { - lines.push(` ${t.name}: $Exact<${t.name}>;`); + lines.push(` ${t.name}: $Exact<${messageTypeValue}>;`); } else { - lines.push(` ${t.name}: ${t.name};`); + lines.push(` ${t.name}: ${messageTypeValue};`); } // lines.push(' ' + t.name + ': $Exact<' + t.name + '>;'); }); @@ -241,11 +425,18 @@ export type MessageResponse = { message: $ElementType; }; -export type TypedCall = ( +export type TypedCall = { + >( + type: T, + resType: R, + message?: $ElementType + ): Promise>>; + ( type: T, resType: R, message?: $ElementType -) => Promise>; + ): Promise>; +}; `); } else { lines.push('// custom connect definitions'); @@ -253,7 +444,11 @@ export type TypedCall = ( types .flatMap(t => (t && t.type === 'message' ? [t] : [])) .forEach(t => { - lines.push(` ${t.name}: ${t.name};`); + if (skipMessageTypeKeys.has(t.name)) return; + const messageTypeValue = messageTypeAliases[t.name] + ? messageTypeAliases[t.name].join(' | ') + : t.name; + lines.push(` ${t.name}: ${messageTypeValue};`); }); lines.push('};'); @@ -266,11 +461,22 @@ export type MessageResponse = { message: MessageType[T]; }; -export type TypedCall = ( - type: T, - resType: R, - message?: MessageType[T], -) => Promise>; +export type MessageResponseMap = { + [K in MessageKey]: MessageResponse; +}; + +export type TypedCall = { + ( + type: T, + resType: R, + message?: MessageType[T], + ): Promise; + ( + type: T, + resType: R, + message?: MessageType[T], + ): Promise>; +}; `); } diff --git a/packages/hd-transport/src/constants.ts b/packages/hd-transport/src/constants.ts index a4bbe01aa..a5bef2638 100644 --- a/packages/hd-transport/src/constants.ts +++ b/packages/hd-transport/src/constants.ts @@ -1,8 +1,50 @@ -export const MESSAGE_TOP_CHAR = 0x003f; -export const MESSAGE_HEADER_BYTE = 0x23; -export const HEADER_SIZE = 1 + 1 + 4 + 2; -export const BUFFER_SIZE = 63; +// ---- Protocol V1 (Pro1 / Touch / Mini / Classic) ---- + +/** Protocol V1 USB report 标记,ASCII '?'。 */ +export const PROTOCOL_V1_REPORT_ID = 0x3f; + +/** Protocol V1 envelope 头部标记,ASCII '#'。 */ +export const PROTOCOL_V1_HEADER_BYTE = 0x23; + +/** Protocol V1 单个 chunk 去掉 report 标记后的可用 payload 长度。 */ +export const PROTOCOL_V1_CHUNK_PAYLOAD_SIZE = 63; + +/** Protocol V1 USB packet 长度:report 标记 + chunk payload。 */ +export const PROTOCOL_V1_USB_PACKET_SIZE = PROTOCOL_V1_CHUNK_PAYLOAD_SIZE + 1; + +/** Protocol V1 message metadata:message type 2 字节 + payload length 4 字节。 */ +export const PROTOCOL_V1_MESSAGE_HEADER_SIZE = 2 + 4; + +/** Protocol V1 envelope metadata:## + message type + payload length。 */ +export const PROTOCOL_V1_ENVELOPE_HEADER_SIZE = 1 + 1 + PROTOCOL_V1_MESSAGE_HEADER_SIZE; + +// ---- Protocol V2 (Pro2 USB / BLE transports) ---- + +/** Protocol V2 单帧最大长度,包含 header、payload 和 CRC;需容纳 4000 数据块及 protobuf 开销。 */ +export const PROTOCOL_V2_FRAME_MAX_BYTES = 4608; + +/** WebUSB 下 FilesystemFileWrite 的文件数据分块大小。 */ +export const PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE = 4000; + +/** BLE 下 FilesystemFileWrite 的文件数据分块大小。 */ +export const PROTOCOL_V2_BLE_FILE_CHUNK_SIZE = 1800; + +/** BLE 下 FilesystemFileRead 的文件数据分块大小,受 Pro2 UART 1024B TX 缓冲限制。 */ +export const PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE = 900; + +/** Pro2 BLE/UART 接收 FIFO 必须容纳完整 Proto Link 帧。 */ +export const PROTOCOL_V2_BLE_FRAME_MAX_BYTES = 2048; + +/** @deprecated 使用按传输区分的 PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE / PROTOCOL_V2_BLE_FILE_CHUNK_SIZE。 */ +export const PROTOCOL_V2_FILE_CHUNK_SIZE = PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE; + /** - * exclude ?## + * Protocol V2 路由 channel。 + * USB 端点直接到主 MCU,不需要 proto-link 路由;BLE 需要经过 BLE 协处理器 UART bridge。 */ -export const COMMON_HEADER_SIZE = 6; +export const PROTOCOL_V2_CHANNEL_USB = 0; +export const PROTOCOL_V2_CHANNEL_BLE_UART = 1; +export const PROTOCOL_V2_CHANNEL_SOCKET = 2; + +/** protobuf 命令流的 packet_src,固件侧会把 0 路由到 protobuf dispatcher。 */ +export const PROTOCOL_V2_PACKET_SRC_COMMAND = 0; diff --git a/packages/hd-transport/src/index.ts b/packages/hd-transport/src/index.ts index 2653f634b..1be752bda 100644 --- a/packages/hd-transport/src/index.ts +++ b/packages/hd-transport/src/index.ts @@ -1,14 +1,28 @@ import * as protobuf from 'protobufjs/light'; -import * as Long from 'long'; +import Long from 'long'; import { - buildBuffers, - buildEncodeBuffers, - buildOne, - decodeProtocol, + createMessageFromName, + createMessageFromType, + decodeProtobuf, + encodeProtobuf, parseConfigure, - receiveOne, } from './serialization'; +import { PROTOCOL_V2_SYS_MESSAGE_THRESHOLD, ProtocolV1, ProtocolV2 } from './protocols'; +import * as protocolV2Codec from './protocols/v2'; +import { ProtocolV2LinkManager } from './protocols/v2/link-manager'; +import { ProtocolV2UsbTransportBase } from './protocols/v2/usb-transport-base'; +import { + ProtocolV2FrameAssembler, + ProtocolV2SequenceCursor, + ProtocolV2Session, + bytesToHex, + concatUint8Arrays, + getErrorMessage, + hexToBytes, + probeProtocolV2, + withProtocolTimeout, +} from './protocols/v2/session'; import * as check from './utils/highlevel-checks'; protobuf.util.Long = Long; @@ -21,10 +35,12 @@ export type { OneKeyMobileDeviceInfo, OneKeyDeviceInfoWithSession, MessageFromOneKey, + TransportCallOptions, LowlevelTransportSharedPlugin, LowLevelDevice, OneKeyDeviceInfoBase, OneKeyDeviceCommType, + ProtocolType, } from './types'; export { Messages } from './types'; @@ -32,13 +48,33 @@ export * from './types/messages'; export * from './utils/logBlockCommand'; export * from './constants'; +export * from './protocols'; +export * as protocolV1 from './protocols/v1'; +export * as protocolV2 from './protocols/v2'; +export * from './protocols/v2/session'; +export * from './protocols/v2/link-manager'; +export * from './protocols/v2/usb-transport-base'; export default { check, - buildOne, - buildBuffers, - buildEncodeBuffers, - receiveOne, parseConfigure, - decodeProtocol, + protocolV2: protocolV2Codec, + ProtocolV1, + ProtocolV2, + PROTOCOL_V2_SYS_MESSAGE_THRESHOLD, + ProtocolV2FrameAssembler, + ProtocolV2LinkManager, + ProtocolV2SequenceCursor, + ProtocolV2Session, + ProtocolV2UsbTransportBase, + bytesToHex, + concatUint8Arrays, + createMessageFromName, + createMessageFromType, + encodeProtobuf, + decodeProtobuf, + getErrorMessage, + hexToBytes, + probeProtocolV2, + withProtocolTimeout, }; diff --git a/packages/hd-transport/src/protocols/index.ts b/packages/hd-transport/src/protocols/index.ts new file mode 100644 index 000000000..40ba0371b --- /dev/null +++ b/packages/hd-transport/src/protocols/index.ts @@ -0,0 +1,112 @@ +import ByteBuffer from 'bytebuffer'; + +import { encodeEnvelopeMessage, encodeMessageChunks, encodeTransportPackets } from './v1/packets'; +import { decodeFirstChunk } from './v1/decode'; +import { decodeMessage as decodeV1Message } from './v1/receive'; +import { createMessageFromName, createMessageFromType } from '../serialization/protobuf/messages'; +import { encode as encodeProtobuf } from '../serialization/protobuf/encode'; +import { decode as decodeProtobuf } from '../serialization/protobuf/decode'; +import { decodeFrame as decodeV2Frame, encodeProtobufFrame, isAckFrame } from './v2'; + +import type { Root } from 'protobufjs/light'; + +export const PROTOCOL_V2_SYS_MESSAGE_THRESHOLD = 60000; + +type ProtocolV2Schemas = { + protocolV1: Root; + protocolV2: Root; +}; + +type ProtocolV2FrameOptions = { + packetSrc?: number; + router?: number; + /** Sequence number (1-255). Managed per-session by ProtocolV2Session. */ + seq?: number; +}; + +const resolveProtocolV2EncodeSchema = (name: string, schemas: ProtocolV2Schemas) => { + try { + schemas.protocolV2.lookupType(name); + return schemas.protocolV2; + } catch { + throw new Error(`Protocol V2 message "${name}" is not defined in Protocol V2 schema`); + } +}; + +const PROTOCOL_V2_LEGACY_DECODE_ALLOWLIST = new Set([ + 'Failure', + 'ButtonRequest', + 'EntropyRequest', + 'PinMatrixRequest', + 'PassphraseRequest', + 'Deprecated_PassphraseStateRequest', + 'WordRequest', +]); + +const createProtocolV2MessageFromType = (messageTypeId: number, schemas: ProtocolV2Schemas) => { + try { + return createMessageFromType(schemas.protocolV2, messageTypeId); + } catch (protocolV2Error) { + let legacyMessage: ReturnType; + try { + legacyMessage = createMessageFromType(schemas.protocolV1, messageTypeId); + } catch { + throw protocolV2Error; + } + if (PROTOCOL_V2_LEGACY_DECODE_ALLOWLIST.has(legacyMessage.messageName)) { + return legacyMessage; + } + throw protocolV2Error; + } +}; + +export const ProtocolV1 = { + encodeEnvelope: encodeEnvelopeMessage, + encodeMessageChunks, + encodeTransportPackets, + decodeFirstChunk, + + decodeMessage: decodeV1Message, +}; + +export const ProtocolV2 = { + isAckFrame, + + encodeFrame( + schemas: ProtocolV2Schemas, + name: string, + data: Record, + options: ProtocolV2FrameOptions = {} + ) { + const encodeMessages = resolveProtocolV2EncodeSchema(name, schemas); + const { Message, messageTypeId } = createMessageFromName(encodeMessages, name); + const pbBuffer = encodeProtobuf(Message, data); + pbBuffer.reset(); + const rawPbBuffer = pbBuffer.toBuffer() as unknown as ArrayBuffer; + const pbBytes = new Uint8Array(rawPbBuffer); + + return encodeProtobufFrame( + messageTypeId, + pbBytes, + options.packetSrc, + options.router, + options.seq + ); + }, + + decodeFrame(schemas: ProtocolV2Schemas, frame: Uint8Array) { + const { messageTypeId, pbPayload, seq } = decodeV2Frame(frame); + const { Message, messageName } = createProtocolV2MessageFromType(messageTypeId, schemas); + const rxByteBuffer = ByteBuffer.wrap(Buffer.from(pbPayload) as unknown as ArrayBuffer); + const message = decodeProtobuf(Message, rxByteBuffer); + + return { + message, + messageName, + messageTypeId, + pbPayload, + seq, + type: messageName, + }; + }, +}; diff --git a/packages/hd-transport/src/serialization/protocol/decode.ts b/packages/hd-transport/src/protocols/v1/decode.ts similarity index 82% rename from packages/hd-transport/src/serialization/protocol/decode.ts rename to packages/hd-transport/src/protocols/v1/decode.ts index a9021e856..d370e4cdd 100644 --- a/packages/hd-transport/src/serialization/protocol/decode.ts +++ b/packages/hd-transport/src/protocols/v1/decode.ts @@ -1,6 +1,6 @@ import ByteBuffer from 'bytebuffer'; -import { MESSAGE_HEADER_BYTE } from '../../constants'; +import { PROTOCOL_V1_HEADER_BYTE } from '../../constants'; /** * Reads meta information from buffer @@ -24,7 +24,7 @@ const readHeaderChunked = (buffer: ByteBuffer) => { return { sharp1, sharp2, typeId, length }; }; -export const decode = (byteBuffer: ByteBuffer) => { +export const decodeEnvelope = (byteBuffer: ByteBuffer) => { const { typeId } = readHeader(byteBuffer); return { @@ -35,13 +35,13 @@ export const decode = (byteBuffer: ByteBuffer) => { // Parses first raw input that comes from Trezor and returns some information about the whole message. // [compatibility]: accept Buffer just like decode does. But this would require changes in lower levels -export const decodeChunked = (bytes: ArrayBuffer) => { +export const decodeFirstChunk = (bytes: ArrayBuffer) => { // convert to ByteBuffer so it's easier to read const byteBuffer = ByteBuffer.wrap(bytes, undefined, undefined, true); const { sharp1, sharp2, typeId, length } = readHeaderChunked(byteBuffer); - if (sharp1 !== MESSAGE_HEADER_BYTE || sharp2 !== MESSAGE_HEADER_BYTE) { + if (sharp1 !== PROTOCOL_V1_HEADER_BYTE || sharp2 !== PROTOCOL_V1_HEADER_BYTE) { throw new Error("Didn't receive expected header signature."); } diff --git a/packages/hd-transport/src/serialization/protocol/encode.ts b/packages/hd-transport/src/protocols/v1/encode.ts similarity index 53% rename from packages/hd-transport/src/serialization/protocol/encode.ts rename to packages/hd-transport/src/protocols/v1/encode.ts index 990a40112..3b50a88e6 100644 --- a/packages/hd-transport/src/serialization/protocol/encode.ts +++ b/packages/hd-transport/src/protocols/v1/encode.ts @@ -1,30 +1,35 @@ import ByteBuffer from 'bytebuffer'; -import { BUFFER_SIZE, HEADER_SIZE, MESSAGE_HEADER_BYTE } from '../../constants'; +import { + PROTOCOL_V1_CHUNK_PAYLOAD_SIZE, + PROTOCOL_V1_ENVELOPE_HEADER_SIZE, + PROTOCOL_V1_HEADER_BYTE, +} from '../../constants'; type Options = { chunked: Chunked; addTrezorHeaders: boolean; - messageType: number; + messageTypeId: number; }; -function encode(data: ByteBuffer, options: Options): Buffer[]; -function encode(data: ByteBuffer, options: Options): Buffer; -function encode(data: any, options: any): any { - const { addTrezorHeaders, chunked, messageType } = options; - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - const fullSize = (addTrezorHeaders ? HEADER_SIZE : HEADER_SIZE - 2) + data.limit; +function encodeEnvelope(data: ByteBuffer, options: Options): Buffer[]; +function encodeEnvelope(data: ByteBuffer, options: Options): Buffer; +function encodeEnvelope(data: any, options: any): any { + const { addTrezorHeaders, chunked, messageTypeId } = options; + const fullSize = + (addTrezorHeaders ? PROTOCOL_V1_ENVELOPE_HEADER_SIZE : PROTOCOL_V1_ENVELOPE_HEADER_SIZE - 2) + + Number(data.limit); const encodedByteBuffer = new ByteBuffer(fullSize); if (addTrezorHeaders) { // 2*1 byte - encodedByteBuffer.writeByte(MESSAGE_HEADER_BYTE); - encodedByteBuffer.writeByte(MESSAGE_HEADER_BYTE); + encodedByteBuffer.writeByte(PROTOCOL_V1_HEADER_BYTE); + encodedByteBuffer.writeByte(PROTOCOL_V1_HEADER_BYTE); } // 2 bytes - encodedByteBuffer.writeUint16(messageType); + encodedByteBuffer.writeUint16(messageTypeId); // 4 bytes (so 8 in total) encodedByteBuffer.writeUint32(data.limit); @@ -39,7 +44,7 @@ function encode(data: any, options: any): any { } const result: Buffer[] = []; - const size = BUFFER_SIZE; + const size = PROTOCOL_V1_CHUNK_PAYLOAD_SIZE; // How many pieces will there actually be const count = Math.floor((encodedByteBuffer.limit - 1) / size) + 1 || 1; @@ -56,4 +61,4 @@ function encode(data: any, options: any): any { return result; } -export { encode }; +export { encodeEnvelope }; diff --git a/packages/hd-transport/src/protocols/v1/index.ts b/packages/hd-transport/src/protocols/v1/index.ts new file mode 100644 index 000000000..20ab6ec2d --- /dev/null +++ b/packages/hd-transport/src/protocols/v1/index.ts @@ -0,0 +1,4 @@ +export * from './decode'; +export * from './encode'; +export * from './packets'; +export * from './receive'; diff --git a/packages/hd-transport/src/protocols/v1/packets.ts b/packages/hd-transport/src/protocols/v1/packets.ts new file mode 100644 index 000000000..129a3970e --- /dev/null +++ b/packages/hd-transport/src/protocols/v1/packets.ts @@ -0,0 +1,53 @@ +import ByteBuffer from 'bytebuffer'; + +import { encode as encodeProtobuf } from '../../serialization/protobuf'; +import { encodeEnvelope } from './encode'; +import { createMessageFromName } from '../../serialization/protobuf/messages'; +import { PROTOCOL_V1_CHUNK_PAYLOAD_SIZE, PROTOCOL_V1_REPORT_ID } from '../../constants'; + +import type { Root } from 'protobufjs/light'; + +export function encodeEnvelopeMessage(messages: Root, name: string, data: Record) { + const { Message, messageTypeId } = createMessageFromName(messages, name); + + const buffer = encodeProtobuf(Message, data); + return encodeEnvelope(buffer, { + addTrezorHeaders: false, + chunked: false, + messageTypeId, + }); +} + +export const encodeMessageChunks = ( + messages: Root, + name: string, + data: Record +) => { + const { Message, messageTypeId } = createMessageFromName(messages, name); + const buffer = encodeProtobuf(Message, data); + return encodeEnvelope(buffer, { + addTrezorHeaders: true, + chunked: true, + messageTypeId, + }); +}; + +export const encodeTransportPackets = ( + messages: Root, + name: string, + data: Record +) => { + const encodeBuffers = encodeMessageChunks(messages, name, data); + + const outBuffers: ByteBuffer[] = []; + + for (const buf of encodeBuffers) { + const chunkBuffer = new ByteBuffer(PROTOCOL_V1_CHUNK_PAYLOAD_SIZE + 1); + chunkBuffer.writeByte(PROTOCOL_V1_REPORT_ID); + chunkBuffer.append(buf); + chunkBuffer.reset(); + outBuffers.push(chunkBuffer); + } + + return outBuffers; +}; diff --git a/packages/hd-transport/src/serialization/receive.ts b/packages/hd-transport/src/protocols/v1/receive.ts similarity index 51% rename from packages/hd-transport/src/serialization/receive.ts rename to packages/hd-transport/src/protocols/v1/receive.ts index 253fa0424..4ca9925e0 100644 --- a/packages/hd-transport/src/serialization/receive.ts +++ b/packages/hd-transport/src/protocols/v1/receive.ts @@ -1,15 +1,15 @@ import ByteBuffer from 'bytebuffer'; -import * as decodeProtobuf from './protobuf/decode'; -import * as decodeProtocol from './protocol/decode'; -import { createMessageFromType } from './protobuf/messages'; +import * as decodeProtobuf from '../../serialization/protobuf/decode'; +import { decodeEnvelope } from './decode'; +import { createMessageFromType } from '../../serialization/protobuf/messages'; import type { Root } from 'protobufjs/light'; -export function receiveOne(messages: Root, data: string) { +export function decodeMessage(messages: Root, data: string) { const bytebuffer = ByteBuffer.wrap(data, 'hex'); - const { typeId, buffer } = decodeProtocol.decode(bytebuffer); + const { typeId, buffer } = decodeEnvelope(bytebuffer); const { Message, messageName } = createMessageFromType(messages, typeId); const message = decodeProtobuf.decode(Message, buffer); return { diff --git a/packages/hd-transport/src/protocols/v2/constants.ts b/packages/hd-transport/src/protocols/v2/constants.ts new file mode 100644 index 000000000..2cc2dcb7b --- /dev/null +++ b/packages/hd-transport/src/protocols/v2/constants.ts @@ -0,0 +1,7 @@ +export const PROTO_HEAD_SOF = 0x5a; +export const PROTO_PRE_HEAD_SIZE = 4; +export const PROTO_HEAD_CRC_SIZE = 8; +export const CRC8_INIT = 0x30; +export const PACKET_SIZE = 4096; +export const PROTO_DATA_TYPE_PACKET = 0; +export const PROTO_DATA_TYPE_ACK = 1; diff --git a/packages/hd-transport/src/protocols/v2/crc8.ts b/packages/hd-transport/src/protocols/v2/crc8.ts new file mode 100644 index 000000000..05416f37a --- /dev/null +++ b/packages/hd-transport/src/protocols/v2/crc8.ts @@ -0,0 +1,34 @@ +import { CRC8_INIT } from './constants'; + +// Protocol V2 帧校验用的 CRC-8 查表,不是设备或业务数据。 +// 表值与固件侧 crc8 实现保持一致;初始值见 CRC8_INIT。 +export const CRC8_TABLE = new Uint8Array([ + 0x00, 0x5e, 0xbc, 0xe2, 0x61, 0x3f, 0xdd, 0x83, 0xc2, 0x9c, 0x7e, 0x20, 0xa3, 0xfd, 0x1f, 0x41, + 0x9d, 0xc3, 0x21, 0x7f, 0xfc, 0xa2, 0x40, 0x1e, 0x5f, 0x01, 0xe3, 0xbd, 0x3e, 0x60, 0x82, 0xdc, + 0x23, 0x7d, 0x9f, 0xc1, 0x42, 0x1c, 0xfe, 0xa0, 0xe1, 0xbf, 0x5d, 0x03, 0x80, 0xde, 0x3c, 0x62, + 0xbe, 0xe0, 0x02, 0x5c, 0xdf, 0x81, 0x63, 0x3d, 0x7c, 0x22, 0xc0, 0x9e, 0x1d, 0x43, 0xa1, 0xff, + 0x46, 0x18, 0xfa, 0xa4, 0x27, 0x79, 0x9b, 0xc5, 0x84, 0xda, 0x38, 0x66, 0xe5, 0xbb, 0x59, 0x07, + 0xdb, 0x85, 0x67, 0x39, 0xba, 0xe4, 0x06, 0x58, 0x19, 0x47, 0xa5, 0xfb, 0x78, 0x26, 0xc4, 0x9a, + 0x65, 0x3b, 0xd9, 0x87, 0x04, 0x5a, 0xb8, 0xe6, 0xa7, 0xf9, 0x1b, 0x45, 0xc6, 0x98, 0x7a, 0x24, + 0xf8, 0xa6, 0x44, 0x1a, 0x99, 0xc7, 0x25, 0x7b, 0x3a, 0x64, 0x86, 0xd8, 0x5b, 0x05, 0xe7, 0xb9, + 0x8c, 0xd2, 0x30, 0x6e, 0xed, 0xb3, 0x51, 0x0f, 0x4e, 0x10, 0xf2, 0xac, 0x2f, 0x71, 0x93, 0xcd, + 0x11, 0x4f, 0xad, 0xf3, 0x70, 0x2e, 0xcc, 0x92, 0xd3, 0x8d, 0x6f, 0x31, 0xb2, 0xec, 0x0e, 0x50, + 0xaf, 0xf1, 0x13, 0x4d, 0xce, 0x90, 0x72, 0x2c, 0x6d, 0x33, 0xd1, 0x8f, 0x0c, 0x52, 0xb0, 0xee, + 0x32, 0x6c, 0x8e, 0xd0, 0x53, 0x0d, 0xef, 0xb1, 0xf0, 0xae, 0x4c, 0x12, 0x91, 0xcf, 0x2d, 0x73, + 0xca, 0x94, 0x76, 0x28, 0xab, 0xf5, 0x17, 0x49, 0x08, 0x56, 0xb4, 0xea, 0x69, 0x37, 0xd5, 0x8b, + 0x57, 0x09, 0xeb, 0xb5, 0x36, 0x68, 0x8a, 0xd4, 0x95, 0xcb, 0x29, 0x77, 0xf4, 0xaa, 0x48, 0x16, + 0xe9, 0xb7, 0x55, 0x0b, 0x88, 0xd6, 0x34, 0x6a, 0x2b, 0x75, 0x97, 0xc9, 0x4a, 0x14, 0xf6, 0xa8, + 0x74, 0x2a, 0xc8, 0x96, 0x15, 0x4b, 0xa9, 0xf7, 0xb6, 0xe8, 0x0a, 0x54, 0xd7, 0x89, 0x6b, 0x35, +]); + +/** + * 计算 data 前 len 个字节的 CRC-8,使用与固件侧一致的初始值。 + */ +export function crc8(data: Uint8Array, len: number): number { + let crc = CRC8_INIT; + for (let i = 0; i < len; i++) { + // eslint-disable-next-line no-bitwise + crc = CRC8_TABLE[crc ^ data[i]]; + } + return crc; +} diff --git a/packages/hd-transport/src/protocols/v2/decode.ts b/packages/hd-transport/src/protocols/v2/decode.ts new file mode 100644 index 000000000..792646a03 --- /dev/null +++ b/packages/hd-transport/src/protocols/v2/decode.ts @@ -0,0 +1,89 @@ +import { PROTO_DATA_TYPE_ACK, PROTO_HEAD_CRC_SIZE, PROTO_HEAD_SOF } from './constants'; +import { crc8 } from './crc8'; + +export interface ProtoV2Frame { + /** Little-endian message type ID */ + messageTypeId: number; + /** Raw protobuf-encoded payload (bytes after the 2-byte messageTypeId) */ + pbPayload: Uint8Array; + /** Sequence number from the frame header */ + seq: number; +} + +function validateFrame(data: Uint8Array): number { + if (data.length < PROTO_HEAD_CRC_SIZE) { + throw new Error(`Protocol V2 frame too short: ${data.length} bytes`); + } + + if (data[0] !== PROTO_HEAD_SOF) { + throw new Error( + `Invalid SOF byte: expected 0x5A, got 0x${data[0].toString(16).padStart(2, '0')}` + ); + } + + const frameLen = data[1] + data[2] * 256; + + if (data.length < frameLen) { + throw new Error(`Frame truncated: expected ${frameLen} bytes, got ${data.length}`); + } + + const expectedHeaderCrc = crc8(data, 3); + if (data[3] !== expectedHeaderCrc) { + throw new Error( + `Header CRC mismatch: expected 0x${expectedHeaderCrc + .toString(16) + .padStart(2, '0')}, got 0x${data[3].toString(16).padStart(2, '0')}` + ); + } + + const expectedFrameCrc = crc8(data, frameLen - 1); + if (data[frameLen - 1] !== expectedFrameCrc) { + throw new Error( + `Frame CRC mismatch: expected 0x${expectedFrameCrc + .toString(16) + .padStart(2, '0')}, got 0x${data[frameLen - 1].toString(16).padStart(2, '0')}` + ); + } + + return frameLen; +} + +export function isAckFrame(data: Uint8Array): boolean { + // eslint-disable-next-line no-bitwise + if (data.length < 6 || (data[5] & 0x03) !== PROTO_DATA_TYPE_ACK) { + return false; + } + + const frameLen = validateFrame(data); + if (frameLen !== PROTO_HEAD_CRC_SIZE) { + throw new Error(`Invalid Protocol V2 ACK frame length: ${frameLen}`); + } + return true; +} + +/** + * Parse and validate a Protocol V2 response frame. + * + * Validates: + * - SOF byte (0x5A) + * - Header CRC (bytes 0-2) + * - Frame CRC (full frame except last byte) + * + * Returns the decoded messageTypeId, raw protobuf payload, and sequence number. + */ +export function decodeFrame(data: Uint8Array): ProtoV2Frame { + const frameLen = validateFrame(data); + + const seq = data[6]; + // Payload spans bytes 7 to frameLen-2 (inclusive), excluding final CRC byte + const payloadData = data.slice(7, frameLen - 1); + + if (payloadData.length < 2) { + throw new Error(`Protocol V2 payload too short (need >=2 bytes for messageTypeId)`); + } + + const messageTypeId = payloadData[0] + payloadData[1] * 256; + const pbPayload = payloadData.slice(2); + + return { messageTypeId, pbPayload, seq }; +} diff --git a/packages/hd-transport/src/protocols/v2/encode.ts b/packages/hd-transport/src/protocols/v2/encode.ts new file mode 100644 index 000000000..14d412046 --- /dev/null +++ b/packages/hd-transport/src/protocols/v2/encode.ts @@ -0,0 +1,90 @@ +import { PROTO_DATA_TYPE_PACKET, PROTO_HEAD_CRC_SIZE, PROTO_HEAD_SOF } from './constants'; +import { crc8 } from './crc8'; +import { PROTOCOL_V2_FRAME_MAX_BYTES } from '../../constants'; + +// Default sequence number when callers do not manage one themselves. +// Stateful per-session sequencing lives in ProtocolV2Session (session.ts), +// which advances the counter via nextProtoSeq() and passes it in explicitly. +const PROTO_SEQ_DEFAULT = 1; + +/** + * Advance a Protocol V2 sequence counter: 1-255, wraps around skipping 0. + */ +export function nextProtoSeq(current: number): number { + return current >= 255 ? 1 : current + 1; +} + +/** + * Build a raw Protocol V2 frame (0x5A framing). + * + * Frame layout (PROTO_HEAD_CRC_SIZE = 8 overhead bytes): + * [0] SOF = 0x5A + * [1] frameLen low byte + * [2] frameLen high byte + * [3] CRC8 of bytes 0-2 (pre-header CRC) + * [4] router + * [5] attr = ((packetSrc & 0x0F) << 2) | dataType + * [6] seq (1-255, wraps skipping 0) + * [7..N-2] payload + * [N-1] CRC8 of bytes 0 to N-2 (frame CRC) + */ +export function encodeFrame( + payload: Uint8Array | null, + packetSrc?: number, + router?: number, + seq?: number +): Uint8Array { + const resolvedPacketSrc = packetSrc ?? 0; + const resolvedRouter = router ?? 0; + const resolvedSeq = seq ?? PROTO_SEQ_DEFAULT; + if (!Number.isInteger(resolvedSeq) || resolvedSeq < 1 || resolvedSeq > 255) { + throw new Error(`Protocol V2 seq out of range (1-255): ${resolvedSeq}`); + } + const payloadLen = payload ? payload.length : 0; + const frameLen = payloadLen + PROTO_HEAD_CRC_SIZE; + if (frameLen > PROTOCOL_V2_FRAME_MAX_BYTES) { + throw new Error(`Protocol V2 frame too large: ${frameLen}`); + } + const frame = new Uint8Array(frameLen); + + frame[0] = PROTO_HEAD_SOF; + frame[1] = frameLen % 256; + frame[2] = Math.floor(frameLen / 256) % 256; + frame[3] = 0; // placeholder — filled in below + frame[4] = resolvedRouter % 256; + frame[5] = (resolvedPacketSrc % 16) * 4 + (PROTO_DATA_TYPE_PACKET % 4); + frame[6] = resolvedSeq; + + // CRC8 over first 3 bytes (SOF + length) + frame[3] = crc8(frame, 3); + + if (payload && payloadLen > 0) { + frame.set(payload, 7); + } + + // CRC8 over entire frame except last byte + frame[frameLen - 1] = crc8(frame, frameLen - 1); + + return frame; +} + +/** + * Build a Protocol V2 frame carrying a protobuf message. + * + * Payload layout: + * [0-1] messageTypeId as little-endian uint16 + * [2..] protobuf-encoded message bytes + */ +export function encodeProtobufFrame( + messageTypeId: number, + pbPayload: Uint8Array, + packetSrc?: number, + router?: number, + seq?: number +): Uint8Array { + const payload = new Uint8Array(2 + pbPayload.length); + payload[0] = messageTypeId % 256; + payload[1] = Math.floor(messageTypeId / 256) % 256; + payload.set(pbPayload, 2); + return encodeFrame(payload, packetSrc, router, seq); +} diff --git a/packages/hd-transport/src/protocols/v2/frame-assembler.ts b/packages/hd-transport/src/protocols/v2/frame-assembler.ts new file mode 100644 index 000000000..e9e764637 --- /dev/null +++ b/packages/hd-transport/src/protocols/v2/frame-assembler.ts @@ -0,0 +1,98 @@ +import { PROTOCOL_V2_FRAME_MAX_BYTES } from '../../constants'; +import { PROTO_HEAD_CRC_SIZE, PROTO_HEAD_SOF, PROTO_PRE_HEAD_SIZE } from './constants'; +import { crc8 } from './crc8'; + +export function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array { + const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0); + const result = new Uint8Array(totalLength); + let offset = 0; + for (const arr of arrays) { + result.set(arr, offset); + offset += arr.length; + } + return result; +} + +export class ProtocolV2FrameAssembler { + private buffer = new Uint8Array(0); + + private readonly maxFrameBytes: number; + + constructor(maxFrameBytes = PROTOCOL_V2_FRAME_MAX_BYTES) { + this.maxFrameBytes = maxFrameBytes; + } + + reset() { + this.buffer = new Uint8Array(0); + } + + push(chunk: Uint8Array): Uint8Array | undefined { + this.append(chunk); + return this.extractFrame(); + } + + /** + * Append a chunk (optional) and extract every complete frame currently + * buffered. Same validation/throw semantics as push(); push() stays + * backward compatible for callers that drain one frame at a time. + */ + drain(chunk: Uint8Array = new Uint8Array(0)): Uint8Array[] { + this.append(chunk); + const frames: Uint8Array[] = []; + for (;;) { + const frame = this.extractFrame(); + if (!frame) break; + frames.push(frame); + } + return frames; + } + + private append(chunk: Uint8Array) { + if (chunk.length > 0) { + this.buffer = concatUint8Arrays([this.buffer, chunk]); + } + } + + private extractFrame(): Uint8Array | undefined { + if (this.buffer.length < 3) return undefined; + + if (this.buffer[0] !== PROTO_HEAD_SOF) { + this.reset(); + throw new Error('Invalid Protocol V2 SOF'); + } + + const expectedLen = this.buffer[1] + this.buffer[2] * 256; + if (expectedLen < PROTO_HEAD_CRC_SIZE) { + // A declared length below the 8-byte frame overhead can never become a + // complete frame: without resetting, this poison prefix would stay in + // the buffer forever and deadlock the caller's drain loop. + this.reset(); + throw new Error(`Protocol V2 frame length too small: ${expectedLen}`); + } + if (expectedLen > this.maxFrameBytes) { + this.reset(); + throw new Error(`Protocol V2 frame too large: ${expectedLen}`); + } + + if (this.buffer.length < PROTO_PRE_HEAD_SIZE) return undefined; + + // Validate the pre-header CRC (byte 3 covers bytes 0-2) as soon as the + // first 4 bytes arrive, so a corrupted length field fails fast instead of + // waiting for bytes that will never come. + const expectedHeaderCrc = crc8(this.buffer, 3); + if (this.buffer[3] !== expectedHeaderCrc) { + this.reset(); + throw new Error( + `Protocol V2 header CRC mismatch: expected 0x${expectedHeaderCrc + .toString(16) + .padStart(2, '0')}` + ); + } + + if (this.buffer.length < expectedLen) return undefined; + + const frame = this.buffer.slice(0, expectedLen); + this.buffer = this.buffer.slice(expectedLen); + return frame; + } +} diff --git a/packages/hd-transport/src/protocols/v2/index.ts b/packages/hd-transport/src/protocols/v2/index.ts new file mode 100644 index 000000000..194b1c5be --- /dev/null +++ b/packages/hd-transport/src/protocols/v2/index.ts @@ -0,0 +1,5 @@ +export * from './constants'; +export * from './crc8'; +export * from './encode'; +export * from './decode'; +export * from './frame-assembler'; diff --git a/packages/hd-transport/src/protocols/v2/link-manager.ts b/packages/hd-transport/src/protocols/v2/link-manager.ts new file mode 100644 index 000000000..3abb035d0 --- /dev/null +++ b/packages/hd-transport/src/protocols/v2/link-manager.ts @@ -0,0 +1,160 @@ +import { ProtocolV2SequenceCursor } from './sequence-cursor'; +import { ProtocolV2Session, getErrorMessage } from './session'; + +import type { MessageFromOneKey, TransportCallOptions } from '../../types'; +import type { ProtocolV2CallContext, ProtocolV2Schemas, ProtocolV2SessionOptions } from './session'; + +export type ProtocolV2LinkErrorClassification = 'link-fatal' | 'recoverable'; + +export interface ProtocolV2LinkAdapter { + router: number; + maxFrameBytes?: number; + generation: number; + prepareCall(context: ProtocolV2CallContext): Promise | void; + writeFrame(frame: Uint8Array, context: ProtocolV2CallContext): Promise; + readFrame(context: ProtocolV2CallContext): Promise; + reset(reason: string): Promise | void; + logger?: ProtocolV2SessionOptions['logger']; + logPrefix?: string; + createTimeoutError?: ProtocolV2SessionOptions['createTimeoutError']; +} + +export type ProtocolV2LinkManagerOptions = { + getSchemas: () => ProtocolV2Schemas; + classifyError: (error: unknown) => ProtocolV2LinkErrorClassification; + onLinkInvalidated?: (key: Key, reason: string) => Promise | void; +}; + +type ProtocolV2Link = { + adapter: ProtocolV2LinkAdapter; + session: ProtocolV2Session; + state: { + invalidatedReason?: string; + }; +}; + +export class ProtocolV2LinkManager { + private readonly links = new Map(); + + private readonly sequences = new Map(); + + private readonly callQueues = new Map>(); + + private readonly options: ProtocolV2LinkManagerOptions; + + constructor(options: ProtocolV2LinkManagerOptions) { + this.options = options; + } + + call( + key: Key, + createAdapter: () => ProtocolV2LinkAdapter, + name: string, + data: Record, + options?: TransportCallOptions + ): Promise { + const run = () => this.executeCall(key, createAdapter, name, data, options); + const previous = this.callQueues.get(key) ?? Promise.resolve(); + const result = previous.then(run, run); + const queue = result.catch(() => undefined); + this.callQueues.set(key, queue); + result + .then( + () => this.clearSettledCallQueue(key, queue), + () => this.clearSettledCallQueue(key, queue) + ) + .catch(() => undefined); + return result; + } + + async invalidateLink(key: Key, reason: string): Promise { + const link = this.links.get(key); + if (!link) return; + + this.links.delete(key); + link.state.invalidatedReason = reason; + await link.adapter.reset(reason); + await this.options.onLinkInvalidated?.(key, reason); + } + + async invalidateAllLinks(reason: string): Promise { + await Promise.all(Array.from(this.links.keys(), key => this.invalidateLink(key, reason))); + } + + async dispose(reason: string): Promise { + await this.invalidateAllLinks(reason); + this.sequences.clear(); + this.callQueues.clear(); + } + + private getOrCreateLink(key: Key, createAdapter: () => ProtocolV2LinkAdapter): ProtocolV2Link { + const existing = this.links.get(key); + if (existing) return existing; + + const adapter = createAdapter(); + const state: ProtocolV2Link['state'] = {}; + const assertLinkActive = () => { + if (state.invalidatedReason) { + throw new Error(state.invalidatedReason); + } + }; + let sequenceCursor = this.sequences.get(key); + if (!sequenceCursor) { + sequenceCursor = new ProtocolV2SequenceCursor(); + this.sequences.set(key, sequenceCursor); + } + const session = new ProtocolV2Session({ + schemas: this.options.getSchemas(), + router: adapter.router, + maxFrameBytes: adapter.maxFrameBytes, + generation: adapter.generation, + sequenceCursor, + prepareCall: async context => { + assertLinkActive(); + await adapter.prepareCall(context); + assertLinkActive(); + }, + writeFrame: async (frame, context) => { + assertLinkActive(); + await adapter.writeFrame(frame, context); + assertLinkActive(); + }, + readFrame: async context => { + assertLinkActive(); + const frame = await adapter.readFrame(context); + assertLinkActive(); + return frame; + }, + logger: adapter.logger, + logPrefix: adapter.logPrefix, + createTimeoutError: adapter.createTimeoutError, + }); + const link = { adapter, session, state }; + this.links.set(key, link); + return link; + } + + private async executeCall( + key: Key, + createAdapter: () => ProtocolV2LinkAdapter, + name: string, + data: Record, + options?: TransportCallOptions + ): Promise { + try { + return await this.getOrCreateLink(key, createAdapter).session.call(name, data, options); + } catch (error) { + if (this.options.classifyError(error) === 'link-fatal') { + const errorMessage = getErrorMessage(error) || 'unknown error'; + await this.invalidateLink(key, `Protocol V2 link-fatal error: ${errorMessage}`); + } + throw error; + } + } + + private clearSettledCallQueue(key: Key, queue: Promise) { + if (this.callQueues.get(key) === queue) { + this.callQueues.delete(key); + } + } +} diff --git a/packages/hd-transport/src/protocols/v2/sequence-cursor.ts b/packages/hd-transport/src/protocols/v2/sequence-cursor.ts new file mode 100644 index 000000000..992ece49e --- /dev/null +++ b/packages/hd-transport/src/protocols/v2/sequence-cursor.ts @@ -0,0 +1,10 @@ +import { nextProtoSeq } from './encode'; + +export class ProtocolV2SequenceCursor { + private current = 0; + + next(): number { + this.current = nextProtoSeq(this.current); + return this.current; + } +} diff --git a/packages/hd-transport/src/protocols/v2/session.ts b/packages/hd-transport/src/protocols/v2/session.ts new file mode 100644 index 000000000..6d204aeaa --- /dev/null +++ b/packages/hd-transport/src/protocols/v2/session.ts @@ -0,0 +1,313 @@ +import { PROTOCOL_V2_PACKET_SRC_COMMAND } from '../../constants'; +import { ProtocolV2FrameAssembler, concatUint8Arrays } from './frame-assembler'; +import { ProtocolV2SequenceCursor } from './sequence-cursor'; +import { ProtocolV2 } from '..'; +import * as check from '../../utils/highlevel-checks'; +import { LogBlockCommand } from '../../utils/logBlockCommand'; + +import type { Root } from 'protobufjs/light'; +import type { MessageFromOneKey } from '../../types'; + +export type ProtocolV2Schemas = { + protocolV1: Root; + protocolV2: Root; +}; + +export type ProtocolV2CallContext = { + messageName: string; + timeoutMs?: number; + highVolume: boolean; + generation: number; +}; + +type ProtocolLogger = { + debug?: (...args: any[]) => void; + error?: (...args: any[]) => void; +}; + +export type ProtocolV2SessionOptions = { + schemas: ProtocolV2Schemas; + router: number; + packetSrc?: number; + maxFrameBytes?: number; + prepareCall?: (context: ProtocolV2CallContext) => Promise | void; + writeFrame: (frame: Uint8Array, context: ProtocolV2CallContext) => Promise; + readFrame: (context: ProtocolV2CallContext) => Promise; + logger?: ProtocolLogger; + logPrefix?: string; + createTimeoutError?: (name: string, timeoutMs: number) => Error; + sequenceCursor?: ProtocolV2SequenceCursor; + generation?: number; +}; + +export type ProtocolV2CallOptions = { + timeoutMs?: number; + expectedTypes?: string[]; + intermediateTypes?: string[]; + onIntermediateResponse?: (response: MessageFromOneKey) => void; +}; + +export { concatUint8Arrays, ProtocolV2FrameAssembler }; +export { ProtocolV2SequenceCursor }; + +export function hexToBytes(hex: string): Uint8Array { + const clean = hex.replace(/\s+/g, ''); + if (clean.length % 2 !== 0) { + throw new Error(`Invalid hex string: odd length ${clean.length}`); + } + if (!/^[0-9a-fA-F]*$/.test(clean)) { + throw new Error('Invalid hex string: contains non-hex characters'); + } + const bytes = new Uint8Array(clean.length / 2); + for (let i = 0; i < bytes.length; i += 1) { + bytes[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16); + } + return bytes; +} + +export function bytesToHex(bytes: Uint8Array): string { + return Array.from(bytes) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); +} + +const HIGH_VOLUME_PROTOCOL_V2_CALLS = new Set([ + ...LogBlockCommand, + 'FilesystemFileRead', + 'FileRead', + 'EmmcFileRead', +]); + +function shouldReduceProtocolV2Debug(name: string) { + return HIGH_VOLUME_PROTOCOL_V2_CALLS.has(name); +} + +const COMMON_TERMINAL_RESPONSE_TYPES = new Set([ + 'Failure', + 'ButtonRequest', + 'EntropyRequest', + 'PinMatrixRequest', + 'PassphraseRequest', + 'Deprecated_PassphraseStateRequest', + 'WordRequest', +]); + +function isExpectedTerminalResponse( + response: MessageFromOneKey, + expectedTypes: string[] | undefined +) { + if (!expectedTypes || expectedTypes.length === 0) return true; + return expectedTypes.includes(response.type) || COMMON_TERMINAL_RESPONSE_TYPES.has(response.type); +} + +export function getErrorMessage(error: unknown) { + if (!error) return ''; + if (typeof error === 'string') return error; + if (typeof error === 'object' && 'message' in error) { + const { message } = error as { message?: unknown }; + return typeof message === 'string' ? message : String(message ?? ''); + } + return String(error); +} + +export async function withProtocolTimeout( + promise: Promise, + timeoutMs: number | undefined, + createTimeoutError: () => Error, + onTimeout?: () => void +): Promise { + if (!timeoutMs) return promise; + + let timer: ReturnType | undefined; + try { + return await Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout(() => { + // Give the caller a chance to cancel the underlying work; a plain + // Promise.race rejection leaves the raced promise running. + onTimeout?.(); + reject(createTimeoutError()); + }, timeoutMs); + }), + ]); + } finally { + if (timer) clearTimeout(timer); + } +} + +// Write completion is owned by the concrete transport. A Promise.race watchdog +// here can return while libusb/WebUSB is still flushing a large Protocol V2 +// frame, which desynchronizes later request/response pairs. +export const PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS = 0; + +export class ProtocolV2Session { + private readonly options: ProtocolV2SessionOptions; + + private readonly sequenceCursor: ProtocolV2SequenceCursor; + + // Serializes call() invocations: responses are matched only by type, so two + // in-flight calls on the same session would steal each other's responses. + private pendingCall: Promise = Promise.resolve(); + + constructor(options: ProtocolV2SessionOptions) { + this.options = options; + this.sequenceCursor = options.sequenceCursor ?? new ProtocolV2SequenceCursor(); + } + + call( + name: string, + data: Record, + callOptions: ProtocolV2CallOptions = {} + ): Promise { + const run = () => this.executeCall(name, data, callOptions); + const result = this.pendingCall.then(run, run); + // Keep the chain alive even when a call fails; errors still propagate to + // the per-call promise returned below. + this.pendingCall = result.catch(() => undefined); + return result; + } + + private async executeCall( + name: string, + data: Record, + callOptions: ProtocolV2CallOptions + ): Promise { + const { + schemas, + router, + packetSrc = PROTOCOL_V2_PACKET_SRC_COMMAND, + maxFrameBytes, + prepareCall, + writeFrame, + readFrame, + logger, + logPrefix = 'ProtocolV2', + createTimeoutError, + generation = 0, + } = this.options; + + const shouldReduceDebug = shouldReduceProtocolV2Debug(name); + const callContext: ProtocolV2CallContext = { + messageName: name, + timeoutMs: callOptions.timeoutMs, + highVolume: shouldReduceDebug, + generation, + }; + await prepareCall?.(callContext); + const protoSeq = this.sequenceCursor.next(); + const frame = ProtocolV2.encodeFrame(schemas, name, data, { + packetSrc, + router, + seq: protoSeq, + }); + + if (maxFrameBytes !== undefined && frame.length > maxFrameBytes) { + throw new Error( + `Protocol V2 frame too large for transport: ${frame.length} > ${maxFrameBytes}` + ); + } + + // Lenient watchdog on the write phase only — see + // PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS for the rationale. + await withProtocolTimeout( + writeFrame(frame, callContext), + PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS, + () => + new Error( + `Protocol V2 write timeout after ${PROTOCOL_V2_WRITE_WATCHDOG_TIMEOUT_MS}ms for ${name}` + ) + ); + + // Cancellation flag for the read loop: when the response timeout fires, + // Promise.race alone would leave this loop running as a zombie that keeps + // consuming frames meant for the next call. The timeout callback flips the + // flag so the loop exits and discards any late frame. + const cancellation = { cancelled: false }; + + const readResponse = async (): Promise => { + // Some Protocol V2 operations emit progress notifications before the + // terminal response. Consume those frames here so callers still see a + // request/terminal-response shaped API. + while (!cancellation.cancelled) { + const rxFrame = await readFrame(callContext); + if (cancellation.cancelled) { + // Timed out while waiting: drop the late frame and stop reading. + break; + } + const isAck = ProtocolV2.isAckFrame(rxFrame); + if (!isAck) { + const decoded = ProtocolV2.decodeFrame(schemas, rxFrame); + + const response = check.call(decoded); + if (callOptions.intermediateTypes?.includes(response.type)) { + callOptions.onIntermediateResponse?.(response); + } else if (isExpectedTerminalResponse(response, callOptions.expectedTypes)) { + return response; + } else if (!shouldReduceDebug) { + logger?.debug?.( + `[${logPrefix}] skip unexpected response for ${name}: expected=${callOptions.expectedTypes?.join( + '|' + )} got=${response.type}` + ); + } + } + } + // Only reachable after cancellation; the outer promise has already been + // rejected by the timeout, so this rejection is consumed by the race. + throw new Error(`Protocol V2 read loop cancelled after timeout for ${name}`); + }; + + return withProtocolTimeout( + readResponse(), + callOptions.timeoutMs, + () => + createTimeoutError + ? createTimeoutError(name, callOptions.timeoutMs ?? 0) + : new Error(`Protocol V2 response timeout after ${callOptions.timeoutMs}ms for ${name}`), + () => { + cancellation.cancelled = true; + } + ); + } +} + +export async function probeProtocolV2({ + call, + timeoutMs, + logger, + logPrefix = 'ProtocolV2', + onBeforeProbe, + onProbeFailed, +}: { + call: ( + name: string, + data: Record, + options?: ProtocolV2CallOptions + ) => Promise; + timeoutMs: number; + logger?: ProtocolLogger; + logPrefix?: string; + onBeforeProbe?: () => Promise | void; + onProbeFailed?: (error: unknown) => Promise | void; +}) { + let probeError: unknown; + try { + await onBeforeProbe?.(); + const response = await call( + 'Ping', + { message: 'protocol-v2-probe' }, + { timeoutMs, expectedTypes: ['Success'] } + ); + if (response.type === 'Success') { + return true; + } + probeError = new Error(`unexpected response type ${response.type}`); + } catch (error) { + probeError = error; + } + + logger?.debug?.(`[${logPrefix}] Protocol V2 probe failed:`, getErrorMessage(probeError)); + await onProbeFailed?.(probeError); + return false; +} diff --git a/packages/hd-transport/src/protocols/v2/usb-transport-base.ts b/packages/hd-transport/src/protocols/v2/usb-transport-base.ts new file mode 100644 index 000000000..964864ddb --- /dev/null +++ b/packages/hd-transport/src/protocols/v2/usb-transport-base.ts @@ -0,0 +1,194 @@ +import { ProtocolV2FrameAssembler } from './frame-assembler'; +import { ProtocolV2LinkManager } from './link-manager'; + +import type { MessageFromOneKey, TransportCallOptions } from '../../types'; +import type { ProtocolV2CallContext, ProtocolV2Schemas, ProtocolV2SessionOptions } from './session'; + +export type ProtocolV2UsbTransportBaseOptions = { + router: number; + maxFrameBytes: number; + logPrefix: string; +}; + +type ProtocolV2UsbCancellation = { + generation: number; + reason?: string; + promise: Promise; + cancel: (reason: string) => void; +}; + +export abstract class ProtocolV2UsbTransportBase { + private readonly protocolV2UsbLinks: ProtocolV2LinkManager; + + private readonly protocolV2UsbAssemblers = new Map(); + + private readonly protocolV2UsbGenerations = new Map(); + + private readonly protocolV2UsbCancellations = new Map(); + + private readonly protocolV2UsbOptions: ProtocolV2UsbTransportBaseOptions; + + protected constructor(options: ProtocolV2UsbTransportBaseOptions) { + this.protocolV2UsbOptions = options; + this.protocolV2UsbLinks = new ProtocolV2LinkManager({ + getSchemas: () => this.getProtocolV2UsbSchemas(), + classifyError: () => 'link-fatal', + onLinkInvalidated: async (key, reason) => { + this.protocolV2UsbAssemblers.get(key)?.reset(); + await this.resetProtocolV2UsbNativeLink(key, reason); + await this.onProtocolV2UsbLinkInvalidated(key, reason); + }, + }); + } + + protected abstract getProtocolV2UsbSchemas(): ProtocolV2Schemas; + + protected abstract getProtocolV2UsbLogger(): ProtocolV2SessionOptions['logger']; + + protected abstract writeProtocolV2UsbPacket( + key: Key, + frame: Uint8Array, + context: ProtocolV2CallContext + ): Promise; + + protected abstract readProtocolV2UsbPacket( + key: Key, + context: ProtocolV2CallContext + ): Promise; + + protected abstract resetProtocolV2UsbNativeLink(key: Key, reason: string): Promise; + + protected abstract createProtocolV2UsbTimeoutError(name: string, timeoutMs: number): Error; + + protected onProtocolV2UsbLinkInvalidated(_key: Key, _reason: string): Promise | void { + // 子类按需清理协议缓存或记录平台日志。 + } + + protected async rotateProtocolV2UsbGeneration(key: Key, reason: string): Promise { + await this.protocolV2UsbLinks.invalidateLink(key, reason); + const generation = (this.protocolV2UsbGenerations.get(key) ?? 0) + 1; + this.protocolV2UsbGenerations.set(key, generation); + this.protocolV2UsbCancellations.set(key, this.createProtocolV2UsbCancellation(generation)); + this.protocolV2UsbAssemblers.set( + key, + new ProtocolV2FrameAssembler(this.protocolV2UsbOptions.maxFrameBytes) + ); + return generation; + } + + protected callProtocolV2Usb( + key: Key, + name: string, + data: Record, + options?: TransportCallOptions + ): Promise { + return this.protocolV2UsbLinks.call( + key, + () => this.createProtocolV2UsbAdapter(key), + name, + data, + options + ); + } + + protected invalidateProtocolV2UsbLink(key: Key, reason: string): Promise { + return this.protocolV2UsbLinks.invalidateLink(key, reason); + } + + protected invalidateAllProtocolV2UsbLinks(reason: string): Promise { + return this.protocolV2UsbLinks.invalidateAllLinks(reason); + } + + protected async disposeProtocolV2UsbLinks(reason: string): Promise { + await this.protocolV2UsbLinks.dispose(reason); + this.protocolV2UsbAssemblers.clear(); + this.protocolV2UsbGenerations.clear(); + this.protocolV2UsbCancellations.clear(); + } + + private createProtocolV2UsbAdapter(key: Key) { + const generation = this.protocolV2UsbGenerations.get(key); + if (generation === undefined) { + throw new Error('Protocol V2 USB generation has not been initialized'); + } + const cancellation = this.protocolV2UsbCancellations.get(key); + if (!cancellation || cancellation.generation !== generation) { + throw new Error('Protocol V2 USB cancellation state has not been initialized'); + } + + const assertCurrentGeneration = () => { + if (cancellation.reason) { + throw new Error(cancellation.reason); + } + if (this.protocolV2UsbGenerations.get(key) !== generation) { + throw new Error('Protocol V2 USB connection generation changed'); + } + }; + + return { + router: this.protocolV2UsbOptions.router, + maxFrameBytes: this.protocolV2UsbOptions.maxFrameBytes, + generation, + prepareCall: () => { + assertCurrentGeneration(); + this.getProtocolV2UsbAssembler(key).reset(); + }, + writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => { + assertCurrentGeneration(); + await this.writeProtocolV2UsbPacket(key, frame, context); + assertCurrentGeneration(); + }, + readFrame: async (context: ProtocolV2CallContext) => { + assertCurrentGeneration(); + const assembler = this.getProtocolV2UsbAssembler(key); + let frame = assembler.push(new Uint8Array(0)); + while (!frame) { + const packetRead = this.readProtocolV2UsbPacket(key, context).then(packet => ({ + packet, + })); + const cancelled = cancellation.promise.then(reason => ({ reason })); + const result = await Promise.race([packetRead, cancelled]); + if ('reason' in result) { + throw new Error(result.reason); + } + assertCurrentGeneration(); + frame = assembler.push(result.packet); + } + assertCurrentGeneration(); + return frame; + }, + reset: (reason: string) => { + cancellation.cancel(reason); + this.protocolV2UsbAssemblers.get(key)?.reset(); + }, + logger: this.getProtocolV2UsbLogger(), + logPrefix: this.protocolV2UsbOptions.logPrefix, + createTimeoutError: (messageName: string, timeoutMs: number) => + this.createProtocolV2UsbTimeoutError(messageName, timeoutMs), + }; + } + + private getProtocolV2UsbAssembler(key: Key): ProtocolV2FrameAssembler { + const assembler = this.protocolV2UsbAssemblers.get(key); + if (!assembler) { + throw new Error('Protocol V2 USB assembler has not been initialized'); + } + return assembler; + } + + private createProtocolV2UsbCancellation(generation: number): ProtocolV2UsbCancellation { + let resolveCancellation: (reason: string) => void = () => undefined; + const cancellation: ProtocolV2UsbCancellation = { + generation, + promise: new Promise(resolve => { + resolveCancellation = resolve; + }), + cancel: reason => { + if (cancellation.reason) return; + cancellation.reason = reason; + resolveCancellation(reason); + }, + }; + return cancellation; + } +} diff --git a/packages/hd-transport/src/serialization/index.ts b/packages/hd-transport/src/serialization/index.ts index e7f6ecea2..268e29867 100644 --- a/packages/hd-transport/src/serialization/index.ts +++ b/packages/hd-transport/src/serialization/index.ts @@ -1,8 +1,9 @@ import { parseConfigure } from './protobuf'; -export * from './send'; -export * from './receive'; - -export * as decodeProtocol from './protocol/decode'; - export { parseConfigure }; +export { createMessageFromName, createMessageFromType } from './protobuf/messages'; +export { encode as encodeProtobuf } from './protobuf/encode'; +export { decode as decodeProtobuf } from './protobuf/decode'; +export { PROTOCOL_V2_SYS_MESSAGE_THRESHOLD, ProtocolV1, ProtocolV2 } from '../protocols'; +export * as protocolV1 from '../protocols/v1'; +export * as protocolV2 from '../protocols/v2'; diff --git a/packages/hd-transport/src/serialization/protobuf/decode.ts b/packages/hd-transport/src/serialization/protobuf/decode.ts index a372fede8..eefa963b8 100644 --- a/packages/hd-transport/src/serialization/protobuf/decode.ts +++ b/packages/hd-transport/src/serialization/protobuf/decode.ts @@ -40,6 +40,13 @@ function messageToJSON(Message: Message>, fields: Type[' // @ts-ignore const value = message[key]; + if (value == null) { + if (field.optional) { + res[key] = null; + } + return; + } + /* istanbul ignore else */ if (field.repeated) { /* istanbul ignore else */ diff --git a/packages/hd-transport/src/serialization/protobuf/messages.ts b/packages/hd-transport/src/serialization/protobuf/messages.ts index e757d11e8..ed043fdcc 100644 --- a/packages/hd-transport/src/serialization/protobuf/messages.ts +++ b/packages/hd-transport/src/serialization/protobuf/messages.ts @@ -11,22 +11,31 @@ export function parseConfigure(data: protobuf.INamespace) { export const createMessageFromName = (messages: protobuf.Root, name: string) => { const Message = messages.lookupType(name); const MessageType = messages.lookupEnum('MessageType'); - let messageType = MessageType.values[`MessageType_${name}`]; + let messageTypeId = MessageType.values[`MessageType_${name}`]; - if (!messageType && Message.options) { - messageType = Message.options['(wire_type)']; + if (messageTypeId == null && Message.options) { + messageTypeId = Message.options['(wire_type)']; + } + + if (!Number.isInteger(messageTypeId)) { + throw new Error(`MessageType for "${name}" is not defined in protobuf schema`); } return { Message, - messageType, + messageTypeId, }; }; export const createMessageFromType = (messages: protobuf.Root, typeId: number) => { const MessageType = messages.lookupEnum('MessageType'); - const messageName = MessageType.valuesById[typeId].replace('MessageType_', ''); + const rawMessageName = MessageType.valuesById[typeId]; + if (!rawMessageName) { + throw new Error(`MessageType id "${typeId}" is not defined in protobuf schema`); + } + + const messageName = rawMessageName.replace('MessageType_', ''); const Message = messages.lookupType(messageName); diff --git a/packages/hd-transport/src/serialization/protocol/index.ts b/packages/hd-transport/src/serialization/protocol/index.ts deleted file mode 100644 index 6267e16e3..000000000 --- a/packages/hd-transport/src/serialization/protocol/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './decode'; -export * from './encode'; diff --git a/packages/hd-transport/src/serialization/send.ts b/packages/hd-transport/src/serialization/send.ts deleted file mode 100644 index f271e9be8..000000000 --- a/packages/hd-transport/src/serialization/send.ts +++ /dev/null @@ -1,58 +0,0 @@ -// Logic of sending data to trezor -// -// Logic of "call" is broken to two parts - sending and receiving -import ByteBuffer from 'bytebuffer'; - -import { encode as encodeProtobuf } from './protobuf'; -import { encode as encodeProtocol } from './protocol'; -import { createMessageFromName } from './protobuf/messages'; -import { BUFFER_SIZE, MESSAGE_TOP_CHAR } from '../constants'; - -import type { Root } from 'protobufjs/light'; - -// Sends message to device. -// Resolves if everything gets sent -export function buildOne(messages: Root, name: string, data: Record) { - const { Message, messageType } = createMessageFromName(messages, name); - - const buffer = encodeProtobuf(Message, data); - return encodeProtocol(buffer, { - addTrezorHeaders: false, - chunked: false, - messageType, - }); -} - -export const buildEncodeBuffers = (messages: Root, name: string, data: Record) => { - const { Message, messageType } = createMessageFromName(messages, name); - const buffer = encodeProtobuf(Message, data); - return encodeProtocol(buffer, { - addTrezorHeaders: true, - chunked: true, - messageType, - }); -}; - -export const buildBuffers = (messages: Root, name: string, data: Record) => { - // const { Message, messageType } = createMessageFromName(messages, name); - // const buffer = encodeProtobuf(Message, data); - // const encodeBuffers = encodeProtocol(buffer, { - // addTrezorHeaders: true, - // chunked: true, - // messageType, - // }); - - const encodeBuffers = buildEncodeBuffers(messages, name, data); - - const outBuffers: ByteBuffer[] = []; - - for (const buf of encodeBuffers) { - const chunkBuffer = new ByteBuffer(BUFFER_SIZE + 1); - chunkBuffer.writeByte(MESSAGE_TOP_CHAR); - chunkBuffer.append(buf); - chunkBuffer.reset(); - outBuffers.push(chunkBuffer); - } - - return outBuffers; -}; diff --git a/packages/hd-transport/src/types/messages.ts b/packages/hd-transport/src/types/messages.ts index 2fce7dec7..e02f6fa40 100644 --- a/packages/hd-transport/src/types/messages.ts +++ b/packages/hd-transport/src/types/messages.ts @@ -379,6 +379,7 @@ export type GetAddress = { // Address export type Address = { address: string; + mac?: string; }; // GetOwnershipId @@ -418,6 +419,14 @@ export type VerifyMessage = { coin_name?: string; }; +export type CoinJoinRequest = { + fee_rate: number; + no_fee_threshold: number; + min_registrable_amount: number; + mask_public_key: string; + signature: string; +}; + // SignTx export type SignTx = { outputs_count: number; @@ -432,6 +441,8 @@ export type SignTx = { branch_id?: number; amount_unit?: AmountUnit; decred_staking_ticket?: boolean; + serialize?: boolean; + coinjoin_request?: CoinJoinRequest; }; export enum Enum_RequestType { @@ -442,6 +453,7 @@ export enum Enum_RequestType { TXEXTRADATA = 4, TXORIGINPUT = 5, TXORIGOUTPUT = 6, + TXPAYMENTREQ = 7, } export type RequestType = keyof typeof Enum_RequestType; @@ -486,6 +498,7 @@ type CommonTxInputType = { witness?: string; // used by EXTERNAL, depending on script_pubkey ownership_proof?: string; // used by EXTERNAL, depending on script_pubkey commitment_data?: string; // used by EXTERNAL, depending on ownership_proof + coinjoin_flags?: number; // Protocol V2 only (CoinJoin) }; export type TxInputType = @@ -688,12 +701,15 @@ export type OwnershipProof = { // AuthorizeCoinJoin export type AuthorizeCoinJoin = { coordinator: string; - max_total_fee: number; + max_total_fee?: number; fee_per_anonymity?: number; address_n: number[]; coin_name?: string; script_type?: InputScriptType; amount_unit?: AmountUnit; + max_rounds?: number; + max_coordinator_fee_rate?: number; + max_fee_per_kvbyte?: number; }; export type BIP32Address = { @@ -1177,12 +1193,16 @@ export enum FailureType { Failure_WipeCodeMismatch = 13, Failure_InvalidSession = 14, Failure_FirmwareError = 99, + Failure_InvalidMessage = 1, + Failure_UndefinedError = 2, + Failure_UsageError = 3, } // Failure export type Failure = { code?: FailureType; message?: string; + subcode?: number; }; export enum Enum_ButtonRequestType { @@ -2230,6 +2250,9 @@ export enum Enum_BackupType { Bip39 = 0, Slip39_Basic = 1, Slip39_Advanced = 2, + Slip39_Single_Extendable = 3, + Slip39_Basic_Extendable = 4, + Slip39_Advanced_Extendable = 5, } export type BackupType = keyof typeof Enum_BackupType; @@ -2389,6 +2412,19 @@ export type Features = { onekey_se04_state?: string | null; attach_to_pin_user?: boolean; unlocked_attach_pin?: boolean; + coprocessor_bt_name?: string; + coprocessor_version?: string; + coprocessor_bt_enable?: boolean; + romloader_version?: string; + onekey_romloader_version?: string; + onekey_romloader_hash?: string; + onekey_bootloader_version?: string; + onekey_bootloader_hash?: string; + onekey_bootloader_build_id?: string; + onekey_coprocessor_bt_name?: string; + onekey_coprocessor_version?: string; + onekey_coprocessor_build_id?: string; + onekey_coprocessor_hash?: string; }; // OnekeyFeatures @@ -2437,6 +2473,28 @@ export type OnekeyFeatures = { onekey_se02_boot_build_id?: string; onekey_se03_boot_build_id?: string; onekey_se04_boot_build_id?: string; + onekey_romloader_version?: string; + onekey_bootloader_version?: string; + onekey_romloader_hash?: string; + onekey_bootloader_hash?: string; + onekey_romloader_build_id?: string; + onekey_bootloader_build_id?: string; + onekey_coprocessor_bt_name?: string; + onekey_coprocessor_version?: string; + onekey_coprocessor_build_id?: string; + onekey_coprocessor_hash?: string; + onekey_se01_bootloader_version?: string; + onekey_se02_bootloader_version?: string; + onekey_se03_bootloader_version?: string; + onekey_se04_bootloader_version?: string; + onekey_se01_bootloader_hash?: string; + onekey_se02_bootloader_hash?: string; + onekey_se03_bootloader_hash?: string; + onekey_se04_bootloader_hash?: string; + onekey_se01_bootloader_build_id?: string; + onekey_se02_bootloader_build_id?: string; + onekey_se03_bootloader_build_id?: string; + onekey_se04_bootloader_build_id?: string; }; // LockDevice @@ -2935,7 +2993,7 @@ export type MoneroTransactionRsigData = { export type MoneroGetAddress = { address_n: number[]; show_display?: boolean; - network_type?: number; + network_type?: number | MoneroNetworkType; account?: number; minor?: number; payment_id?: string; @@ -2949,7 +3007,7 @@ export type MoneroAddress = { // MoneroGetWatchKey export type MoneroGetWatchKey = { address_n: number[]; - network_type?: number; + network_type?: number | MoneroNetworkType; }; // MoneroWatchKey @@ -2980,7 +3038,7 @@ export type MoneroTransactionData = { export type MoneroTransactionInitRequest = { version?: number; address_n: number[]; - network_type?: number; + network_type?: number | MoneroNetworkType; tsx_data?: MoneroTransactionData; }; @@ -3110,7 +3168,7 @@ export type MoneroKeyImageExportInitRequest = { num?: number; hash?: string; address_n: number[]; - network_type?: number; + network_type?: number | MoneroNetworkType; subs: MoneroSubAddressIndicesList[]; }; @@ -3152,7 +3210,7 @@ export type MoneroKeyImageSyncFinalAck = { // MoneroGetTxKeyRequest export type MoneroGetTxKeyRequest = { address_n: number[]; - network_type?: number; + network_type?: number | MoneroNetworkType; salt1?: string; salt2?: string; tx_enc_keys?: string; @@ -3171,7 +3229,7 @@ export type MoneroGetTxKeyAck = { // MoneroLiveRefreshStartRequest export type MoneroLiveRefreshStartRequest = { address_n: number[]; - network_type?: number; + network_type?: number | MoneroNetworkType; }; // MoneroLiveRefreshStartAck @@ -4199,6 +4257,7 @@ export type TonSignedMessage = { signature?: string; signning_message?: string; init_data_length?: number; + signing_message?: string; }; // TonSignProof @@ -4393,6 +4452,647 @@ export enum CommandFlags { Factory_Only = 1, } +// experimental_message +export type experimental_message = {}; + +// experimental_field +export type experimental_field = {}; + +export type TextMemo = { + text: string; +}; + +export type RefundMemo = { + address: string; + mac: string; +}; + +export type CoinPurchaseMemo = { + coin_type: number; + amount: UintType; + address: string; + mac: string; +}; + +export type PaymentRequestMemo = { + text_memo?: TextMemo; + refund_memo?: RefundMemo; + coin_purchase_memo?: CoinPurchaseMemo; +}; + +// TxAckPaymentRequest +export type TxAckPaymentRequest = { + nonce?: string; + recipient_name: string; + memos?: PaymentRequestMemo[]; + amount?: UintType; + signature: string; +}; + +// EthereumSignTypedDataQR +export type EthereumSignTypedDataQR = { + address_n: number[]; + json_data?: string; + chain_id?: number; + metamask_v4_compat?: boolean; + request_id?: string; +}; + +// InternalMyAddressRequest +export type InternalMyAddressRequest = { + coin_type: number; + chain_id: number; + account_index: number; + derive_type: number; +}; + +// StartSession +export type StartSession = { + session_id?: string; + _skip_passphrase?: boolean; + derive_cardano?: boolean; +}; + +// SetBusy +export type SetBusy = { + expiry_ms?: number; +}; + +// GetFirmwareHash +export type GetFirmwareHash = { + challenge?: string; +}; + +// FirmwareHash +export type FirmwareHash = { + hash: string; +}; + +// GetNonce +export type GetNonce = {}; + +// Nonce +export type Nonce = { + nonce: string; +}; + +// WriteSEPrivateKey +export type WriteSEPrivateKey = { + private_key: string; +}; + +export enum WallpaperTarget { + Home = 0, + Lock = 1, +} + +// SetWallpaper +export type SetWallpaper = { + target: WallpaperTarget; + path: string; +}; + +// GetWallpaper +export type GetWallpaper = { + target: WallpaperTarget; +}; + +// Wallpaper +export type Wallpaper = { + target: WallpaperTarget; + path: string; +}; + +// UnlockPath +export type UnlockPath = { + address_n: number[]; + mac?: string; +}; + +// UnlockedPathRequest +export type UnlockedPathRequest = { + mac?: string; +}; + +export enum MoneroNetworkType { + MAINNET = 0, + TESTNET = 1, + STAGENET = 2, + FAKECHAIN = 3, +} + +// ViewAmount +export type ViewAmount = { + is_unlimited: boolean; + num: string; +}; + +// ViewDetail +export type ViewDetail = { + key: number; + value: string; + is_overview: boolean; + has_icon: boolean; +}; + +export enum ViewTipType { + Default = 0, + Highlight = 1, + Recommend = 2, + Warning = 3, + Danger = 4, +} + +// ViewTip +export type ViewTip = { + type: ViewTipType; + text: string; +}; + +// ViewRawData +export type ViewRawData = { + initial_data: string; + placeholder: number; +}; + +export enum ViewSignLayout { + LayoutDefault = 0, + LayoutSafeTxCreate = 1, + LayoutFinalConfirm = 2, + Layout7702 = 3, + LayoutFlat = 4, +} + +// ViewSignPage +export type ViewSignPage = { + title: string; + amount?: UintType; + general: ViewDetail[]; + tip?: ViewTip; + raw_data?: ViewRawData; + slide_to_confirm?: boolean; + layout?: ViewSignLayout; +}; + +// ViewVerifyPage +export type ViewVerifyPage = { + title: string; + address: string; + path: string; +}; + +// ProtocolInfoRequest +export type ProtocolInfoRequest = {}; + +// ProtocolInfo +export type ProtocolInfo = { + version: number; + supported_messages: number[]; + protobuf_definition?: string; +}; + +export enum DeviceErrorCode { + DeviceError_None = 0, + DeviceError_Busy = 1, + DeviceError_NotInitialized = 2, + DeviceError_ActionCancelled = 3, + DeviceError_PinAlreadyUsed = 4, + DeviceError_PersistFailed = 5, + DeviceError_SeError = 6, + DeviceError_InvalidLanguage = 7, + DeviceError_WallpaperNotUsable = 8, + DeviceError_DeviceLocked = 9, +} + +export enum DeviceRebootType { + Normal = 0, + Romloader = 1, + Bootloader = 2, +} + +// DeviceReboot +export type DeviceReboot = { + reboot_type: DeviceRebootType; +}; + +// DeviceSettings +export type DeviceSettings = { + label?: string; + bt_enable?: boolean; + language?: string; + wallpaper_path?: string; + passphrase_enable?: boolean; + brightness?: number; + autolock_delay_ms?: number; + autoshutdown_delay_ms?: number; + animation_enable?: boolean; + tap_to_wake?: boolean; + haptic_feedback?: boolean; + device_name_display_enabled?: boolean; + airgap_mode?: boolean; + fido_enabled?: boolean; + experimental_features?: boolean; + usb_lock_enable?: boolean; + random_keypad?: boolean; +}; + +// DeviceSettingsGet +export type DeviceSettingsGet = {}; + +// DeviceSettingsSet +export type DeviceSettingsSet = { + settings: DeviceSettings; +}; + +export enum DeviceSettingsPage { + DeviceReset = 0, + DevicePinChange = 1, + DevicePassphrase = 2, + DeviceAirgap = 3, +} + +// DeviceSettingsPageShow +export type DeviceSettingsPageShow = { + page: DeviceSettingsPage; + field_name?: string; +}; + +// DeviceCertificate +export type DeviceCertificate = { + cert_and_pubkey: string; + private_key?: string; +}; + +// DeviceCertificateWrite +export type DeviceCertificateWrite = { + cert: DeviceCertificate; +}; + +// DeviceCertificateRead +export type DeviceCertificateRead = {}; + +// DeviceCertificateSignature +export type DeviceCertificateSignature = { + data: string; +}; + +// DeviceCertificateSign +export type DeviceCertificateSign = { + data: string; +}; + +export enum DeviceFirmwareTargetType { + FW_MGMT_TARGET_INVALID = 0, + FW_MGMT_TARGET_CRATE = 1, + FW_MGMT_TARGET_ROMLOADER = 2, + FW_MGMT_TARGET_BOOTLOADER = 3, + FW_MGMT_TARGET_APPLICATION_P1 = 4, + FW_MGMT_TARGET_APPLICATION_P2 = 5, + FW_MGMT_TARGET_COPROCESSOR = 6, + FW_MGMT_TARGET_SE01 = 7, + FW_MGMT_TARGET_SE02 = 8, + FW_MGMT_TARGET_SE03 = 9, + FW_MGMT_TARGET_SE04 = 10, +} + +export enum DeviceFirmwareUpdateTaskStatus { + FW_MGMT_UPDATER_TASK_STATUS_PENDING = 0, + FW_MGMT_UPDATER_TASK_STATUS_IN_PROGRESS = 1, + FW_MGMT_UPDATER_TASK_STATUS_FINISHED = 2, + FW_MGMT_UPDATER_TASK_STATUS_FAILED_FILE_NOT_FOUND = 3, + FW_MGMT_UPDATER_TASK_STATUS_FAILED_FILE_READ = 4, + FW_MGMT_UPDATER_TASK_STATUS_FAILED_FILE_WRITE = 5, + FW_MGMT_UPDATER_TASK_STATUS_FAILED_VERIFY = 6, + FW_MGMT_UPDATER_TASK_STATUS_FAILED_INSTALL = 7, + FW_MGMT_UPDATER_TASK_STATUS_FAILED_ABORT = 8, + FW_MGMT_UPDATER_TASK_STATUS_FAILED_BUSY = 9, + FW_MGMT_UPDATER_TASK_STATUS_FAILED_ENTRY_OUT_OF_BOUNDS = 10, +} + +// DeviceFirmwareTarget +export type DeviceFirmwareTarget = { + target_id: DeviceFirmwareTargetType; + path: string; +}; + +// DeviceFirmwareUpdateRequest +export type DeviceFirmwareUpdateRequest = { + targets: DeviceFirmwareTarget[]; +}; + +// DeviceFirmwareUpdateRecord +export type DeviceFirmwareUpdateRecord = { + target_id: DeviceFirmwareTargetType; + status?: DeviceFirmwareUpdateTaskStatus; + payload_version?: number; + path?: string; +}; + +// DeviceFirmwareUpdateRecordFields +export type DeviceFirmwareUpdateRecordFields = { + status?: boolean; + payload_version?: boolean; + path?: boolean; +}; + +// DeviceFirmwareUpdateStatusGet +export type DeviceFirmwareUpdateStatusGet = { + fields?: DeviceFirmwareUpdateRecordFields; +}; + +// DeviceFirmwareUpdateStatus +export type DeviceFirmwareUpdateStatus = { + records: DeviceFirmwareUpdateRecord[]; +}; + +export enum DeviceFactoryAck { + FACTORY_ACK_SUCCESS = 0, + FACTORY_ACK_FAIL = 1, +} + +// DeviceFactoryInfoManufactureTime +export type DeviceFactoryInfoManufactureTime = { + year: number; + month: number; + day: number; + hour: number; + minute: number; + second: number; +}; + +// DeviceFactoryInfo +export type DeviceFactoryInfo = { + version?: number; + serial_number?: string; + burn_in_completed?: boolean; + factory_test_completed?: boolean; + manufacture_time?: DeviceFactoryInfoManufactureTime; +}; + +// DeviceFactoryInfoSet +export type DeviceFactoryInfoSet = { + info: DeviceFactoryInfo; +}; + +// DeviceFactoryInfoGet +export type DeviceFactoryInfoGet = {}; + +// DeviceFactoryPermanentLock +export type DeviceFactoryPermanentLock = { + check_a: string; + check_b: string; +}; + +// DeviceFactoryTest +export type DeviceFactoryTest = { + burn_in_test: boolean; +}; + +export enum DeviceType { + CLASSIC1 = 0, + CLASSIC1S = 1, + MINI = 2, + TOUCH = 3, + PRO = 5, + CLASSIC1S_PURE = 6, + PRO2 = 7, + NEO = 8, +} + +export enum DeviceSeType { + THD89 = 0, + SE608A = 1, +} + +export enum DeviceSEState { + BOOT = 0, + APP_FACTORY = 51, + APP = 85, +} + +// DeviceFirmwareImageInfo +export type DeviceFirmwareImageInfo = { + version?: string; + build_id?: string; + hash?: string; +}; + +// DeviceHardwareInfo +export type DeviceHardwareInfo = { + Device_type?: DeviceType; + serial_no?: string; + hardware_version?: string; + hardware_version_raw_adc?: number; +}; + +// DeviceMainMcuInfo +export type DeviceMainMcuInfo = { + romloader?: DeviceFirmwareImageInfo; + bootloader?: DeviceFirmwareImageInfo; + application?: DeviceFirmwareImageInfo; + application_data?: DeviceFirmwareImageInfo; +}; + +// DeviceCoprocessorInfo +export type DeviceCoprocessorInfo = { + bootloader?: DeviceFirmwareImageInfo; + application?: DeviceFirmwareImageInfo; + bt_adv_name?: string; + bt_mac?: string; +}; + +// DeviceSEInfo +export type DeviceSEInfo = { + bootloader?: DeviceFirmwareImageInfo; + application?: DeviceFirmwareImageInfo; + type?: DeviceSeType; + state?: DeviceSEState; +}; + +// DeviceInfoTargets +export type DeviceInfoTargets = { + hw?: boolean; + fw?: boolean; + coprocessor?: boolean; + se1?: boolean; + se2?: boolean; + se3?: boolean; + se4?: boolean; + status?: boolean; +}; + +// DeviceInfoTypes +export type DeviceInfoTypes = { + version?: boolean; + build_id?: boolean; + hash?: boolean; + specific?: boolean; +}; + +// DeviceInfoGet +export type DeviceInfoGet = { + targets?: DeviceInfoTargets; + types?: DeviceInfoTypes; +}; + +// ProtocolV2DeviceInfo +export type ProtocolV2DeviceInfo = { + protocol_version: number; + hw?: DeviceHardwareInfo; + fw?: DeviceMainMcuInfo; + coprocessor?: DeviceCoprocessorInfo; + se1?: DeviceSEInfo; + se2?: DeviceSEInfo; + se3?: DeviceSEInfo; + se4?: DeviceSEInfo; + status?: DeviceStatus; +}; + +// DeviceSessionGet +export type DeviceSessionGet = { + session_id?: string; +}; + +// DeviceSession +export type DeviceSession = { + session_id?: string; + btc_test_address?: string; +}; + +// DeviceSessionAskPin +export type DeviceSessionAskPin = {}; + +export enum DeviceSessionAskPin_FailureSubCodes { + UserCancel = 1, +} + +// DeviceStatus +export type DeviceStatus = { + device_id?: string; + unlocked?: boolean; + init_states?: boolean; + backup_required?: boolean; + passphrase_enabled?: boolean; + attach_to_pin_enabled?: boolean; + unlocked_by_attach_to_pin?: boolean; +}; + +// DeviceStatusGet +export type DeviceStatusGet = {}; + +export enum DevOnboardingStage { + DEV_ONBOARDING_STAGE_UNKNOWN = 0, + DEV_ONBOARDING_STAGE_SAFETY_CHECK = 1, + DEV_ONBOARDING_STAGE_PERSONALIZATION = 2, + DEV_ONBOARDING_STAGE_SELECT_SETUP_METHOD = 3, + DEV_ONBOARDING_STAGE_NEW_DEVICE = 4, + DEV_ONBOARDING_STAGE_SELECT_RESTORE_METHOD = 5, + DEV_ONBOARDING_STAGE_RESTORE_MNEMONIC = 6, + DEV_ONBOARDING_STAGE_RESTORE_SEEDCARD = 7, + DEV_ONBOARDING_STAGE_WALLET_READY = 8, + DEV_ONBOARDING_STAGE_SEEDCARD_BACKUP_PROMPT = 9, + DEV_ONBOARDING_STAGE_SELECT_SEEDCARD_BACKUP_METHOD = 10, + DEV_ONBOARDING_STAGE_SEEDCARD_BACKUP = 11, + DEV_ONBOARDING_STAGE_DONE = 12, +} + +// DevGetOnboardingStatus +export type DevGetOnboardingStatus = {}; + +// DevOnboardingStatus +export type DevOnboardingStatus = { + stage: DevOnboardingStage; + status_code?: number; + detail_code?: number; +}; + +// FilesystemPermissionFix +export type FilesystemPermissionFix = {}; + +// FilesystemPathInfo +export type FilesystemPathInfo = { + exist: boolean; + size: number; + year: number; + month: number; + day: number; + hour: number; + minute: number; + second: number; + readonly: boolean; + hidden: boolean; + system: boolean; + archive: boolean; + directory: boolean; +}; + +// FilesystemPathInfoQuery +export type FilesystemPathInfoQuery = { + path: string; +}; + +// FilesystemFile +export type FilesystemFile = { + path: string; + offset: number; + total_size: number; + data?: Buffer | ArrayBuffer | Uint8Array | string; + data_hash?: number; + processed_byte?: number; +}; + +// FilesystemFileRead +export type FilesystemFileRead = { + file: FilesystemFile; + chunk_len?: number; + ui_percentage?: number; +}; + +// FilesystemFileWrite +export type FilesystemFileWrite = { + file: FilesystemFile; + overwrite: boolean; + append: boolean; + ui_percentage?: number; +}; + +// FilesystemFileDelete +export type FilesystemFileDelete = { + path: string; +}; + +// FilesystemDir +export type FilesystemDir = { + path: string; + child_dirs?: string; + child_files?: string; +}; + +// FilesystemDirList +export type FilesystemDirList = { + path: string; + depth?: number; +}; + +// FilesystemDirMake +export type FilesystemDirMake = { + path: string; +}; + +// FilesystemDirRemove +export type FilesystemDirRemove = { + path: string; +}; + +// FilesystemFormat +export type FilesystemFormat = { + data: boolean; + user: boolean; +}; + +// PortfolioUpdate +export type PortfolioUpdate = {}; + // custom connect definitions export type MessageType = { AlephiumGetAddress: AlephiumGetAddress; @@ -4449,6 +5149,7 @@ export type MessageType = { SignMessage: SignMessage; MessageSignature: MessageSignature; VerifyMessage: VerifyMessage; + CoinJoinRequest: CoinJoinRequest; SignTx: SignTx; TxRequestDetailsType: TxRequestDetailsType; TxRequestSerializedType: TxRequestSerializedType; @@ -4717,7 +5418,7 @@ export type MessageType = { BixinBackupDeviceAck: BixinBackupDeviceAck; DeviceInfoSettings: DeviceInfoSettings; GetDeviceInfo: GetDeviceInfo; - DeviceInfo: DeviceInfo; + DeviceInfo: DeviceInfo | ProtocolV2DeviceInfo; ReadSEPublicKey: ReadSEPublicKey; SEPublicKey: SEPublicKey; WriteSEPublicCert: WriteSEPublicCert; @@ -4949,6 +5650,85 @@ export type MessageType = { TronSignMessage: TronSignMessage; TronMessageSignature: TronMessageSignature; facotry: facotry; + experimental_message: experimental_message; + experimental_field: experimental_field; + TextMemo: TextMemo; + RefundMemo: RefundMemo; + CoinPurchaseMemo: CoinPurchaseMemo; + PaymentRequestMemo: PaymentRequestMemo; + TxAckPaymentRequest: TxAckPaymentRequest; + EthereumSignTypedDataQR: EthereumSignTypedDataQR; + InternalMyAddressRequest: InternalMyAddressRequest; + StartSession: StartSession; + SetBusy: SetBusy; + GetFirmwareHash: GetFirmwareHash; + FirmwareHash: FirmwareHash; + GetNonce: GetNonce; + Nonce: Nonce; + WriteSEPrivateKey: WriteSEPrivateKey; + SetWallpaper: SetWallpaper; + GetWallpaper: GetWallpaper; + Wallpaper: Wallpaper; + UnlockPath: UnlockPath; + UnlockedPathRequest: UnlockedPathRequest; + ViewAmount: ViewAmount; + ViewDetail: ViewDetail; + ViewTip: ViewTip; + ViewRawData: ViewRawData; + ViewSignPage: ViewSignPage; + ViewVerifyPage: ViewVerifyPage; + ProtocolInfoRequest: ProtocolInfoRequest; + ProtocolInfo: ProtocolInfo; + DeviceReboot: DeviceReboot; + DeviceSettings: DeviceSettings; + DeviceSettingsGet: DeviceSettingsGet; + DeviceSettingsSet: DeviceSettingsSet; + DeviceSettingsPageShow: DeviceSettingsPageShow; + DeviceCertificate: DeviceCertificate; + DeviceCertificateWrite: DeviceCertificateWrite; + DeviceCertificateRead: DeviceCertificateRead; + DeviceCertificateSignature: DeviceCertificateSignature; + DeviceCertificateSign: DeviceCertificateSign; + DeviceFirmwareTarget: DeviceFirmwareTarget; + DeviceFirmwareUpdateRequest: DeviceFirmwareUpdateRequest; + DeviceFirmwareUpdateRecord: DeviceFirmwareUpdateRecord; + DeviceFirmwareUpdateRecordFields: DeviceFirmwareUpdateRecordFields; + DeviceFirmwareUpdateStatusGet: DeviceFirmwareUpdateStatusGet; + DeviceFirmwareUpdateStatus: DeviceFirmwareUpdateStatus; + DeviceFactoryInfoManufactureTime: DeviceFactoryInfoManufactureTime; + DeviceFactoryInfo: DeviceFactoryInfo; + DeviceFactoryInfoSet: DeviceFactoryInfoSet; + DeviceFactoryInfoGet: DeviceFactoryInfoGet; + DeviceFactoryPermanentLock: DeviceFactoryPermanentLock; + DeviceFactoryTest: DeviceFactoryTest; + DeviceFirmwareImageInfo: DeviceFirmwareImageInfo; + DeviceHardwareInfo: DeviceHardwareInfo; + DeviceMainMcuInfo: DeviceMainMcuInfo; + DeviceCoprocessorInfo: DeviceCoprocessorInfo; + DeviceSEInfo: DeviceSEInfo; + DeviceInfoTargets: DeviceInfoTargets; + DeviceInfoTypes: DeviceInfoTypes; + DeviceInfoGet: DeviceInfoGet; + DeviceSessionGet: DeviceSessionGet; + DeviceSession: DeviceSession; + DeviceSessionAskPin: DeviceSessionAskPin; + DeviceStatus: DeviceStatus; + DeviceStatusGet: DeviceStatusGet; + DevGetOnboardingStatus: DevGetOnboardingStatus; + DevOnboardingStatus: DevOnboardingStatus; + FilesystemPermissionFix: FilesystemPermissionFix; + FilesystemPathInfo: FilesystemPathInfo; + FilesystemPathInfoQuery: FilesystemPathInfoQuery; + FilesystemFile: FilesystemFile; + FilesystemFileRead: FilesystemFileRead; + FilesystemFileWrite: FilesystemFileWrite; + FilesystemFileDelete: FilesystemFileDelete; + FilesystemDir: FilesystemDir; + FilesystemDirList: FilesystemDirList; + FilesystemDirMake: FilesystemDirMake; + FilesystemDirRemove: FilesystemDirRemove; + FilesystemFormat: FilesystemFormat; + PortfolioUpdate: PortfolioUpdate; }; export type MessageKey = keyof MessageType; @@ -4958,8 +5738,19 @@ export type MessageResponse = { message: MessageType[T]; }; -export type TypedCall = ( - type: T, - resType: R, - message?: MessageType[T] -) => Promise>; +export type MessageResponseMap = { + [K in MessageKey]: MessageResponse; +}; + +export type TypedCall = { + ( + type: T, + resType: R, + message?: MessageType[T] + ): Promise; + ( + type: T, + resType: R, + message?: MessageType[T] + ): Promise>; +}; diff --git a/packages/hd-transport/src/types/transport.ts b/packages/hd-transport/src/types/transport.ts index 653f09274..ddbfa8adf 100644 --- a/packages/hd-transport/src/types/transport.ts +++ b/packages/hd-transport/src/types/transport.ts @@ -1,5 +1,7 @@ import type EventEmitter from 'events'; +export type ProtocolType = 'V1' | 'V2'; + export type OneKeyDeviceCommType = | 'usb' | 'webusb' @@ -31,17 +33,27 @@ export type OneKeyDeviceInfoBase = { // TODO: sorting type by communication type export type OneKeyDeviceInfo = OneKeyDeviceInfoBase & OneKeyDeviceInfoWithSession & - OneKeyMobileDeviceInfo; + OneKeyMobileDeviceInfo & { + protocolType?: ProtocolType; + }; export type AcquireInput = { path?: string; previous?: string | null; uuid?: string; forceCleanRunPromise?: boolean; + expectedProtocol?: ProtocolType; }; export type MessageFromOneKey = { type: string; message: Record }; +export type TransportCallOptions = { + timeoutMs?: number; + expectedTypes?: string[]; + intermediateTypes?: string[]; + onIntermediateResponse?: (response: MessageFromOneKey) => void; +}; + type ITransportInitFn = ( logger?: any, emitter?: EventEmitter, @@ -54,7 +66,13 @@ export type Transport = { acquire(input: AcquireInput): Promise; release(session: string, onclose: boolean): Promise; configure(signedData: JSON | string): Promise; - call(session: string, name: string, data: Record): Promise; + configureProtocolV2?: (signedData: JSON | string) => Promise | void; + call( + session: string, + name: string, + data: Record, + options?: TransportCallOptions + ): Promise; post(session: string, name: string, data: Record): Promise; read(session: string): Promise; cancel(): Promise; @@ -63,6 +81,12 @@ export type Transport = { // used to reset the session of the transport when the session is not valid disconnect?: (session: string) => Promise; + // Returns the protocol type for a given device path. + // Single-protocol transports (HTTP, emulator, etc.) return 'V1'. + // Protocol V2-capable transports return the probed protocol for each device, + // or undefined before protocol detection succeeds. + getProtocolType: (path: string) => ProtocolType | undefined; + // web-usb, web-bluetooth request device promptDeviceAccess?: () => Promise; @@ -82,7 +106,7 @@ export type LowLevelDevice = OneKeyDeviceInfoBase & { id: string; name: string } export type LowlevelTransportSharedPlugin = { enumerate: () => Promise; send: (uuid: string, data: string) => Promise; - receive: () => Promise; + receive: (uuid?: string) => Promise; connect: (uuid: string) => Promise; disconnect: (uuid: string) => Promise; diff --git a/packages/hd-transport/src/utils/logBlockCommand.ts b/packages/hd-transport/src/utils/logBlockCommand.ts index b27c65ec0..8616c72a3 100644 --- a/packages/hd-transport/src/utils/logBlockCommand.ts +++ b/packages/hd-transport/src/utils/logBlockCommand.ts @@ -1 +1,9 @@ -export const LogBlockCommand = new Set(['PassphraseAck', 'PinMatrixAck']); +export const LogBlockCommand = new Set([ + 'PassphraseAck', + 'PinMatrixAck', + 'FilesystemFileWrite', + 'FileWrite', + 'EmmcFileWrite', + 'FirmwareUpload', + 'ResourceAck', +]); diff --git a/packages/hd-web-sdk/src/iframe/index.ts b/packages/hd-web-sdk/src/iframe/index.ts index 5e72994e9..0a6df8881 100644 --- a/packages/hd-web-sdk/src/iframe/index.ts +++ b/packages/hd-web-sdk/src/iframe/index.ts @@ -5,17 +5,16 @@ import { CORE_EVENT, DataManager, IFRAME, - LogBlockEvent, LoggerNames, createErrorMessage, createIFrameMessage, + getLogBlockLabel, getLogger, initCore, parseConnectSettings, parseMessage, switchTransport, } from '@onekeyfe/hd-core'; -import { get } from 'lodash'; import { getOrigin } from '../utils/urlUtils'; import { createJsBridge, sendMessage } from '../utils/bridgeUtils'; @@ -87,7 +86,7 @@ export async function init(payload: IFrameInit['payload']) { targetOrigin: getOrigin(settings.parentOrigin as string), receiveHandler: async messageEvent => { const message = parseMessage(messageEvent); - const blockLog = LogBlockEvent.has(get(message, 'type')) ? message.type : undefined; + const blockLog = getLogBlockLabel(message); if (blockLog) { Log.debug('Frame Bridge Receive message: ', blockLog); } else { diff --git a/packages/hd-web-sdk/src/index.ts b/packages/hd-web-sdk/src/index.ts index 78bbe7070..d287f74cf 100644 --- a/packages/hd-web-sdk/src/index.ts +++ b/packages/hd-web-sdk/src/index.ts @@ -13,6 +13,7 @@ import HardwareSdk, { createErrorMessage, enableLog, executeCallback, + getLogBlockLabel, getLogger, parseConnectSettings, parseMessage, @@ -118,6 +119,14 @@ const cancel = (connectId?: string) => { sendMessage({ event: IFRAME.CANCEL, type: IFRAME.CANCEL, payload: { connectId } }); }; +const cancelOperation = (operationId: string) => { + sendMessage({ + event: IFRAME.CANCEL_OPERATION, + type: IFRAME.CANCEL_OPERATION, + payload: { operationId }, + } as CoreMessage); +}; + let prevFrameInstance: Window | null | undefined = null; const createJSBridge = (messageEvent: PostMessageEvent) => { if (messageEvent.origin !== iframe.origin) { @@ -135,9 +144,12 @@ const createJSBridge = (messageEvent: PostMessageEvent) => { receiveHandler: async messageEvent => { const message = parseMessage(messageEvent); + const blockLog = getLogBlockLabel(message); if (message.event !== 'LOG_EVENT') { if (['DEVICE_EVENT', 'FIRMWARE_EVENT'].includes(message.event)) { // Log.debug('Host Bridge Receive message: ', message); + } else if (blockLog) { + Log.debug('Host Bridge Receive message: ', blockLog); } else { Log.debug('Host Bridge Receive message: ', message); } @@ -146,6 +158,8 @@ const createJSBridge = (messageEvent: PostMessageEvent) => { if (message.event !== 'LOG_EVENT') { if (['DEVICE_EVENT', 'FIRMWARE_EVENT'].includes(message.event)) { // Log.debug('Host Bridge response: ', message); + } else if (blockLog) { + Log.debug('Host Bridge response: ', blockLog); } else { Log.debug('Host Bridge response: ', message); } @@ -184,7 +198,8 @@ const init = async (settings: Partial) => { }; const call = async (params: any) => { - Log.debug('call : ', params); + const blockLog = getLogBlockLabel(params); + Log.debug('call : ', blockLog ?? params); /** * Try to recreate iframe if it's initialize failed */ @@ -283,6 +298,7 @@ const HardwareSDKLowLevel = HardwareLowLevelSdk({ init, call, cancel, + cancelOperation, dispose, addHardwareGlobalEventListener, uiResponse, @@ -297,6 +313,7 @@ const HardwareWebSdk = HardwareSdk({ init, call, cancel, + cancelOperation, dispose, uiResponse, updateSettings, diff --git a/packages/hd-web-sdk/src/utils/bridgeUtils.ts b/packages/hd-web-sdk/src/utils/bridgeUtils.ts index 59622eff3..7e8f803d0 100644 --- a/packages/hd-web-sdk/src/utils/bridgeUtils.ts +++ b/packages/hd-web-sdk/src/utils/bridgeUtils.ts @@ -1,7 +1,6 @@ import { JsBridgeIframe, setPostMessageListenerFlag } from '@onekeyfe/cross-inpage-provider-core'; -import { LogBlockEvent, LoggerNames, getLogger } from '@onekeyfe/hd-core'; +import { LoggerNames, getLogBlockLabel, getLogger } from '@onekeyfe/hd-core'; import { ERRORS } from '@onekeyfe/hd-shared'; -import { get } from 'lodash'; import JSBridgeConfig from '../iframe/bridge-config'; @@ -30,7 +29,7 @@ export const sendMessage = async (messages: CoreMessage, isHost = true): Promise const bridge = isHost ? hostBridge : frameBridge; try { - const blockLog = LogBlockEvent.has(get(messages, 'type')) ? messages.type : undefined; + const blockLog = getLogBlockLabel(messages); if (messages.event !== 'LOG_EVENT') { if (blockLog) { Log.debug('request: ', blockLog); diff --git a/packages/shared/src/HardwareError.ts b/packages/shared/src/HardwareError.ts index 3d6acdec6..43ab8da4e 100644 --- a/packages/shared/src/HardwareError.ts +++ b/packages/shared/src/HardwareError.ts @@ -484,6 +484,12 @@ export const HardwareErrorCode = { */ PinMismatch: 828, + /** + * Protocol V2 device must be unlocked before the requested operation. + * @params: { failureCode?: string; subcode?: number; firmwareMessage?: string } + */ + DeviceLocked: 829, + /** * Lowlevel transport connect error */ @@ -635,6 +641,7 @@ export const HardwareErrorCodeMessage: HardwareErrorCodeMessageMapping = { [HardwareErrorCode.DeviceSettingsLanguageNotSupport]: 'Language not supported', [HardwareErrorCode.TooManyInputs]: 'Too many inputs', [HardwareErrorCode.PinMismatch]: 'PIN mismatch', + [HardwareErrorCode.DeviceLocked]: 'Device locked', /** * Lowlevel transport diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index c91605e51..bee9faca4 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -1,11 +1,19 @@ import { HardwareErrorCode } from './HardwareError'; +export const HARDWARE_CONNECT_PROTOCOL = { + V1: 'V1', + V2: 'V2', +} as const; + +export type HardwareConnectProtocol = + (typeof HARDWARE_CONNECT_PROTOCOL)[keyof typeof HARDWARE_CONNECT_PROTOCOL]; + export const ONEKEY_WEBUSB_FILTER = [ { vendorId: 0x1209, productId: 0x53c0 }, // Classic Boot、Classic1s Boot、Mini Boot - { vendorId: 0x1209, productId: 0x53c1 }, // Classic Firmware、Classic1s Firmware、Mini Firmware、Pro Firmware、Touch Firmware - { vendorId: 0x1209, productId: 0x4f4a }, // Pro Boot、Touch Boot - { vendorId: 0x1209, productId: 0x4f4b }, // Pro Firmware、Touch Firmware(Not implemented Trezor) - // { vendorId: 0x1209, productId: 0x4f4c }, // Pro Board + { vendorId: 0x1209, productId: 0x53c1 }, // Classic Firmware、Classic1s Firmware、Mini Firmware、Pro Firmware、Touch Firmware、Pro2(旧固件,勿删:存量设备仍以此 PID 枚举) + { vendorId: 0x1209, productId: 0x4f4a }, // Pro Boot、Touch Boot、Pro2 + { vendorId: 0x1209, productId: 0x4f4b }, // Pro Firmware、Touch Firmware(Not implemented Trezor)、Pro2 + { vendorId: 0x1209, productId: 0x4f4c }, // Pro Board、Pro2(新固件 PID) // { vendorId: 0x1209, productId: 0x4f50 }, // Touch Board ]; diff --git a/packages/shared/src/deviceType.ts b/packages/shared/src/deviceType.ts index 9c5167035..133ae3a10 100644 --- a/packages/shared/src/deviceType.ts +++ b/packages/shared/src/deviceType.ts @@ -6,4 +6,5 @@ export enum EDeviceType { Mini = 'mini', Touch = 'touch', Pro = 'pro', + Pro2 = 'pro2', } diff --git a/submodules/firmware-pro2 b/submodules/firmware-pro2 new file mode 160000 index 000000000..a3e5c5447 --- /dev/null +++ b/submodules/firmware-pro2 @@ -0,0 +1 @@ +Subproject commit a3e5c5447ffdc3dab3115d54368934b77e74584a