Skip to content

Commit 49cad15

Browse files
committed
return error for metadata fetching failures
1 parent ab8c062 commit 49cad15

File tree

3 files changed

+57
-18
lines changed

3 files changed

+57
-18
lines changed

internal/services/contract_metadata.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,15 +126,17 @@ func (s *contractMetadataService) FetchSep41Metadata(ctx context.Context, contra
126126
// FetchSACMetadata fetches metadata for SAC contracts by calling name() via RPC.
127127
// SAC contracts return "code:issuer" format from name() (or "native" for XLM).
128128
// Returns []*data.Contract with Code, Issuer, Name, Symbol, and Decimals=7 (hardcoded for Stellar assets).
129+
// This function fails fast if any contract metadata fetch fails - partial results are not returned.
129130
func (s *contractMetadataService) FetchSACMetadata(ctx context.Context, contractIDs []string) ([]*data.Contract, error) {
130131
if len(contractIDs) == 0 {
131132
return []*data.Contract{}, nil
132133
}
133134

134135
start := time.Now()
135136
var (
136-
contracts []*data.Contract
137-
mu sync.Mutex
137+
contracts []*data.Contract
138+
mu sync.Mutex
139+
fetchErrors []error
138140
)
139141

140142
// Process in batches to avoid overwhelming the RPC
@@ -147,7 +149,9 @@ func (s *contractMetadataService) FetchSACMetadata(ctx context.Context, contract
147149
group.Submit(func() {
148150
contract, err := s.fetchSACMetadataForContract(ctx, contractID)
149151
if err != nil {
150-
log.Ctx(ctx).Warnf("Failed to fetch SAC metadata for contract %s: %v", contractID, err)
152+
mu.Lock()
153+
fetchErrors = append(fetchErrors, fmt.Errorf("contract %s: %w", contractID, err))
154+
mu.Unlock()
151155
return
152156
}
153157
mu.Lock()
@@ -157,7 +161,7 @@ func (s *contractMetadataService) FetchSACMetadata(ctx context.Context, contract
157161
}
158162

159163
if err := group.Wait(); err != nil {
160-
log.Ctx(ctx).Warnf("Error waiting for SAC metadata batch: %v", err)
164+
return nil, fmt.Errorf("error in SAC metadata batch: %w", err)
161165
}
162166

163167
// Sleep between batches to avoid overwhelming the RPC (skip for last batch)
@@ -166,6 +170,11 @@ func (s *contractMetadataService) FetchSACMetadata(ctx context.Context, contract
166170
}
167171
}
168172

173+
// Fail if any contract metadata fetch failed - partial results are not acceptable
174+
if len(fetchErrors) > 0 {
175+
return nil, fmt.Errorf("failed to fetch metadata for %d SAC contracts: %w", len(fetchErrors), errors.Join(fetchErrors...))
176+
}
177+
169178
log.Ctx(ctx).Infof("Fetched metadata for %d SAC contracts in %.4f seconds", len(contracts), time.Since(start).Seconds())
170179
return contracts, nil
171180
}

internal/services/contract_metadata_test.go

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ func TestFetchSACMetadata(t *testing.T) {
471471
assert.Equal(t, uint32(7), contract.Decimals)
472472
})
473473

474-
t.Run("skips contract with malformed name gracefully", func(t *testing.T) {
474+
t.Run("returns error for contract with malformed name", func(t *testing.T) {
475475
mockRPCService := NewRPCServiceMock(t)
476476
mockContractModel := data.NewContractModelMock(t)
477477
pool := pond.NewPool(5)
@@ -493,12 +493,14 @@ func TestFetchSACMetadata(t *testing.T) {
493493

494494
result, err := service.FetchSACMetadata(ctx, []string{contractID})
495495

496-
// Should not error, but skip the malformed contract
497-
require.NoError(t, err)
498-
assert.Empty(t, result)
496+
// Should return error for malformed contract name
497+
require.Error(t, err)
498+
assert.Nil(t, result)
499+
assert.Contains(t, err.Error(), "failed to fetch metadata")
500+
assert.Contains(t, err.Error(), "malformed SAC name")
499501
})
500502

501-
t.Run("skips contract when RPC fails gracefully", func(t *testing.T) {
503+
t.Run("returns error when RPC fails", func(t *testing.T) {
502504
mockRPCService := NewRPCServiceMock(t)
503505
mockContractModel := data.NewContractModelMock(t)
504506
pool := pond.NewPool(5)
@@ -515,9 +517,11 @@ func TestFetchSACMetadata(t *testing.T) {
515517

516518
result, err := service.FetchSACMetadata(ctx, []string{contractID})
517519

518-
// Should not error, but skip the failed contract
519-
require.NoError(t, err)
520-
assert.Empty(t, result)
520+
// Should return error when RPC fails
521+
require.Error(t, err)
522+
assert.Nil(t, result)
523+
assert.Contains(t, err.Error(), "failed to fetch metadata")
524+
assert.Contains(t, err.Error(), "RPC timeout")
521525
})
522526

523527
t.Run("processes multiple contracts successfully", func(t *testing.T) {
@@ -553,6 +557,38 @@ func TestFetchSACMetadata(t *testing.T) {
553557
require.NoError(t, err)
554558
assert.Len(t, result, 2)
555559
})
560+
561+
t.Run("returns error and no partial results when one contract fails", func(t *testing.T) {
562+
mockRPCService := NewRPCServiceMock(t)
563+
mockContractModel := data.NewContractModelMock(t)
564+
pool := pond.NewPool(5)
565+
defer pool.Stop()
566+
567+
contractID1 := "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"
568+
contractID2 := "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
569+
570+
// First contract succeeds, second fails
571+
nameScVal1 := xdr.ScVal{Type: xdr.ScValTypeScvString, Str: ptrToScString("USDC:GCNY5OXYSY4FKHOPT2SPOQZAOEIGXB5LBYW3HVU3OWSTQITS65M5RCNY")}
572+
573+
mockRPCService.On("SimulateTransaction", mock.Anything, mock.Anything).Return(
574+
entities.RPCSimulateTransactionResult{
575+
Results: []entities.RPCSimulateHostFunctionResult{{XDR: nameScVal1}},
576+
}, nil,
577+
).Once()
578+
mockRPCService.On("SimulateTransaction", mock.Anything, mock.Anything).Return(
579+
entities.RPCSimulateTransactionResult{}, errors.New("RPC timeout"),
580+
).Once()
581+
582+
service, err := NewContractMetadataService(mockRPCService, mockContractModel, pool)
583+
require.NoError(t, err)
584+
585+
result, err := service.FetchSACMetadata(ctx, []string{contractID1, contractID2})
586+
587+
// Should return error and no partial results
588+
require.Error(t, err)
589+
assert.Nil(t, result)
590+
assert.Contains(t, err.Error(), "failed to fetch metadata for 1 SAC contracts")
591+
})
556592
}
557593

558594
func TestFetchBatch(t *testing.T) {

internal/services/token_ingestion.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,6 @@ func (s *tokenIngestionService) PopulateAccountTokens(ctx context.Context, check
254254
}
255255
}
256256
if len(sacContractsNeedingMetadata) > 0 {
257-
log.Ctx(ctx).Infof("Fetching metadata for %d SAC contracts via RPC", len(sacContractsNeedingMetadata))
258257
sacContracts, txErr := s.contractMetadataService.FetchSACMetadata(ctx, sacContractsNeedingMetadata)
259258
if txErr != nil {
260259
return fmt.Errorf("fetching SAC metadata: %w", txErr)
@@ -792,11 +791,6 @@ func (s *tokenIngestionService) storeTokensInDB(
792791
contractTokens = append(contractTokens, contract)
793792
}
794793
if len(contractTokens) > 0 {
795-
for _, contract := range contractTokens {
796-
if contract.Type == string(types.ContractTypeSAC) && contract.Code == nil {
797-
log.Ctx(ctx).Warnf("Contract %s is missing code and issuer", contract.ID)
798-
}
799-
}
800794
if err := s.contractModel.BatchInsert(ctx, dbTx, contractTokens); err != nil {
801795
return fmt.Errorf("batch inserting contracts: %w", err)
802796
}

0 commit comments

Comments
 (0)