@@ -381,6 +381,180 @@ func TestFetchSingleField(t *testing.T) {
381381 })
382382}
383383
384+ func TestFetchSACMetadata (t * testing.T ) {
385+ ctx := context .Background ()
386+
387+ t .Run ("returns empty slice for empty input" , func (t * testing.T ) {
388+ mockRPCService := NewRPCServiceMock (t )
389+ mockContractModel := data .NewContractModelMock (t )
390+ pool := pond .NewPool (5 )
391+ defer pool .Stop ()
392+
393+ service , err := NewContractMetadataService (mockRPCService , mockContractModel , pool )
394+ require .NoError (t , err )
395+
396+ result , err := service .FetchSACMetadata (ctx , []string {})
397+
398+ require .NoError (t , err )
399+ assert .Empty (t , result )
400+ // Verify no RPC calls were made
401+ mockRPCService .AssertNotCalled (t , "SimulateTransaction" , mock .Anything , mock .Anything )
402+ })
403+
404+ t .Run ("parses code:issuer format successfully" , func (t * testing.T ) {
405+ mockRPCService := NewRPCServiceMock (t )
406+ mockContractModel := data .NewContractModelMock (t )
407+ pool := pond .NewPool (5 )
408+ defer pool .Stop ()
409+
410+ contractID := "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
411+
412+ // Mock name() returning "USDC:GCNY..."
413+ nameScVal := xdr.ScVal {Type : xdr .ScValTypeScvString , Str : ptrToScString ("USDC:GCNY5OXYSY4FKHOPT2SPOQZAOEIGXB5LBYW3HVU3OWSTQITS65M5RCNY" )}
414+
415+ mockRPCService .On ("SimulateTransaction" , mock .Anything , mock .Anything ).Return (
416+ entities.RPCSimulateTransactionResult {
417+ Results : []entities.RPCSimulateHostFunctionResult {{XDR : nameScVal }},
418+ }, nil ,
419+ )
420+
421+ service , err := NewContractMetadataService (mockRPCService , mockContractModel , pool )
422+ require .NoError (t , err )
423+
424+ result , err := service .FetchSACMetadata (ctx , []string {contractID })
425+
426+ require .NoError (t , err )
427+ require .Len (t , result , 1 )
428+
429+ contract := result [0 ]
430+ assert .Equal (t , contractID , contract .ContractID )
431+ assert .Equal (t , "SAC" , contract .Type )
432+ assert .Equal (t , "USDC" , * contract .Code )
433+ assert .Equal (t , "GCNY5OXYSY4FKHOPT2SPOQZAOEIGXB5LBYW3HVU3OWSTQITS65M5RCNY" , * contract .Issuer )
434+ assert .Equal (t , "USDC:GCNY5OXYSY4FKHOPT2SPOQZAOEIGXB5LBYW3HVU3OWSTQITS65M5RCNY" , * contract .Name )
435+ assert .Equal (t , "USDC" , * contract .Symbol )
436+ assert .Equal (t , uint32 (7 ), contract .Decimals )
437+ })
438+
439+ t .Run ("handles native XLM asset" , func (t * testing.T ) {
440+ mockRPCService := NewRPCServiceMock (t )
441+ mockContractModel := data .NewContractModelMock (t )
442+ pool := pond .NewPool (5 )
443+ defer pool .Stop ()
444+
445+ contractID := "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
446+
447+ // Mock name() returning "native"
448+ nameScVal := xdr.ScVal {Type : xdr .ScValTypeScvString , Str : ptrToScString ("native" )}
449+
450+ mockRPCService .On ("SimulateTransaction" , mock .Anything , mock .Anything ).Return (
451+ entities.RPCSimulateTransactionResult {
452+ Results : []entities.RPCSimulateHostFunctionResult {{XDR : nameScVal }},
453+ }, nil ,
454+ )
455+
456+ service , err := NewContractMetadataService (mockRPCService , mockContractModel , pool )
457+ require .NoError (t , err )
458+
459+ result , err := service .FetchSACMetadata (ctx , []string {contractID })
460+
461+ require .NoError (t , err )
462+ require .Len (t , result , 1 )
463+
464+ contract := result [0 ]
465+ assert .Equal (t , contractID , contract .ContractID )
466+ assert .Equal (t , "SAC" , contract .Type )
467+ assert .Equal (t , "XLM" , * contract .Code )
468+ assert .Equal (t , "" , * contract .Issuer )
469+ assert .Equal (t , "native" , * contract .Name )
470+ assert .Equal (t , "XLM" , * contract .Symbol )
471+ assert .Equal (t , uint32 (7 ), contract .Decimals )
472+ })
473+
474+ t .Run ("skips contract with malformed name gracefully" , func (t * testing.T ) {
475+ mockRPCService := NewRPCServiceMock (t )
476+ mockContractModel := data .NewContractModelMock (t )
477+ pool := pond .NewPool (5 )
478+ defer pool .Stop ()
479+
480+ contractID := "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
481+
482+ // Mock name() returning malformed value (no colon)
483+ nameScVal := xdr.ScVal {Type : xdr .ScValTypeScvString , Str : ptrToScString ("MALFORMED_NO_COLON" )}
484+
485+ mockRPCService .On ("SimulateTransaction" , mock .Anything , mock .Anything ).Return (
486+ entities.RPCSimulateTransactionResult {
487+ Results : []entities.RPCSimulateHostFunctionResult {{XDR : nameScVal }},
488+ }, nil ,
489+ )
490+
491+ service , err := NewContractMetadataService (mockRPCService , mockContractModel , pool )
492+ require .NoError (t , err )
493+
494+ result , err := service .FetchSACMetadata (ctx , []string {contractID })
495+
496+ // Should not error, but skip the malformed contract
497+ require .NoError (t , err )
498+ assert .Empty (t , result )
499+ })
500+
501+ t .Run ("skips contract when RPC fails gracefully" , func (t * testing.T ) {
502+ mockRPCService := NewRPCServiceMock (t )
503+ mockContractModel := data .NewContractModelMock (t )
504+ pool := pond .NewPool (5 )
505+ defer pool .Stop ()
506+
507+ contractID := "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
508+
509+ mockRPCService .On ("SimulateTransaction" , mock .Anything , mock .Anything ).Return (
510+ entities.RPCSimulateTransactionResult {}, errors .New ("RPC timeout" ),
511+ )
512+
513+ service , err := NewContractMetadataService (mockRPCService , mockContractModel , pool )
514+ require .NoError (t , err )
515+
516+ result , err := service .FetchSACMetadata (ctx , []string {contractID })
517+
518+ // Should not error, but skip the failed contract
519+ require .NoError (t , err )
520+ assert .Empty (t , result )
521+ })
522+
523+ t .Run ("processes multiple contracts successfully" , func (t * testing.T ) {
524+ mockRPCService := NewRPCServiceMock (t )
525+ mockContractModel := data .NewContractModelMock (t )
526+ pool := pond .NewPool (5 )
527+ defer pool .Stop ()
528+
529+ contractID1 := "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
530+ contractID2 := "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
531+
532+ // Mock responses for two contracts - use mock.Anything for both calls
533+ nameScVal1 := xdr.ScVal {Type : xdr .ScValTypeScvString , Str : ptrToScString ("USDC:GCNY5OXYSY4FKHOPT2SPOQZAOEIGXB5LBYW3HVU3OWSTQITS65M5RCNY" )}
534+ nameScVal2 := xdr.ScVal {Type : xdr .ScValTypeScvString , Str : ptrToScString ("native" )}
535+
536+ // Return different values for the two calls
537+ mockRPCService .On ("SimulateTransaction" , mock .Anything , mock .Anything ).Return (
538+ entities.RPCSimulateTransactionResult {
539+ Results : []entities.RPCSimulateHostFunctionResult {{XDR : nameScVal1 }},
540+ }, nil ,
541+ ).Once ()
542+ mockRPCService .On ("SimulateTransaction" , mock .Anything , mock .Anything ).Return (
543+ entities.RPCSimulateTransactionResult {
544+ Results : []entities.RPCSimulateHostFunctionResult {{XDR : nameScVal2 }},
545+ }, nil ,
546+ ).Once ()
547+
548+ service , err := NewContractMetadataService (mockRPCService , mockContractModel , pool )
549+ require .NoError (t , err )
550+
551+ result , err := service .FetchSACMetadata (ctx , []string {contractID1 , contractID2 })
552+
553+ require .NoError (t , err )
554+ assert .Len (t , result , 2 )
555+ })
556+ }
557+
384558func TestFetchBatch (t * testing.T ) {
385559 ctx := context .Background ()
386560
0 commit comments