33 WalletConnection ,
44 TokenBalance ,
55 GasEstimate ,
6+ WalletError ,
7+ WalletErrorCode ,
8+ errorTracker ,
69} from '../walletService' ;
710import { ethers } from 'ethers' ;
811import { getContractAddress , ERC20__factory } from '../../contracts' ;
@@ -250,42 +253,58 @@ describe('WalletServiceManager', () => {
250253 } ) ;
251254
252255 describe ( 'getWalletSigner (private)' , ( ) => {
253- it ( 'throws when no connection' , ( ) => {
256+ it ( 'throws WalletError with NOT_CONNECTED code when no connection' , ( ) => {
254257 const mgr = freshManager ( ) ;
255- // Access private via casting
256- expect ( ( ) => ( mgr as any ) . getWalletSigner ( ) ) . toThrow ( 'Wallet is not connected' ) ;
258+ try {
259+ ( mgr as any ) . getWalletSigner ( ) ;
260+ fail ( 'expected to throw' ) ;
261+ } catch ( e ) {
262+ expect ( e ) . toBeInstanceOf ( WalletError ) ;
263+ expect ( ( e as WalletError ) . code ) . toBe ( WalletErrorCode . NOT_CONNECTED ) ;
264+ expect ( ( e as WalletError ) . userMessage ) . toBe ( 'Wallet is not connected.' ) ;
265+ expect ( ( e as WalletError ) . recovery ) . toBeDefined ( ) ;
266+ }
257267 } ) ;
258268
259- it ( 'throws when connection has no eip1193Provider' , ( ) => {
269+ it ( 'throws WalletError when connection has no eip1193Provider' , ( ) => {
260270 const mgr = freshManager ( ) ;
261271 mgr . setConnection ( createMockConnection ( { eip1193Provider : undefined } ) ) ;
262- expect ( ( ) => ( mgr as any ) . getWalletSigner ( ) ) . toThrow ( 'does not expose a signing provider' ) ;
272+ try {
273+ ( mgr as any ) . getWalletSigner ( ) ;
274+ fail ( 'expected to throw' ) ;
275+ } catch ( e ) {
276+ expect ( e ) . toBeInstanceOf ( WalletError ) ;
277+ expect ( ( e as WalletError ) . code ) . toBe ( WalletErrorCode . NOT_CONNECTED ) ;
278+ }
263279 } ) ;
264280 } ) ;
265281
266282 describe ( 'createSuperfluidStream – user rejection' , ( ) => {
267- it ( 'throws friendly error when user rejects transaction' , async ( ) => {
283+ it ( 'throws WalletError USER_REJECTED when user rejects transaction' , async ( ) => {
268284 const mgr = freshManager ( ) ;
269285 const mockSigner = {
270286 provider : { getNetwork : jest . fn ( ) . mockResolvedValue ( { chainId : 1 } ) } ,
271287 getAddress : jest . fn ( ) . mockResolvedValue ( '0xSender' ) ,
272288 } ;
273289 jest . spyOn ( mgr as any , 'getWalletSigner' ) . mockReturnValue ( mockSigner ) ;
274-
275- // Mock buildSuperfluidCreateFlowContext to throw rejection-like error
276290 jest . spyOn ( mgr as any , 'buildSuperfluidCreateFlowContext' ) . mockRejectedValue ( {
277291 code : 4001 ,
278292 message : 'User rejected' ,
279293 } ) ;
280294
281- await expect ( mgr . createSuperfluidStream ( 'ETH' , '10' , '0xRecipient' , 1 ) ) . rejects . toThrow (
282- 'Transaction was rejected in your wallet.'
283- ) ;
295+ try {
296+ await mgr . createSuperfluidStream ( 'ETH' , '10' , '0xRecipient' , 1 ) ;
297+ fail ( 'expected to throw' ) ;
298+ } catch ( e ) {
299+ expect ( e ) . toBeInstanceOf ( WalletError ) ;
300+ expect ( ( e as WalletError ) . code ) . toBe ( WalletErrorCode . USER_REJECTED ) ;
301+ expect ( ( e as WalletError ) . recovery ) . toBeDefined ( ) ;
302+ }
284303 } ) ;
285304 } ) ;
286305
287306 describe ( 'createSuperfluidStream – user denied (string code)' , ( ) => {
288- it ( 'throws friendly error for ACTION_REJECTED code' , async ( ) => {
307+ it ( 'throws WalletError USER_REJECTED for ACTION_REJECTED code' , async ( ) => {
289308 const mgr = freshManager ( ) ;
290309 const mockSigner = {
291310 provider : { getNetwork : jest . fn ( ) . mockResolvedValue ( { chainId : 1 } ) } ,
@@ -296,9 +315,13 @@ describe('WalletServiceManager', () => {
296315 code : 'ACTION_REJECTED' ,
297316 } ) ;
298317
299- await expect ( mgr . createSuperfluidStream ( 'ETH' , '10' , '0xRecipient' , 1 ) ) . rejects . toThrow (
300- 'Transaction was rejected in your wallet.'
301- ) ;
318+ try {
319+ await mgr . createSuperfluidStream ( 'ETH' , '10' , '0xRecipient' , 1 ) ;
320+ fail ( 'expected to throw' ) ;
321+ } catch ( e ) {
322+ expect ( e ) . toBeInstanceOf ( WalletError ) ;
323+ expect ( ( e as WalletError ) . code ) . toBe ( WalletErrorCode . USER_REJECTED ) ;
324+ }
302325 } ) ;
303326 } ) ;
304327
@@ -317,29 +340,86 @@ describe('WalletServiceManager', () => {
317340 } ) ;
318341
319342 describe ( 'createSablierStream – user denied via message' , ( ) => {
320- it ( 'throws friendly error for user denied message' , async ( ) => {
343+ it ( 'throws WalletError USER_REJECTED for user denied message' , async ( ) => {
321344 const mgr = freshManager ( ) ;
322345 const mockSigner = {
323346 provider : { getNetwork : jest . fn ( ) . mockResolvedValue ( { chainId : 1 } ) } ,
324347 getAddress : jest . fn ( ) . mockResolvedValue ( '0xSender' ) ,
325348 } ;
326349 jest . spyOn ( mgr as any , 'getWalletSigner' ) . mockReturnValue ( mockSigner ) ;
327350
328- // Simulate a generic error with "user denied" in message
329351 jest . spyOn ( ethers , 'Contract' as any ) . mockImplementation ( ( ) => {
330352 throw new Error ( 'user denied transaction' ) ;
331353 } ) ;
332354
333- await expect (
334- mgr . createSablierStream (
335- '0xToken' ,
336- '10' ,
337- Date . now ( ) ,
338- Date . now ( ) + 86400000 ,
339- '0xRecipient' ,
340- 1
341- )
342- ) . rejects . toThrow ( 'Transaction was rejected in your wallet.' ) ;
355+ try {
356+ await mgr . createSablierStream ( '0xToken' , '10' , Date . now ( ) , Date . now ( ) + 86400000 , '0xRecipient' , 1 ) ;
357+ fail ( 'expected to throw' ) ;
358+ } catch ( e ) {
359+ expect ( e ) . toBeInstanceOf ( WalletError ) ;
360+ expect ( ( e as WalletError ) . code ) . toBe ( WalletErrorCode . USER_REJECTED ) ;
361+ }
362+ } ) ;
363+ } ) ;
364+
365+ describe ( 'WalletError structure' , ( ) => {
366+ it ( 'has code, userMessage, and recovery fields' , ( ) => {
367+ const err = new WalletError (
368+ WalletErrorCode . STREAM_CREATION_FAILED ,
369+ 'Stream creation failed.' ,
370+ 'Check your token balance and try again.'
371+ ) ;
372+ expect ( err . code ) . toBe ( WalletErrorCode . STREAM_CREATION_FAILED ) ;
373+ expect ( err . userMessage ) . toBe ( 'Stream creation failed.' ) ;
374+ expect ( err . recovery ) . toBe ( 'Check your token balance and try again.' ) ;
375+ expect ( err . name ) . toBe ( 'WalletError' ) ;
376+ } ) ;
377+
378+ it ( 'preserves cause stack when cause is an Error' , ( ) => {
379+ const cause = new Error ( 'rpc timeout' ) ;
380+ const err = new WalletError ( WalletErrorCode . UNKNOWN , 'Something went wrong.' , undefined , cause ) ;
381+ expect ( err . stack ) . toContain ( 'Caused by:' ) ;
382+ } ) ;
383+ } ) ;
384+
385+ describe ( 'errorTracker' , ( ) => {
386+ beforeEach ( ( ) => errorTracker . reset ( ) ) ;
387+
388+ it ( 'records error counts by code' , ( ) => {
389+ errorTracker . record ( WalletErrorCode . USER_REJECTED ) ;
390+ errorTracker . record ( WalletErrorCode . USER_REJECTED ) ;
391+ errorTracker . record ( WalletErrorCode . NOT_CONNECTED ) ;
392+ const stats = errorTracker . getStats ( ) ;
393+ expect ( stats [ WalletErrorCode . USER_REJECTED ] . count ) . toBe ( 2 ) ;
394+ expect ( stats [ WalletErrorCode . NOT_CONNECTED ] . count ) . toBe ( 1 ) ;
395+ } ) ;
396+
397+ it ( 'reset clears all counts' , ( ) => {
398+ errorTracker . record ( WalletErrorCode . APPROVAL_FAILED ) ;
399+ errorTracker . reset ( ) ;
400+ expect ( Object . keys ( errorTracker . getStats ( ) ) . length ) . toBe ( 0 ) ;
401+ } ) ;
402+ } ) ;
403+
404+ describe ( 'getTokenBalances – structured error' , ( ) => {
405+ it ( 'throws WalletError BALANCE_FETCH_FAILED when provider fails' , async ( ) => {
406+ const mgr = freshManager ( ) ;
407+ const mockProvider = {
408+ getBalance : jest . fn ( ) . mockRejectedValue ( new Error ( 'RPC down' ) ) ,
409+ getGasPrice : jest . fn ( ) ,
410+ } ;
411+ jest
412+ . spyOn ( ethers . providers , 'JsonRpcProvider' )
413+ . mockImplementation ( ( ) => mockProvider as unknown as ethers . providers . JsonRpcProvider ) ;
414+
415+ try {
416+ await mgr . getTokenBalances ( '0xAddr' , 1 ) ;
417+ fail ( 'expected to throw' ) ;
418+ } catch ( e ) {
419+ expect ( e ) . toBeInstanceOf ( WalletError ) ;
420+ expect ( ( e as WalletError ) . code ) . toBe ( WalletErrorCode . BALANCE_FETCH_FAILED ) ;
421+ expect ( ( e as WalletError ) . recovery ) . toBeDefined ( ) ;
422+ }
343423 } ) ;
344424 } ) ;
345425} ) ;
0 commit comments