Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions cmd/indexer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func main() {
contribRepo := contribution.NewRepository(db)
payoutRepo := payout.NewRepository(db)
reputationRepo := reputation.NewRepository(db)
_ = user.NewRepository(db) // wired for future account auto-creation
userRepo := user.NewRepository(db)

// --- Indexer components ---

Expand All @@ -66,7 +66,7 @@ func main() {
poller := indexer.NewPoller(cfg.Stellar.HorizonURL, contractIDs)
processor := indexer.NewEventProcessor(
db, rmqClient,
circleRepo, contribRepo, payoutRepo, reputationRepo,
circleRepo, contribRepo, payoutRepo, reputationRepo, userRepo,
)

// Wire WebSocket broadcast via Redis so API server instances
Expand Down
Binary file added indexer.exe
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS contract_events;
28 changes: 28 additions & 0 deletions internal/database/migrations/032_create_contract_events.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-- Append-only audit log of all Soroban contract events processed by the indexer.
-- Used for debugging, replay, and analytics. Rows are never updated or deleted.

CREATE TABLE IF NOT EXISTS contract_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tx_hash TEXT NOT NULL,
ledger BIGINT NOT NULL,
contract_id TEXT NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL DEFAULT '{}',
processed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Fast lookup by transaction hash (deduplication checks).
CREATE INDEX IF NOT EXISTS idx_contract_events_tx_hash
ON contract_events (tx_hash);

-- Fast lookup by event type (analytics, debugging).
CREATE INDEX IF NOT EXISTS idx_contract_events_event_type
ON contract_events (event_type);

-- Fast lookup by contract (per-circle event history).
CREATE INDEX IF NOT EXISTS idx_contract_events_contract_id
ON contract_events (contract_id);

-- Prevent duplicate events for the same tx + contract + type combination.
CREATE UNIQUE INDEX IF NOT EXISTS idx_contract_events_unique
ON contract_events (tx_hash, contract_id, event_type);
17 changes: 17 additions & 0 deletions internal/domain/user/mocks/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,20 @@ func (m *Repository) Count(ctx context.Context, filter user.UserFilter) (int, er
args := m.Called(ctx, filter)
return args.Int(0), args.Error(1)
}

func (m *Repository) Delete(ctx context.Context, id uuid.UUID) error {
return m.Called(ctx, id).Error(0)
}

func (m *Repository) FindByPasskeyCredentialID(ctx context.Context, credentialID string) (*user.User, error) {
args := m.Called(ctx, credentialID)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*user.User), args.Error(1)
}

func (m *Repository) ClaimNextName(ctx context.Context) (int64, error) {
args := m.Called(ctx)
return args.Get(0).(int64), args.Error(1)
}
128 changes: 128 additions & 0 deletions internal/indexer/events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package indexer

// Contract event type constants — must match the Symbol topics emitted by the
// Soroban contracts (CircleFactory, Circle, ReputationRegistry, Treasury).
const (
EventCircleCreated = "CircleCreated"
EventMemberJoined = "MemberJoined"
EventContributionReceived = "ContributionReceived"
EventPayoutExecuted = "PayoutExecuted"
EventLateReported = "LateReported"
EventMemberExited = "MemberExited"
EventDefaultRecorded = "DefaultRecorded"
EventCircleCompleted = "CircleCompleted"
EventAuctionBid = "AuctionBid"
EventVoteCast = "VoteCast"
EventDisputeRaised = "DisputeRaised"
EventFeeDeposited = "FeeDeposited"
)

// ContractEvent is a fully-decoded Soroban contract event extracted from a
// Stellar transaction's result_meta_xdr. All 12 event types share this struct;
// the typed payload structs below are used only within handler implementations.
type ContractEvent struct {
// ContractID is the Soroban contract address that emitted the event.
ContractID string `json:"contract_id"`
// EventType corresponds to one of the EventXxx constants above.
EventType string `json:"event_type"`
// Ledger is the Stellar ledger sequence number containing this event.
Ledger int64 `json:"ledger"`
// TxHash is the transaction hash that produced this event.
TxHash string `json:"tx_hash"`
// Payload is a flat map of decoded XDR field names → Go-native values.
// Keys and value types match the typed payload structs below.
Payload map[string]any `json:"payload"`
}

// ---------------------------------------------------------------------------
// Typed payload structs — one per event type.
// These are used inside handler functions to safely extract Payload fields.
// ---------------------------------------------------------------------------

// CircleCreatedPayload corresponds to CircleCreated(circle_id, creator, config_hash).
type CircleCreatedPayload struct {
CircleID string `json:"circle_id"`
Creator string `json:"creator"`
ConfigHash string `json:"config_hash"`
}

// MemberJoinedPayload corresponds to MemberJoined(circle_id, member, contribution_amount).
type MemberJoinedPayload struct {
CircleID string `json:"circle_id"`
Member string `json:"member"`
ContributionAmount float64 `json:"contribution_amount"`
}

// ContributionReceivedPayload corresponds to ContributionReceived(circle_id, member, amount, round).
type ContributionReceivedPayload struct {
CircleID string `json:"circle_id"`
Member string `json:"member"`
Amount float64 `json:"amount"`
Round int `json:"round"`
}

// PayoutExecutedPayload corresponds to PayoutExecuted(circle_id, recipient, amount, round, payout_type).
type PayoutExecutedPayload struct {
CircleID string `json:"circle_id"`
Recipient string `json:"recipient"`
Amount float64 `json:"amount"`
Round int `json:"round"`
PayoutType string `json:"payout_type"`
}

// LateReportedPayload corresponds to LateReported(circle_id, member, penalty_amount, strikes).
type LateReportedPayload struct {
CircleID string `json:"circle_id"`
Member string `json:"member"`
PenaltyAmount float64 `json:"penalty_amount"`
Strikes int `json:"strikes"`
}

// MemberExitedPayload corresponds to MemberExited(circle_id, member, penalty).
type MemberExitedPayload struct {
CircleID string `json:"circle_id"`
Member string `json:"member"`
Penalty float64 `json:"penalty"`
}

// DefaultRecordedPayload corresponds to DefaultRecorded(circle_id, member).
type DefaultRecordedPayload struct {
CircleID string `json:"circle_id"`
Member string `json:"member"`
}

// CircleCompletedPayload corresponds to CircleCompleted(circle_id, total_contributions).
type CircleCompletedPayload struct {
CircleID string `json:"circle_id"`
TotalContributions float64 `json:"total_contributions"`
}

// AuctionBidPayload corresponds to AuctionBid(circle_id, bidder, discount_bips, round).
type AuctionBidPayload struct {
CircleID string `json:"circle_id"`
Bidder string `json:"bidder"`
DiscountBips int `json:"discount_bips"`
Round int `json:"round"`
}

// VoteCastPayload corresponds to VoteCast(circle_id, voter, vote_for, round).
type VoteCastPayload struct {
CircleID string `json:"circle_id"`
Voter string `json:"voter"`
VoteFor string `json:"vote_for"`
Round int `json:"round"`
}

// DisputeRaisedPayload corresponds to DisputeRaised(circle_id, member, evidence_hash).
type DisputeRaisedPayload struct {
CircleID string `json:"circle_id"`
Member string `json:"member"`
EvidenceHash string `json:"evidence_hash"`
}

// FeeDepositedPayload corresponds to the Treasury contract's FeeDeposited event.
type FeeDepositedPayload struct {
CircleID string `json:"circle_id"`
Amount float64 `json:"amount"`
TxHash string `json:"tx_hash"`
}
4 changes: 4 additions & 0 deletions internal/indexer/poller.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ type Operation struct {
ID int64 `json:"id"`
Type string `json:"type"`
SourceAccount string `json:"source_account"`
// ResultMetaXDR is the base64-encoded TransactionMeta XDR from Horizon.
// Populated for invoke_host_function operations and used by the XDR parser
// to extract Soroban contract events (ContractEvent topic/data SCVals).
ResultMetaXDR string `json:"result_meta_xdr"`
}

// NewPoller creates a Poller that queries the given Horizon URL and filters
Expand Down
Loading
Loading