@@ -9354,6 +9354,163 @@ describe('HyperLiquidProvider', () => {
93549354 expect ( mockExchangeClient . agentSetAbstraction ) . not . toHaveBeenCalled ( ) ;
93559355 } ) ;
93569356
9357+ // ─────────────────────────────────────────────────
9358+ // "User or API Wallet does not exist" — unfunded wallet no-op
9359+ //
9360+ // HL returns this error when a wallet has never deposited funds.
9361+ // The migration should be silently recorded as not_applicable
9362+ // and must NOT fire a Sentry error or a failed analytics event.
9363+ // ─────────────────────────────────────────────────
9364+
9365+ it ( 'silences the "User or API Wallet does not exist" error from agentSetAbstraction (default → unified)' , async ( ) => {
9366+ // Arrange: HL reports the wallet has no account
9367+ const userNotFoundError = new Error (
9368+ 'User or API Wallet 0xabc does not exist.' ,
9369+ ) ;
9370+ const mockExchangeClient = createMockExchangeClient ( ) ;
9371+ mockExchangeClient . agentSetAbstraction = jest
9372+ . fn ( )
9373+ . mockRejectedValue ( userNotFoundError ) ;
9374+ mockClientService . getExchangeClient = jest
9375+ . fn ( )
9376+ . mockReturnValue ( mockExchangeClient ) ;
9377+ mockClientService . getInfoClient = jest . fn ( ) . mockReturnValue (
9378+ createMockInfoClient ( {
9379+ userAbstraction : jest . fn ( ) . mockResolvedValue ( 'default' ) ,
9380+ } ) ,
9381+ ) ;
9382+
9383+ // Act — must resolve, not throw
9384+ await expect ( provider . getMarketDataWithPrices ( ) ) . resolves . not . toThrow ( ) ;
9385+
9386+ // Cache written with reason: 'no_hl_account' so we stop retrying
9387+ expect (
9388+ ( TradingReadinessCache as jest . Mocked < typeof TradingReadinessCache > )
9389+ . set ,
9390+ ) . toHaveBeenCalledWith ( 'mainnet' , USER_ADDRESS , {
9391+ attempted : true ,
9392+ enabled : false ,
9393+ reason : 'no_hl_account' ,
9394+ } ) ;
9395+
9396+ // Analytics: one migration_required event followed by one not_applicable
9397+ const trackCalls = (
9398+ mockPlatformDependencies . metrics . trackPerpsEvent as jest . Mock
9399+ ) . mock . calls . filter ( ( call ) => call [ 0 ] === 'Perp Account Setup' ) ;
9400+ const notApplicableCall = trackCalls . find (
9401+ ( call ) => call [ 1 ] ?. status === 'not_applicable' ,
9402+ ) ;
9403+ expect ( notApplicableCall ) . toBeDefined ( ) ;
9404+ expect ( notApplicableCall [ 1 ] ) . toEqual (
9405+ expect . objectContaining ( {
9406+ previous_abstraction_mode : 'default' ,
9407+ abstraction_mode : 'unifiedAccount' ,
9408+ status : 'not_applicable' ,
9409+ error_message : 'no_hl_account' ,
9410+ } ) ,
9411+ ) ;
9412+
9413+ // Sentry NOT called — this is not a real failure
9414+ expect ( mockPlatformDependencies . logger . error ) . not . toHaveBeenCalled ( ) ;
9415+ } ) ;
9416+
9417+ it ( 'silences the "User or API Wallet does not exist" error from userSetAbstraction (dexAbstraction → unified)' , async ( ) => {
9418+ // Arrange: dexAbstraction user with no HL account
9419+ const userNotFoundError = new Error (
9420+ 'User or API Wallet 0xdef does not exist.' ,
9421+ ) ;
9422+ const mockExchangeClient = createMockExchangeClient ( ) ;
9423+ mockExchangeClient . userSetAbstraction = jest
9424+ . fn ( )
9425+ . mockRejectedValue ( userNotFoundError ) ;
9426+ mockClientService . getExchangeClient = jest
9427+ . fn ( )
9428+ . mockReturnValue ( mockExchangeClient ) ;
9429+ mockClientService . getInfoClient = jest . fn ( ) . mockReturnValue (
9430+ createMockInfoClient ( {
9431+ userAbstraction : jest . fn ( ) . mockResolvedValue ( 'dexAbstraction' ) ,
9432+ } ) ,
9433+ ) ;
9434+
9435+ // Act — init path for software wallet (allowUserSigning=true via
9436+ // isSelectedHardwareWallet returning false)
9437+ await expect ( provider . getMarketDataWithPrices ( ) ) . resolves . not . toThrow ( ) ;
9438+
9439+ // Cache written with reason: 'no_hl_account' (not the dexAbstraction-
9440+ // failure path — the no_hl_account branch must win)
9441+ expect (
9442+ ( TradingReadinessCache as jest . Mocked < typeof TradingReadinessCache > )
9443+ . set ,
9444+ ) . toHaveBeenCalledWith ( 'mainnet' , USER_ADDRESS , {
9445+ attempted : true ,
9446+ enabled : false ,
9447+ reason : 'no_hl_account' ,
9448+ } ) ;
9449+
9450+ // Analytics: not_applicable, not failed
9451+ const trackCalls = (
9452+ mockPlatformDependencies . metrics . trackPerpsEvent as jest . Mock
9453+ ) . mock . calls . filter ( ( call ) => call [ 0 ] === 'Perp Account Setup' ) ;
9454+ const notApplicableCall = trackCalls . find (
9455+ ( call ) => call [ 1 ] ?. status === 'not_applicable' ,
9456+ ) ;
9457+ expect ( notApplicableCall ) . toBeDefined ( ) ;
9458+ expect ( notApplicableCall [ 1 ] ) . toEqual (
9459+ expect . objectContaining ( {
9460+ previous_abstraction_mode : 'dexAbstraction' ,
9461+ abstraction_mode : 'unifiedAccount' ,
9462+ status : 'not_applicable' ,
9463+ error_message : 'no_hl_account' ,
9464+ } ) ,
9465+ ) ;
9466+
9467+ // Sentry NOT called
9468+ expect ( mockPlatformDependencies . logger . error ) . not . toHaveBeenCalled ( ) ;
9469+ } ) ;
9470+
9471+ it ( 'still fires the failed analytics event and Sentry for unrelated agentSetAbstraction errors' , async ( ) => {
9472+ // Arrange: unrelated error to confirm the special case does not
9473+ // swallow legitimate failures
9474+ const unrelatedError = new Error ( 'Insufficient margin' ) ;
9475+ const mockExchangeClient = createMockExchangeClient ( ) ;
9476+ mockExchangeClient . agentSetAbstraction = jest
9477+ . fn ( )
9478+ . mockRejectedValue ( unrelatedError ) ;
9479+ mockClientService . getExchangeClient = jest
9480+ . fn ( )
9481+ . mockReturnValue ( mockExchangeClient ) ;
9482+ mockClientService . getInfoClient = jest . fn ( ) . mockReturnValue (
9483+ createMockInfoClient ( {
9484+ userAbstraction : jest . fn ( ) . mockResolvedValue ( 'default' ) ,
9485+ } ) ,
9486+ ) ;
9487+
9488+ // Act — still resolves (error is caught internally)
9489+ await expect ( provider . getMarketDataWithPrices ( ) ) . resolves . not . toThrow ( ) ;
9490+
9491+ // Analytics: should have a failed event, no not_applicable
9492+ const trackCalls = (
9493+ mockPlatformDependencies . metrics . trackPerpsEvent as jest . Mock
9494+ ) . mock . calls . filter ( ( call ) => call [ 0 ] === 'Perp Account Setup' ) ;
9495+ const failedCall = trackCalls . find (
9496+ ( call ) => call [ 1 ] ?. status === 'failed' ,
9497+ ) ;
9498+ expect ( failedCall ) . toBeDefined ( ) ;
9499+ expect ( failedCall [ 1 ] ) . toEqual (
9500+ expect . objectContaining ( {
9501+ status : 'failed' ,
9502+ error_message : 'Insufficient margin' ,
9503+ } ) ,
9504+ ) ;
9505+ const notApplicableCall = trackCalls . find (
9506+ ( call ) => call [ 1 ] ?. status === 'not_applicable' ,
9507+ ) ;
9508+ expect ( notApplicableCall ) . toBeUndefined ( ) ;
9509+
9510+ // Sentry IS called
9511+ expect ( mockPlatformDependencies . logger . error ) . toHaveBeenCalled ( ) ;
9512+ } ) ;
9513+
93579514 // ─────────────────────────────────────────────────
93589515 // Network key (mainnet vs testnet)
93599516 // ─────────────────────────────────────────────────
0 commit comments