@@ -3839,6 +3839,214 @@ describe('HyperLiquidSubscriptionService', () => {
38393839
38403840 unsubscribe ( ) ;
38413841 } ) ;
3842+
3843+ it ( 'refreshes spot-backed availableToTradeBalance after streaming fills' , async ( ) => {
3844+ mockSpotClearinghouseState
3845+ . mockResolvedValueOnce ( {
3846+ balances : [ { coin : 'USDC' , total : '100' , hold : '0' } ] ,
3847+ } )
3848+ . mockResolvedValueOnce ( {
3849+ balances : [ { coin : 'USDC' , total : '100' , hold : '3' } ] ,
3850+ } ) ;
3851+
3852+ jest . mocked ( adaptAccountStateFromSDK ) . mockImplementation ( ( ) => ( {
3853+ availableBalance : '50' ,
3854+ availableToTradeBalance : '50' ,
3855+ totalBalance : '200' ,
3856+ marginUsed : '10' ,
3857+ unrealizedPnl : '5' ,
3858+ returnOnEquity : '0.05' ,
3859+ } ) ) ;
3860+
3861+ let userFillsCallback : ( ( data : any ) => void ) | undefined ;
3862+ mockSubscriptionClient . userFills . mockImplementation (
3863+ ( _params : any , callback : any ) => {
3864+ userFillsCallback = callback ;
3865+ return Promise . resolve ( {
3866+ unsubscribe : jest . fn ( ) . mockResolvedValue ( undefined ) ,
3867+ } ) ;
3868+ } ,
3869+ ) ;
3870+
3871+ const singleDexService = new HyperLiquidSubscriptionService (
3872+ mockClientService ,
3873+ mockWalletService ,
3874+ mockDeps ,
3875+ false ,
3876+ ) ;
3877+
3878+ const mockCallback = jest . fn ( ) ;
3879+ const unsubscribe = singleDexService . subscribeToAccount ( {
3880+ callback : mockCallback ,
3881+ } ) ;
3882+
3883+ await jest . runAllTimersAsync ( ) ;
3884+
3885+ expect ( mockCallback ) . toHaveBeenCalled ( ) ;
3886+ expect ( mockCallback . mock . calls . at ( - 1 ) [ 0 ] . availableToTradeBalance ) . toBe (
3887+ '150' ,
3888+ ) ;
3889+
3890+ mockCallback . mockClear ( ) ;
3891+ expect ( userFillsCallback ) . toBeDefined ( ) ;
3892+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
3893+ userFillsCallback ! ( {
3894+ isSnapshot : false ,
3895+ fills : [ { oid : 12345 , coin : 'BTC' , side : 'B' , sz : '0.1' , px : '50000' } ] ,
3896+ } ) ;
3897+
3898+ await jest . advanceTimersByTimeAsync ( 250 ) ;
3899+ await jest . runAllTimersAsync ( ) ;
3900+
3901+ expect ( mockSpotClearinghouseState ) . toHaveBeenCalledTimes ( 2 ) ;
3902+ expect ( mockCallback ) . toHaveBeenCalled ( ) ;
3903+ expect ( mockCallback . mock . calls . at ( - 1 ) [ 0 ] . availableToTradeBalance ) . toBe (
3904+ '147' ,
3905+ ) ;
3906+
3907+ unsubscribe ( ) ;
3908+ } ) ;
3909+
3910+ it ( 'does not refresh spot state on userFills snapshot events' , async ( ) => {
3911+ mockSpotClearinghouseState . mockResolvedValue ( {
3912+ balances : [ { coin : 'USDC' , total : '100' , hold : '0' } ] ,
3913+ } ) ;
3914+
3915+ jest . mocked ( adaptAccountStateFromSDK ) . mockImplementation ( ( ) => ( {
3916+ availableBalance : '50' ,
3917+ availableToTradeBalance : '50' ,
3918+ totalBalance : '200' ,
3919+ marginUsed : '10' ,
3920+ unrealizedPnl : '5' ,
3921+ returnOnEquity : '0.05' ,
3922+ } ) ) ;
3923+
3924+ let userFillsCallback : ( ( data : any ) => void ) | undefined ;
3925+ mockSubscriptionClient . userFills . mockImplementation (
3926+ ( _params : any , callback : any ) => {
3927+ userFillsCallback = callback ;
3928+ return Promise . resolve ( {
3929+ unsubscribe : jest . fn ( ) . mockResolvedValue ( undefined ) ,
3930+ } ) ;
3931+ } ,
3932+ ) ;
3933+
3934+ const singleDexService = new HyperLiquidSubscriptionService (
3935+ mockClientService ,
3936+ mockWalletService ,
3937+ mockDeps ,
3938+ false ,
3939+ ) ;
3940+
3941+ const unsubscribe = singleDexService . subscribeToAccount ( {
3942+ callback : jest . fn ( ) ,
3943+ } ) ;
3944+
3945+ await jest . runAllTimersAsync ( ) ;
3946+
3947+ expect ( userFillsCallback ) . toBeDefined ( ) ;
3948+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
3949+ userFillsCallback ! ( {
3950+ isSnapshot : true ,
3951+ fills : [ { oid : 12345 , coin : 'BTC' , side : 'B' , sz : '0.1' , px : '50000' } ] ,
3952+ } ) ;
3953+
3954+ await jest . advanceTimersByTimeAsync ( 250 ) ;
3955+ await jest . runAllTimersAsync ( ) ;
3956+
3957+ expect ( mockSpotClearinghouseState ) . toHaveBeenCalledTimes ( 1 ) ;
3958+
3959+ unsubscribe ( ) ;
3960+ } ) ;
3961+
3962+ it ( 'coalesces multiple streaming fills into one spot refresh' , async ( ) => {
3963+ mockSpotClearinghouseState
3964+ . mockResolvedValueOnce ( {
3965+ balances : [ { coin : 'USDC' , total : '100' , hold : '0' } ] ,
3966+ } )
3967+ . mockResolvedValueOnce ( {
3968+ balances : [ { coin : 'USDC' , total : '100' , hold : '3' } ] ,
3969+ } ) ;
3970+
3971+ jest . mocked ( adaptAccountStateFromSDK ) . mockImplementation ( ( ) => ( {
3972+ availableBalance : '50' ,
3973+ availableToTradeBalance : '50' ,
3974+ totalBalance : '200' ,
3975+ marginUsed : '10' ,
3976+ unrealizedPnl : '5' ,
3977+ returnOnEquity : '0.05' ,
3978+ } ) ) ;
3979+
3980+ let userFillsCallback : ( ( data : any ) => void ) | undefined ;
3981+ mockSubscriptionClient . userFills . mockImplementation (
3982+ ( _params : any , callback : any ) => {
3983+ userFillsCallback = callback ;
3984+ return Promise . resolve ( {
3985+ unsubscribe : jest . fn ( ) . mockResolvedValue ( undefined ) ,
3986+ } ) ;
3987+ } ,
3988+ ) ;
3989+
3990+ const singleDexService = new HyperLiquidSubscriptionService (
3991+ mockClientService ,
3992+ mockWalletService ,
3993+ mockDeps ,
3994+ false ,
3995+ ) ;
3996+
3997+ const unsubscribe = singleDexService . subscribeToAccount ( {
3998+ callback : jest . fn ( ) ,
3999+ } ) ;
4000+
4001+ await jest . runAllTimersAsync ( ) ;
4002+
4003+ expect ( userFillsCallback ) . toBeDefined ( ) ;
4004+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
4005+ userFillsCallback ! ( {
4006+ isSnapshot : false ,
4007+ fills : [ { oid : 12345 , coin : 'BTC' , side : 'B' , sz : '0.1' , px : '50000' } ] ,
4008+ } ) ;
4009+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
4010+ userFillsCallback ! ( {
4011+ isSnapshot : false ,
4012+ fills : [ { oid : 12346 , coin : 'BTC' , side : 'S' , sz : '0.1' , px : '50010' } ] ,
4013+ } ) ;
4014+
4015+ await jest . advanceTimersByTimeAsync ( 250 ) ;
4016+ await jest . runAllTimersAsync ( ) ;
4017+
4018+ expect ( mockSpotClearinghouseState ) . toHaveBeenCalledTimes ( 2 ) ;
4019+
4020+ unsubscribe ( ) ;
4021+ } ) ;
4022+
4023+ it ( 'cleans up the internal spot refresh fill subscription when account subscribers end' , async ( ) => {
4024+ const accountSpotRefreshSubscription = {
4025+ unsubscribe : jest . fn ( ) . mockResolvedValue ( undefined ) ,
4026+ } ;
4027+
4028+ mockSubscriptionClient . userFills . mockImplementation (
4029+ ( _params : any , _callback : any ) =>
4030+ Promise . resolve ( accountSpotRefreshSubscription ) ,
4031+ ) ;
4032+
4033+ const singleDexService = new HyperLiquidSubscriptionService (
4034+ mockClientService ,
4035+ mockWalletService ,
4036+ mockDeps ,
4037+ false ,
4038+ ) ;
4039+
4040+ const unsubscribe = singleDexService . subscribeToAccount ( {
4041+ callback : jest . fn ( ) ,
4042+ } ) ;
4043+
4044+ await jest . runAllTimersAsync ( ) ;
4045+
4046+ unsubscribe ( ) ;
4047+
4048+ expect ( accountSpotRefreshSubscription . unsubscribe ) . toHaveBeenCalled ( ) ;
4049+ } ) ;
38424050 } ) ;
38434051
38444052 describe ( 'aggregateAccountStates - returnOnEquity calculation' , ( ) => {
0 commit comments