From f9c90123bcd5951457ba00b68aecba8c8760b47d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Leszczyk?= Date: Fri, 21 Nov 2025 19:57:08 +0100 Subject: [PATCH 1/2] fix: remove ledger heartbeat --- .../LedgerProvider/LedgerProvider.tsx | 61 ------------------- 1 file changed, 61 deletions(-) diff --git a/packages/ui/src/contexts/LedgerProvider/LedgerProvider.tsx b/packages/ui/src/contexts/LedgerProvider/LedgerProvider.tsx index d876a40c3..9a351280c 100644 --- a/packages/ui/src/contexts/LedgerProvider/LedgerProvider.tsx +++ b/packages/ui/src/contexts/LedgerProvider/LedgerProvider.tsx @@ -68,12 +68,6 @@ export enum LedgerAppType { export const REQUIRED_LEDGER_VERSION = '0.7.3'; export const LEDGER_VERSION_WITH_EIP_712 = '0.8.0'; -const LEDGER_ERROR_CODES = Object.freeze({ - DEVICE_LOCKED: 0x5515, - INCORRECT_LENGTH: 0x6700, - SOMETHING_WRONG: 0x6b0c, -}); - /** * Run this here since each new window will have a different id * this is used to track the transport and close on window close @@ -501,61 +495,6 @@ export function LedgerContextProvider({ children }: PropsWithChildren) { setLedgerVersionWarningClosed(result); }, [request]); - // Ledger Stax getting locked when connected via USB needs to be detected and the transport needs to be cleared - // Heartbeat mechanism is being used to detect device lock - useEffect(() => { - let isCheckingHeartbeat = false; - - const checkHeartbeat = async () => { - if (isCheckingHeartbeat || !transportRef.current) { - return; - } - - isCheckingHeartbeat = true; - - try { - if (!app) { - // No app instance - try to re-establish connection - await initLedgerApp(transportRef.current); - } else { - // Send a simple GET_VERSION command which should always require device interaction - await transportRef.current.send( - 0xe0, // CLA - Generic command class - 0x01, // INS - Get version instruction - 0x00, // P1 - 0x00, // P2 - Buffer.alloc(0), // Data - [0x9000], // Expected status code for success - ); - } - } catch (error: any) { - // Check if this looks like a device lock error - const isLockError = Object.values(LEDGER_ERROR_CODES).includes( - error?.statusCode, - ); - - if (isLockError && app) { - // Device appears to be locked, clearing transport but keeping heartbeat running - setApp(undefined); - setAppType(LedgerAppType.UNKNOWN); - } - } finally { - isCheckingHeartbeat = false; - } - }; - - // Check heartbeat every 3 seconds to detect if the device is locked (3 seconds might be too excessive, but it's a good starting point to avoid false positives) - const heartbeatInterval = setInterval(checkHeartbeat, 3000); - - checkHeartbeat(); - - return () => { - if (heartbeatInterval) { - clearInterval(heartbeatInterval); - } - }; - }, [app, initLedgerApp]); - return ( Date: Fri, 21 Nov 2025 20:27:19 +0100 Subject: [PATCH 2/2] test: ledger provider --- .../LedgerProvider/LedgerProvider.test.tsx | 113 ------------------ 1 file changed, 113 deletions(-) diff --git a/packages/ui/src/contexts/LedgerProvider/LedgerProvider.test.tsx b/packages/ui/src/contexts/LedgerProvider/LedgerProvider.test.tsx index 50eb836da..790b74380 100644 --- a/packages/ui/src/contexts/LedgerProvider/LedgerProvider.test.tsx +++ b/packages/ui/src/contexts/LedgerProvider/LedgerProvider.test.tsx @@ -1187,117 +1187,4 @@ describe('src/contexts/LedgerProvider.tsx', () => { }); }); }); - - describe('checkHeartbeat', () => { - beforeEach(() => { - // Clear any previous calls to refMock.send before each test - refMock.send.mockClear(); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('should not run heartbeat when transport is not available', async () => { - // Mock transportRef.current to be null - jest.spyOn(React, 'useRef').mockReturnValue({ - current: null, - }); - - renderTestComponent(); - - // Fast-forward time to trigger heartbeat - jest.advanceTimersByTime(3000); - - // Should not call transport.send when no transport - expect(refMock.send).not.toHaveBeenCalled(); - expect(AppAvalanche).not.toHaveBeenCalled(); - }); - - it('should run heartbeat when transport is available', async () => { - // Mock transportRef.current to be available - jest.spyOn(React, 'useRef').mockReturnValue({ - current: refMock, - }); - - // Mock app state to exist so heartbeat runs - const mockApp = new AppAvalanche(refMock as any); - jest.spyOn(React, 'useState').mockImplementation(((initialValue: any) => { - if (initialValue === undefined) { - // This is the app state - return [mockApp, jest.fn()]; - } - return [initialValue, jest.fn()]; - }) as any); - - renderTestComponent(); - - // Fast-forward time to trigger heartbeat - jest.advanceTimersByTime(3000); - - // Should call transport.send when transport is available - expect(refMock.send).toHaveBeenCalled(); - }); - - it('should run heartbeat when no app but transport is available', async () => { - // Mock transportRef.current to be available - jest.spyOn(React, 'useRef').mockReturnValue({ - current: refMock, - }); - - // Mock app state to be undefined (no app) - jest.spyOn(React, 'useState').mockImplementation(((initialValue: any) => { - if (initialValue === undefined) { - // This is the app state - return undefined to simulate no app - return [undefined, jest.fn()]; - } - return [initialValue, jest.fn()]; - }) as any); - - renderTestComponent(); - - // Fast-forward time to trigger heartbeat - jest.advanceTimersByTime(3000); - - // The heartbeat should run (it will attempt to reinitialize the app) - // We can verify this by checking that the heartbeat mechanism is active - // The actual reinitialization will happen through initLedgerApp - expect(refMock.send).not.toHaveBeenCalled(); // No direct send call when no app - }); - - it('should detect device lock error codes correctly', () => { - // Test the error detection logic directly - const testCases = [ - { statusCode: 0x5515, shouldBeLock: true }, - { statusCode: 0x6700, shouldBeLock: true }, - { statusCode: 0x6b0c, shouldBeLock: true }, - { statusCode: 0x9001, shouldBeLock: false }, - ]; - - testCases.forEach(({ statusCode, shouldBeLock }) => { - const error = new Error('Test error') as any; - error.statusCode = statusCode; - const isLockError = - error?.statusCode === 0x5515 || // Device locked - error?.statusCode === 0x6700 || // Incorrect length - error?.statusCode === 0x6b0c; // Something went wrong - - expect(isLockError).toBe(shouldBeLock); - }); - }); - - it('should clean up interval on unmount', () => { - const clearIntervalSpy = jest.spyOn(global, 'clearInterval'); - - const { unmount } = renderTestComponent(); - - // Fast-forward to set up interval - jest.advanceTimersByTime(3000); - - // Unmount component - unmount(); - - expect(clearIntervalSpy).toHaveBeenCalled(); - }); - }); });