@@ -260,6 +260,46 @@ describe('useStellarWallet connect flow', () => {
260260
261261 expectConnected ( IN_APP_PK , 'inapp' , '5' , '75' , '25' ) ;
262262 } ) ;
263+
264+ it ( 'Freighter: connects successfully and records the wallet type' , async ( ) => {
265+ ( globalThis as { window ?: unknown } ) . window = {
266+ freighter : {
267+ isConnected : jest . fn ( ) . mockResolvedValue ( true ) ,
268+ getPublicKey : jest . fn ( ) . mockResolvedValue ( FREIGHTER_PK ) ,
269+ signTransaction : jest . fn ( ) ,
270+ } ,
271+ } ;
272+ tree = await renderProbe ( ) ;
273+ await act ( async ( ) => {
274+ await hook . connectFreighter ( ) ;
275+ } ) ;
276+ expectConnected ( FREIGHTER_PK , 'freighter' , '123.45' , '75' , '25' ) ;
277+ expect ( hook . error ) . toBeNull ( ) ;
278+ } ) ;
279+
280+ it ( 'Lobstr: connects successfully and extracts the key from the signed challenge' , async ( ) => {
281+ tree = await renderProbe ( ) ;
282+ await act ( async ( ) => {
283+ await hook . connectLobstr ( ) ;
284+ } ) ;
285+ expect ( openLobstrForSigning ) . toHaveBeenCalled ( ) ;
286+ expectConnected ( 'GLOBSTRUSERKEY' , 'lobstr' , '123.45' , '75' , '25' ) ;
287+ expect ( hook . error ) . toBeNull ( ) ;
288+ } ) ;
289+
290+ it ( 'import: valid secret key connects and persists to the vault' , async ( ) => {
291+ tree = await renderProbe ( ) ;
292+ let result : { publicKey : string } | undefined ;
293+ await act ( async ( ) => {
294+ result = await hook . importWallet ( ' SVALIDIMPORTSECRET ' ) ;
295+ } ) ;
296+ expect ( result ) . toEqual ( { publicKey : IMPORT_PK } ) ;
297+ // Secret is trimmed before validation/derivation.
298+ expect ( isValidSecretKey ) . toHaveBeenCalledWith ( 'SVALIDIMPORTSECRET' ) ;
299+ expectConnected ( IMPORT_PK , 'inapp' , '123.45' , '75' , '25' ) ;
300+ expect ( getInAppSecret ( IMPORT_PK ) ) . toBe ( 'SVALIDIMPORTSECRET' ) ;
301+ expect ( hook . error ) . toBeNull ( ) ;
302+ } ) ;
263303 } ) ;
264304
265305 describe ( 'balance fetch failure rolls back (all wallet paths)' , ( ) => {
@@ -381,6 +421,90 @@ describe('useStellarWallet connect flow', () => {
381421 } ) ;
382422 } ) ;
383423
424+ describe ( 'connection errors (per wallet path)' , ( ) => {
425+ it ( 'Freighter: reports when the extension is not detected' , async ( ) => {
426+ delete ( globalThis as { window ?: unknown } ) . window ;
427+ tree = await renderProbe ( ) ;
428+ await act ( async ( ) => {
429+ await hook . connectFreighter ( ) ;
430+ } ) ;
431+ expect ( hook . error ) . toBe ( 'Freighter extension not detected' ) ;
432+ expectDisconnected ( ) ;
433+ } ) ;
434+
435+ it ( 'Freighter: asks the user to unlock the extension when not connected' , async ( ) => {
436+ ( globalThis as { window ?: unknown } ) . window = {
437+ freighter : {
438+ isConnected : jest . fn ( ) . mockResolvedValue ( false ) ,
439+ getPublicKey : jest . fn ( ) ,
440+ signTransaction : jest . fn ( ) ,
441+ } ,
442+ } ;
443+ tree = await renderProbe ( ) ;
444+ await act ( async ( ) => {
445+ await hook . connectFreighter ( ) ;
446+ } ) ;
447+ expect ( hook . error ) . toBe ( 'Please unlock Freighter first' ) ;
448+ expect ( getBalance ) . not . toHaveBeenCalled ( ) ;
449+ expectDisconnected ( ) ;
450+ } ) ;
451+
452+ it ( 'Lobstr: surfaces a clear error when the app is not installed' , async ( ) => {
453+ isLobstrInstalled . mockResolvedValue ( false ) ;
454+ tree = await renderProbe ( ) ;
455+ await act ( async ( ) => {
456+ await hook . connectLobstr ( ) ;
457+ } ) ;
458+ expect ( hook . error ) . toMatch ( / L o b s t r i s n o t i n s t a l l e d / i) ;
459+ expect ( openLobstrForSigning ) . not . toHaveBeenCalled ( ) ;
460+ expectDisconnected ( ) ;
461+ } ) ;
462+
463+ it ( 'Lobstr: surfaces an error when signing is cancelled' , async ( ) => {
464+ openLobstrForSigning . mockRejectedValue (
465+ new Error ( 'Lobstr signing was cancelled' ) ,
466+ ) ;
467+ tree = await renderProbe ( ) ;
468+ await act ( async ( ) => {
469+ await hook . connectLobstr ( ) ;
470+ } ) ;
471+ expect ( hook . error ) . toBe ( 'Lobstr signing was cancelled' ) ;
472+ expect ( getBalance ) . not . toHaveBeenCalled ( ) ;
473+ expectDisconnected ( ) ;
474+ } ) ;
475+
476+ it ( 'import: invalid secret key is rejected without persisting' , async ( ) => {
477+ isValidSecretKey . mockReturnValue ( false ) ;
478+ tree = await renderProbe ( ) ;
479+ let result : { publicKey : string } | undefined ;
480+ await act ( async ( ) => {
481+ result = await hook . importWallet ( 'SBADKEY' ) ;
482+ } ) ;
483+ expect ( result ) . toBeUndefined ( ) ;
484+ expect ( hook . error ) . toBe ( 'Invalid secret key' ) ;
485+ expect ( getPublicKeyFromSecret ) . not . toHaveBeenCalled ( ) ;
486+ expect ( getBalance ) . not . toHaveBeenCalled ( ) ;
487+ expectDisconnected ( ) ;
488+ expect ( getInAppSecret ( IMPORT_PK ) ) . toBeNull ( ) ;
489+ } ) ;
490+ } ) ;
491+
492+ describe ( 'disconnectWallet' , ( ) => {
493+ it ( 'clears the vault entry and resets the store' , async ( ) => {
494+ tree = await renderProbe ( ) ;
495+ await act ( async ( ) => {
496+ await hook . createInAppWallet ( ) ;
497+ } ) ;
498+ expect ( getInAppSecret ( IN_APP_PK ) ) . toBe ( IN_APP_SECRET ) ;
499+
500+ await act ( async ( ) => {
501+ hook . disconnectWallet ( ) ;
502+ } ) ;
503+ expect ( getInAppSecret ( IN_APP_PK ) ) . toBeNull ( ) ;
504+ expectDisconnected ( ) ;
505+ } ) ;
506+ } ) ;
507+
384508 describe ( 'refreshes after connect are fail-safe' , ( ) => {
385509 it ( 'keeps the last known balance when a later refresh fails' , async ( ) => {
386510 tree = await renderProbe ( ) ;
@@ -400,6 +524,54 @@ describe('useStellarWallet connect flow', () => {
400524 expect ( useWalletStore . getState ( ) . isConnected ) . toBe ( true ) ;
401525 expect ( hook . error ) . toBe ( 'offline blip' ) ;
402526 } ) ;
527+
528+ it ( 'refreshBalance updates the store with a fresh balance' , async ( ) => {
529+ tree = await renderProbe ( ) ;
530+ await act ( async ( ) => {
531+ await hook . createInAppWallet ( ) ;
532+ } ) ;
533+ expect ( useWalletStore . getState ( ) . balance ) . toBe ( '123.45' ) ;
534+
535+ getBalance . mockResolvedValue ( '999.99' ) ;
536+ await act ( async ( ) => {
537+ await hook . refreshBalance ( ) ;
538+ } ) ;
539+ expect ( useWalletStore . getState ( ) . balance ) . toBe ( '999.99' ) ;
540+ expect ( hook . error ) . toBeNull ( ) ;
541+ } ) ;
542+
543+ it ( 'refreshEcoBalance and refreshUsdcBalance update their token balances' , async ( ) => {
544+ tree = await renderProbe ( ) ;
545+ await act ( async ( ) => {
546+ await hook . createInAppWallet ( ) ;
547+ } ) ;
548+
549+ getTokenBalance . mockImplementation ( ( _pk : string , assetCode : string ) =>
550+ Promise . resolve ( assetCode === 'USDC' ? '42' : '13' ) ,
551+ ) ;
552+ await act ( async ( ) => {
553+ await hook . refreshEcoBalance ( ) ;
554+ await hook . refreshUsdcBalance ( ) ;
555+ } ) ;
556+ expect ( useWalletStore . getState ( ) . ecoBalance ) . toBe ( '13' ) ;
557+ expect ( useWalletStore . getState ( ) . usdcBalance ) . toBe ( '42' ) ;
558+ expect ( hook . error ) . toBeNull ( ) ;
559+ } ) ;
560+
561+ it ( 'keeps the last known token balance when a token refresh fails' , async ( ) => {
562+ tree = await renderProbe ( ) ;
563+ await act ( async ( ) => {
564+ await hook . createInAppWallet ( ) ;
565+ } ) ;
566+ expect ( useWalletStore . getState ( ) . ecoBalance ) . toBe ( '75' ) ;
567+
568+ getTokenBalance . mockRejectedValue ( new Error ( 'token horizon down' ) ) ;
569+ await act ( async ( ) => {
570+ await hook . refreshEcoBalance ( ) ;
571+ } ) ;
572+ expect ( useWalletStore . getState ( ) . ecoBalance ) . toBe ( '75' ) ;
573+ expect ( hook . error ) . toBe ( 'token horizon down' ) ;
574+ } ) ;
403575 } ) ;
404576} ) ;
405577
0 commit comments