Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
d2cf7e3
Add contract events processor for allowance state changes
aditya1702 Sep 8, 2025
b950404
Update events.go
aditya1702 Sep 8, 2025
119b589
Add tests for V4 and error cases in EventsProcessor
aditya1702 Sep 8, 2025
d3e09d0
make check
aditya1702 Sep 8, 2025
789ea47
Add trustline state change handling to EffectsProcessor
aditya1702 Sep 9, 2025
88b0ba4
Add support for sequence bump effects in state changes
aditya1702 Sep 9, 2025
76d201d
make check
aditya1702 Sep 9, 2025
ec2bce4
Update indexer.go
aditya1702 Sep 9, 2025
35a30d8
Fix test
aditya1702 Sep 9, 2025
bf4773d
Ignore invalid and insufficient contract events
aditya1702 Sep 9, 2025
7b3623d
Add TODO
aditya1702 Sep 9, 2025
9552f9a
Update events.go
aditya1702 Sep 9, 2025
36e3461
remove allowance changes
aditya1702 Sep 10, 2025
1705038
Add trustline_limit to state_changes and processing
aditya1702 Sep 10, 2025
193e307
Refactor trustline flag state change categories and reasons
aditya1702 Sep 10, 2025
aee26fb
Rename trustline flag category to BalanceAuthorization
aditya1702 Sep 11, 2025
83a62a2
Add contract events processor and enhance trustline handling
aditya1702 Sep 11, 2025
9a59b03
Refactor EffectsProcessor to use LedgerEntryProvider interface
aditya1702 Sep 11, 2025
ff11404
Refactor effects test to use mock LedgerEntryProvider
aditya1702 Sep 11, 2025
308971c
Update effects_test.go
aditya1702 Sep 11, 2025
c26eac9
Refactor contract event processing and add SAC processor
aditya1702 Sep 12, 2025
35361c4
Add tests for SACEventsProcessor and contract event utils
aditya1702 Sep 12, 2025
472dd8b
Refactor indexer processors and update SAC tests
aditya1702 Sep 12, 2025
335e14b
add some function docstrings
aditya1702 Sep 12, 2025
3b83660
Update sac.go
aditya1702 Sep 12, 2025
4ee0ac4
Add debug logging for skipped SAC contract events
aditya1702 Sep 12, 2025
b9f8e86
Refactor SAC contract event processing and utils
aditya1702 Sep 15, 2025
4303c23
Normalize flag names for authorization and clawback
aditya1702 Sep 15, 2025
2e64ab3
Use StateChangeReasonAdd for trustline creation
aditya1702 Sep 15, 2025
5ece003
Refine SAC processor to detect actual trustline flag changes
aditya1702 Sep 15, 2025
a85b97a
Add edge case tests for SAC trustline flag changes
aditya1702 Sep 15, 2025
3500173
Remove sequence bump effect processing
aditya1702 Sep 15, 2025
cbd20c2
Rename state change reason from Remove to Clear
aditya1702 Sep 15, 2025
2ede386
Handle contract account authorization in SAC processor
aditya1702 Sep 15, 2025
b61bc72
Add contract account authorization tests and helpers
aditya1702 Sep 16, 2025
ce23bae
Update sac.go
aditya1702 Sep 16, 2025
72239e8
Update sac.go
aditya1702 Sep 16, 2025
de75b1b
Filter SAC trustline and contract data changes by asset
aditya1702 Sep 16, 2025
b608421
Add tests for handling missing previous state in SAC events
aditya1702 Sep 16, 2025
4e02495
Refactor test transaction creation utilities
aditya1702 Sep 16, 2025
ca9e999
Rename TxBuilder to testTxBuilder in test_utils.go
aditya1702 Sep 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ require (
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.20.1
github.com/stellar/go v0.0.0-20250822224526-9397ce4b6da2
github.com/stellar/go v0.0.0-20250903085211-00c0b06cd7cc
github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2
github.com/stellar/stellar-rpc v0.9.6-0.20250523162628-6bb9d7a387d5
github.com/stretchr/testify v1.10.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,8 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stellar/go v0.0.0-20250822224526-9397ce4b6da2 h1:nZEjpYUZ+0cYqZXhYO+QiJcwPQnwTrEs2bWsbRv/Tz8=
github.com/stellar/go v0.0.0-20250822224526-9397ce4b6da2/go.mod h1:ac8hwpljbFXC3Sf9nGfqBXXEvAEdnNRqQHGqP7QN8oY=
github.com/stellar/go v0.0.0-20250903085211-00c0b06cd7cc h1:iRveajcdqBdOlp5U6ZCts9dr1J4VM3LJ8sVnpHYkoVE=
github.com/stellar/go v0.0.0-20250903085211-00c0b06cd7cc/go.mod h1:ac8hwpljbFXC3Sf9nGfqBXXEvAEdnNRqQHGqP7QN8oY=
github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2 h1:OzCVd0SV5qE3ZcDeSFCmOWLZfEWZ3Oe8KtmSOYKEVWE=
github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2/go.mod h1:yoxyU/M8nl9LKeWIoBrbDPQ7Cy+4jxRcWcOayZ4BMps=
github.com/stellar/stellar-rpc v0.9.6-0.20250523162628-6bb9d7a387d5 h1:V+XezRLVHuk6c1nMkXkWjCwtoHN7F+DK86dK2kkNSZo=
Expand Down
11 changes: 10 additions & 1 deletion internal/indexer/indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type Indexer struct {
tokenTransferProcessor TokenTransferProcessorInterface
effectsProcessor OperationProcessorInterface
contractDeployProcessor OperationProcessorInterface
contractEventsProcessor OperationProcessorInterface
}

func NewIndexer(networkPassphrase string) *Indexer {
Expand All @@ -54,6 +55,7 @@ func NewIndexer(networkPassphrase string) *Indexer {
tokenTransferProcessor: processors.NewTokenTransferProcessor(networkPassphrase),
effectsProcessor: processors.NewEffectsProcessor(networkPassphrase),
contractDeployProcessor: processors.NewContractDeployProcessor(networkPassphrase),
contractEventsProcessor: processors.NewEventsProcessor(networkPassphrase),
}
}

Expand All @@ -80,7 +82,7 @@ func (i *Indexer) ProcessTransaction(ctx context.Context, transaction ingest.Led
return fmt.Errorf("getting operations participants: %w", err)
}
var dataOp *types.Operation
var effectsStateChanges, contractDeployStateChanges []types.StateChange
var effectsStateChanges, contractDeployStateChanges, contractEventsStateChanges []types.StateChange
for opID, opParticipants := range opsParticipants {
dataOp, err = processors.ConvertOperation(&transaction, &opParticipants.OpWrapper.Operation, opID)
if err != nil {
Expand All @@ -104,6 +106,13 @@ func (i *Indexer) ProcessTransaction(ctx context.Context, transaction ingest.Led
return fmt.Errorf("processing contract deploy state changes: %w", err)
}
i.Buffer.PushStateChanges(contractDeployStateChanges)

// 2.3 Index contract events state changes
contractEventsStateChanges, err = i.contractEventsProcessor.ProcessOperation(ctx, opParticipants.OpWrapper)
if err != nil && !errors.Is(err, processors.ErrInvalidOpType) {
return fmt.Errorf("processing contract events state changes: %w", err)
}
i.Buffer.PushStateChanges(contractEventsStateChanges)
}

// 3. Index token transfer state changes
Expand Down
27 changes: 19 additions & 8 deletions internal/indexer/indexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,14 @@ var (
func TestIndexer_ProcessTransaction(t *testing.T) {
tests := []struct {
name string
setupMocks func(*MockParticipantsProcessor, *MockTokenTransferProcessor, *MockOperationProcessor, *MockOperationProcessor, *MockIndexerBuffer)
setupMocks func(*MockParticipantsProcessor, *MockTokenTransferProcessor, *MockOperationProcessor, *MockOperationProcessor, *MockOperationProcessor, *MockIndexerBuffer)
wantError string
txParticipants set.Set[string]
opsParticipants map[int64]processors.OperationParticipants
}{
{
name: "🟢 successful processing with participants",
setupMocks: func(mockParticipants *MockParticipantsProcessor, mockTokenTransfer *MockTokenTransferProcessor, mockEffects *MockOperationProcessor, mockContractDeploy *MockOperationProcessor, mockBuffer *MockIndexerBuffer) {
setupMocks: func(mockParticipants *MockParticipantsProcessor, mockTokenTransfer *MockTokenTransferProcessor, mockEffects *MockOperationProcessor, mockContractDeploy *MockOperationProcessor, mockEventsProcessor *MockOperationProcessor, mockBuffer *MockIndexerBuffer) {
participants := set.NewSet("alice", "bob")
mockParticipants.On("GetTransactionParticipants", mock.Anything).Return(participants, nil)

Expand All @@ -121,6 +121,9 @@ func TestIndexer_ProcessTransaction(t *testing.T) {
contractDeployStateChanges := []types.StateChange{{ToID: 3, StateChangeOrder: 1}}
mockContractDeploy.On("ProcessOperation", mock.Anything, mock.Anything).Return(contractDeployStateChanges, nil)

contractEventsStateChanges := []types.StateChange{{ToID: 4, StateChangeOrder: 1}}
mockEventsProcessor.On("ProcessOperation", mock.Anything, mock.Anything).Return(contractEventsStateChanges, nil)

// Verify transaction was pushed to buffer with correct participants
// PushParticipantTransaction is called once for each participant
mockBuffer.On("PushParticipantTransaction", "alice",
Expand Down Expand Up @@ -157,6 +160,10 @@ func TestIndexer_ProcessTransaction(t *testing.T) {
mock.MatchedBy(func(stateChanges []types.StateChange) bool {
return len(stateChanges) == 1 && stateChanges[0].ToID == 3 && stateChanges[0].StateChangeOrder == 1
})).Return()
mockBuffer.On("PushStateChanges",
mock.MatchedBy(func(stateChanges []types.StateChange) bool {
return len(stateChanges) == 1 && stateChanges[0].ToID == 4 && stateChanges[0].StateChangeOrder == 1
})).Return()
},
txParticipants: set.NewSet("alice", "bob"),
opsParticipants: map[int64]processors.OperationParticipants{
Expand All @@ -173,7 +180,7 @@ func TestIndexer_ProcessTransaction(t *testing.T) {
},
{
name: "🟢 successful processing without participants",
setupMocks: func(mockParticipants *MockParticipantsProcessor, mockTokenTransfer *MockTokenTransferProcessor, mockEffects *MockOperationProcessor, mockContractDeploy *MockOperationProcessor, mockBuffer *MockIndexerBuffer) {
setupMocks: func(mockParticipants *MockParticipantsProcessor, mockTokenTransfer *MockTokenTransferProcessor, mockEffects *MockOperationProcessor, mockContractDeploy *MockOperationProcessor, mockEventsProcessor *MockOperationProcessor, mockBuffer *MockIndexerBuffer) {
participants := set.NewSet[string]()
mockParticipants.On("GetTransactionParticipants", mock.Anything).Return(participants, nil)

Expand All @@ -196,14 +203,14 @@ func TestIndexer_ProcessTransaction(t *testing.T) {
},
{
name: "🔴 error getting transaction participants",
setupMocks: func(mockParticipants *MockParticipantsProcessor, mockTokenTransfer *MockTokenTransferProcessor, mockEffects *MockOperationProcessor, mockContractDeploy *MockOperationProcessor, mockBuffer *MockIndexerBuffer) {
setupMocks: func(mockParticipants *MockParticipantsProcessor, mockTokenTransfer *MockTokenTransferProcessor, mockEffects *MockOperationProcessor, mockContractDeploy *MockOperationProcessor, mockEventsProcessor *MockOperationProcessor, mockBuffer *MockIndexerBuffer) {
mockParticipants.On("GetTransactionParticipants", mock.Anything).Return(set.NewSet[string](), errors.New("participant error"))
},
wantError: "getting transaction participants: participant error",
},
{
name: "🔴 error getting operations participants",
setupMocks: func(mockParticipants *MockParticipantsProcessor, mockTokenTransfer *MockTokenTransferProcessor, mockEffects *MockOperationProcessor, mockContractDeploy *MockOperationProcessor, mockBuffer *MockIndexerBuffer) {
setupMocks: func(mockParticipants *MockParticipantsProcessor, mockTokenTransfer *MockTokenTransferProcessor, mockEffects *MockOperationProcessor, mockContractDeploy *MockOperationProcessor, mockEventsProcessor *MockOperationProcessor, mockBuffer *MockIndexerBuffer) {
participants := set.NewSet[string]()
mockParticipants.On("GetTransactionParticipants", mock.Anything).Return(participants, nil)
mockParticipants.On("GetOperationsParticipants", mock.Anything).Return(map[int64]processors.OperationParticipants{}, errors.New("operations error"))
Expand All @@ -212,7 +219,7 @@ func TestIndexer_ProcessTransaction(t *testing.T) {
},
{
name: "🔴 error processing effects state changes",
setupMocks: func(mockParticipants *MockParticipantsProcessor, mockTokenTransfer *MockTokenTransferProcessor, mockEffects *MockOperationProcessor, mockContractDeploy *MockOperationProcessor, mockBuffer *MockIndexerBuffer) {
setupMocks: func(mockParticipants *MockParticipantsProcessor, mockTokenTransfer *MockTokenTransferProcessor, mockEffects *MockOperationProcessor, mockContractDeploy *MockOperationProcessor, mockEventsProcessor *MockOperationProcessor, mockBuffer *MockIndexerBuffer) {
participants := set.NewSet[string]()
mockParticipants.On("GetTransactionParticipants", mock.Anything).Return(participants, nil)

Expand All @@ -236,7 +243,7 @@ func TestIndexer_ProcessTransaction(t *testing.T) {
},
{
name: "🔴 error processing token transfer state changes",
setupMocks: func(mockParticipants *MockParticipantsProcessor, mockTokenTransfer *MockTokenTransferProcessor, mockEffects *MockOperationProcessor, mockContractDeploy *MockOperationProcessor, mockBuffer *MockIndexerBuffer) {
setupMocks: func(mockParticipants *MockParticipantsProcessor, mockTokenTransfer *MockTokenTransferProcessor, mockEffects *MockOperationProcessor, mockContractDeploy *MockOperationProcessor, mockEventsProcessor *MockOperationProcessor, mockBuffer *MockIndexerBuffer) {
participants := set.NewSet[string]()
mockParticipants.On("GetTransactionParticipants", mock.Anything).Return(participants, nil)

Expand All @@ -256,10 +263,11 @@ func TestIndexer_ProcessTransaction(t *testing.T) {
mockTokenTransfer := &MockTokenTransferProcessor{}
mockEffects := &MockOperationProcessor{}
mockContractDeploy := &MockOperationProcessor{}
mockEventsProcessor := &MockOperationProcessor{}
mockBuffer := &MockIndexerBuffer{}

// Setup mock expectations
tt.setupMocks(mockParticipants, mockTokenTransfer, mockEffects, mockContractDeploy, mockBuffer)
tt.setupMocks(mockParticipants, mockTokenTransfer, mockEffects, mockContractDeploy, mockEventsProcessor, mockBuffer)

// Create testable indexer with mocked dependencies
indexer := &Indexer{
Expand All @@ -268,6 +276,7 @@ func TestIndexer_ProcessTransaction(t *testing.T) {
tokenTransferProcessor: mockTokenTransfer,
effectsProcessor: mockEffects,
contractDeployProcessor: mockContractDeploy,
contractEventsProcessor: mockEventsProcessor,
}

err := indexer.ProcessTransaction(context.Background(), testTx)
Expand Down Expand Up @@ -305,6 +314,8 @@ func TestIndexer_ProcessTransaction(t *testing.T) {
mockParticipants.AssertExpectations(t)
mockTokenTransfer.AssertExpectations(t)
mockEffects.AssertExpectations(t)
mockContractDeploy.AssertExpectations(t)
mockEventsProcessor.AssertExpectations(t)
mockBuffer.AssertExpectations(t)
})
}
Expand Down
68 changes: 68 additions & 0 deletions internal/indexer/processors/effects.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/stellar/go/ingest"
effects "github.com/stellar/go/processors/effects"
operation_processor "github.com/stellar/go/processors/operation"
"github.com/stellar/go/strkey"
"github.com/stellar/go/xdr"

"github.com/stellar/go/support/log"
Expand Down Expand Up @@ -100,6 +101,11 @@ func (p *EffectsProcessor) ProcessOperation(_ context.Context, opWrapper *operat
// Process different types of effects based on what account property they modify
//exhaustive:ignore
switch effectType {
case effects.EffectSequenceBumped:
stateChanges = append(stateChanges, changeBuilder.
WithCategory(types.StateChangeCategorySequence).
WithReason(types.StateChangeReasonSequenceBump).
Build())
// Signer effects: track changes to account signers (add/remove/update signer keys and weights)
case effects.EffectSignerCreated, effects.EffectSignerRemoved, effects.EffectSignerUpdated:
changeBuilder = changeBuilder.
Expand Down Expand Up @@ -135,6 +141,15 @@ func (p *EffectsProcessor) ProcessOperation(_ context.Context, opWrapper *operat
changeBuilder = changeBuilder.WithCategory(types.StateChangeCategoryTrustlineFlags)
stateChanges = append(stateChanges, p.parseFlags(trustlineFlags, changeBuilder, &effect)...)

// Change trust effects
case effects.EffectTrustlineCreated, effects.EffectTrustlineRemoved, effects.EffectTrustlineUpdated:
changeBuilder = changeBuilder.WithCategory(types.StateChangeCategoryTrustline)
trustlineChange, err := p.parseTrustline(changeBuilder, &effect, effectType, changes)
if err != nil {
return nil, fmt.Errorf("parsing trustline effect: effectType: %s, address: %s, txHash: %s, opID: %d, err: %w", effect.TypeString, effect.Address, txHash, opWrapper.ID(), err)
}
stateChanges = append(stateChanges, trustlineChange)

// Data entry effects: track changes to account data entries (key-value storage)
case effects.EffectDataCreated, effects.EffectDataRemoved, effects.EffectDataUpdated:
keyValueMap := p.parseKeyValue(&effect, effectType, changes, "name")
Expand Down Expand Up @@ -249,6 +264,59 @@ func (p *EffectsProcessor) createTargetSponsorshipChange(reason types.StateChang
return builder.Build()
}

func (p *EffectsProcessor) parseTrustline(baseBuilder *StateChangeBuilder, effect *effects.EffectOutput, effectType effects.EffectType, changes []ingest.Change) (types.StateChange, error) {
parseAsset := func(assetType, assetCode, assetIssuer string) (string, error) {
var asset xdr.Asset

switch assetType {
case "native":
asset = xdr.Asset{
Type: xdr.AssetTypeAssetTypeNative,
}
case "credit_alphanum4", "credit_alphanum12":
asset = xdr.MustNewCreditAsset(assetCode, assetIssuer)
default:
return "", fmt.Errorf("invalid asset type: %s", assetType)
}

contractID, err := asset.ContractID(p.networkPassphrase)
if err != nil {
return "", fmt.Errorf("getting asset contract ID: %w", err)
}

return strkey.MustEncode(strkey.VersionByteContract, contractID[:]), nil
}

var change types.StateChange
assetStr, err := parseAsset(effect.Details["asset_type"].(string), effect.Details["asset_code"].(string), effect.Details["asset_issuer"].(string))
if err != nil {
return types.StateChange{}, fmt.Errorf("parsing asset: %w", err)
}
//exhaustive:ignore
switch effectType {
case effects.EffectTrustlineCreated:
change = baseBuilder.WithReason(types.StateChangeReasonSet).WithToken(assetStr).WithKeyValue(
map[string]any{
"limit": effect.Details["limit"],
},
).Build()
case effects.EffectTrustlineRemoved:
change = baseBuilder.WithReason(types.StateChangeReasonRemove).WithToken(assetStr).Build()
case effects.EffectTrustlineUpdated:
prevLedgerEntryState := p.getPrevLedgerEntryState(effect, xdr.LedgerEntryTypeTrustline, changes)
prevTrustline := prevLedgerEntryState.Data.MustTrustLine()
change = baseBuilder.WithReason(types.StateChangeReasonUpdate).WithToken(assetStr).WithKeyValue(
map[string]any{
"limit": map[string]any{
"old": strconv.FormatInt(int64(prevTrustline.Limit), 10),
"new": effect.Details["limit"],
},
},
).Build()
}
return change, nil
}

// parseKeyValue extracts specified key-value pairs from effect details.
// This is used to capture metadata like home domain values or data entry names and values.
func (p *EffectsProcessor) parseKeyValue(effect *effects.EffectOutput, effectType effects.EffectType, changes []ingest.Change, key string) map[string]any {
Expand Down
Loading