Skip to content

Feat/soroban event polling indexer - #446

Merged
Luluameh merged 3 commits into
LightForgeHub:mainfrom
devchant:feat/soroban-event-polling-indexer
Jul 28, 2026
Merged

Feat/soroban event polling indexer#446
Luluameh merged 3 commits into
LightForgeHub:mainfrom
devchant:feat/soroban-event-polling-indexer

Conversation

@devchant

@devchant devchant commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Pull Request: Stellar / Soroban Escrow & Dispute Smart Contract Event Polling Indexer

1. Problem Being Solved

To maintain consistency between on-chain contract interactions on Stellar/Soroban and the application database, the platform requires a reliable backend indexing service. Without this service, session states (e.g. funding, pausing, refunds, completions) and financial transaction logs in PostgreSQL become out of sync with on-chain Escrow and Dispute smart contract operations.

2. Implemented Solution

We built a resilient TypeScript event polling service using @stellar/stellar-sdk and PostgreSQL:

  • Interval Poller (src/indexer/poller.ts): Periodically queries Soroban RPC (rpc.Server.getEvents) for events emitted by Escrow (ESCROW_CONTRACT_ID) and Dispute (DISPUTE_CONTRACT_ID) smart contracts.
  • Safe XDR Decoder (src/indexer/decoder.ts): Converts binary XDR event topics and payloads into readable JSON data for topics (fund_session, pause_session, refund_session, complete_session, resolve_dispute). All XDR parsing is wrapped in non-crashing try/catch fallbacks.
  • Idempotent Storage & State Update (src/indexer/db/client.ts): Records processed event IDs in processed_events to suppress duplicate events and updates sessions and transactions tables within atomic SQL transactions.
  • Checkpoint Persistence: Stores the last_ledger_sequence in ledger_checkpoints so that service restarts resume polling from the exact saved ledger checkpoint.

3. Files and Components Changed

  • package.json: Added @stellar/stellar-sdk, pg, @types/pg, tsx dependencies and added indexer (npm run indexer) and test:indexer (npm run test:indexer) scripts.
  • src/indexer/config.ts: Environment variable loader for Soroban RPC URL, contract IDs, poll interval, batch limit, and database URL.
  • src/indexer/db/schema.sql: PostgreSQL table definitions for ledger_checkpoints, processed_events, sessions, and transactions.
  • src/indexer/db/client.ts: Database client abstraction supporting PostgreSQL (pg.Pool) and in-memory mock store fallback.
  • src/indexer/decoder.ts: Robust XDR topic & payload decoder.
  • src/indexer/poller.ts: Event polling loop & idempotency manager.
  • src/indexer/index.ts: CLI entry point with graceful signal handlers (SIGINT, SIGTERM).
  • docs/INDEXER_SETUP.md: Documentation for environment setup and running the indexer locally.
  • tests/indexer.spec.ts: Integration & unit test suite.

4. Technical Decisions Made

  • Dual DB Adapter: Implemented both PostgresDBClient for production database environments and InMemoryDBClient for isolated testing without requiring an external PostgreSQL instance.
  • Non-Crashing Error Handling: Designed the EventDecoder to produce safe fallback JSON records if malformed XDR or unknown payload structures are encountered, fulfilling the acceptance criterion that decoding failures do not crash the service.
  • Sub-5-Second Latency: Configured the default polling interval to 3000ms (POLL_INTERVAL_MS), ensuring database updates complete within 5 seconds of event emission.

5. Tests Added & Validation Performed

  • Event Decoding Test: Verified XDR topics and JSON payload normalization for fund_session, pause_session, refund_session, complete_session, resolve_dispute.
  • Corrupt Payload Test: Verified non-crash resilience when encountering corrupted XDR base64 data.
  • Event Latency Test: Simulated a contract event trigger and verified database updates within 273ms (< 5s threshold).
  • Idempotency Test: Verified duplicate event IDs are skipped without duplicating database updates.
  • Checkpoint Recovery Test: Verified that restarting the indexer resumes polling from the saved ledger sequence checkpoint.
  • Production Build Test: Verified npm run build completes cleanly.

Closes #

Summary by CodeRabbit

  • New Features

    • Added an event indexer that monitors contract activity, decodes events, tracks progress, and updates session and transaction records.
    • Added PostgreSQL persistence with an in-memory fallback for local or test environments.
    • Added configuration support for RPC endpoints, contract identifiers, polling, and database settings.
    • Added documentation covering indexer setup, operation, schema, and supported events.
  • Tests

    • Added comprehensive coverage for event decoding, duplicate-event handling, checkpoint recovery, disputes, sessions, fees, settlements, treasury operations, and validation errors.

@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

@devchant is attempting to deploy a commit to the luluameh's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds comprehensive Soroban contract execution snapshots covering session, dispute, fee, staking, treasury, and validation flows. Introduces a configurable Soroban event indexer with event decoding, PostgreSQL or in-memory persistence, checkpointing, polling, graceful shutdown, documentation, and integration tests.

Changes

Contract regression snapshots

Layer / File(s) Summary
Contract execution snapshot coverage
contracts/test_snapshots/test/*.json
Adds complete expected authorization traces, ledger states, and emitted events for session lifecycle, disputes, fee tiers, staking, treasury operations, validation failures, pause/resume, and settlement scenarios.

Soroban event indexer

Layer / File(s) Summary
Indexer foundation and persistence
src/indexer/config.ts, src/indexer/db/*, package.json
Adds environment-based configuration, database interfaces, PostgreSQL schema and transactions, in-memory fallback storage, and related scripts and packages.
Decoding, polling, and runtime lifecycle
src/indexer/decoder.ts, src/indexer/poller.ts, src/indexer/index.ts, pages/test.tsx, docs/INDEXER_SETUP.md
Adds non-throwing event decoding, checkpointed RPC polling, topic-to-state mapping, simulated events, daemon startup/shutdown, client-side page execution, and setup documentation.
Indexer validation
tests/indexer.spec.ts
Tests decoding, malformed events, simulated polling, idempotency, checkpoint recovery, and dispute resolution updates.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

  • LightForgeHub/SkillSphere issue 3 — Covers the same Soroban event indexer objectives: polling, XDR decoding, PostgreSQL updates, idempotency, checkpoint recovery, and failure handling.

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a Soroban event polling indexer.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Luluameh
Luluameh merged commit 991a6cd into LightForgeHub:main Jul 28, 2026
1 of 3 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (3)
src/indexer/config.ts (1)

11-21: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider failing fast (or warning loudly) on missing contract ID configuration.

escrowContractId/disputeContractId silently fall back to hardcoded placeholder values when env vars are absent. A misconfigured deployment (e.g., typo'd env var name) would start successfully and poll indefinitely without ever matching real events, with no signal beyond a normal-looking startup log in index.ts.

♻️ Suggested validation
 export function loadIndexerConfig(): IndexerConfig {
-  return {
-    rpcUrl: process.env.SOROBAN_RPC_URL || 'https://soroban-testnet.stellar.org',
-    escrowContractId: process.env.ESCROW_CONTRACT_ID || 'CC3W26Q6Q32A7O5ZFX2Q37W7EX5R2N3O5W4Q2Z5W4Q2Z5W4Q2Z5W4Q2Z',
-    disputeContractId: process.env.DISPUTE_CONTRACT_ID || 'CD3W26Q6Q32A7O5ZFX2Q37W7EX5R2N3O5W4Q2Z5W4Q2Z5W4Q2Z5W4Q2Z',
+  const escrowContractId = process.env.ESCROW_CONTRACT_ID || 'CC3W26Q6Q32A7O5ZFX2Q37W7EX5R2N3O5W4Q2Z5W4Q2Z5W4Q2Z5W4Q2Z';
+  const disputeContractId = process.env.DISPUTE_CONTRACT_ID || 'CD3W26Q6Q32A7O5ZFX2Q37W7EX5R2N3O5W4Q2Z5W4Q2Z5W4Q2Z5W4Q2Z';
+  if (!process.env.ESCROW_CONTRACT_ID || !process.env.DISPUTE_CONTRACT_ID) {
+    console.warn('[IndexerConfig] Using placeholder contract ID(s) — set ESCROW_CONTRACT_ID/DISPUTE_CONTRACT_ID for real indexing.');
+  }
+  return {
+    rpcUrl: process.env.SOROBAN_RPC_URL || 'https://soroban-testnet.stellar.org',
+    escrowContractId,
+    disputeContractId,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/indexer/config.ts` around lines 11 - 21, Update loadIndexerConfig so
missing ESCROW_CONTRACT_ID or DISPUTE_CONTRACT_ID configuration is detected
explicitly instead of silently using hardcoded placeholder values. Fail fast or
emit a prominent warning before returning the IndexerConfig, while preserving
valid environment-provided IDs and the existing configuration behavior for other
fields.
docs/INDEXER_SETUP.md (2)

8-8: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Topic list here omits the aliases documented in "Supported Contract Events".

Lines 106-119 claim start_session, cancel_session, and settle_session are also handled. Align the two lists so readers know the full accepted set.

📝 Proposed doc fix
-- **XDR Event Decoder**: Decodes binary XDR event topics and payloads into normalized JSON structures (`fund_session`, `pause_session`, `refund_session`, `complete_session`, `resolve_dispute`).
+- **XDR Event Decoder**: Decodes binary XDR event topics and payloads into normalized JSON structures (`fund_session`/`start_session`, `pause_session`, `refund_session`/`cancel_session`, `complete_session`/`settle_session`, `resolve_dispute`).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/INDEXER_SETUP.md` at line 8, Update the XDR Event Decoder event list in
the introductory documentation to include the aliases start_session,
cancel_session, and settle_session, matching the “Supported Contract Events”
list later in the document.

56-100: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep the docs schema in sync with src/indexer/db/schema.sql.

The inline SQL is a copy of the main schema and currently omits the source file’s CREATE INDEX statements for processed_events and transactions. Link to src/indexer/db/schema.sql or generate this block to avoid future drift.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/INDEXER_SETUP.md` around lines 56 - 100, The inline SQL schema in the
documentation is missing the indexes defined in src/indexer/db/schema.sql.
Update the documentation block to include the CREATE INDEX statements for
processed_events and transactions, or replace the copied schema with a link or
generated reference to the canonical schema.sql.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@contracts/test_snapshots/test/test_start_session_locks_tokens_and_creates_session.1.json`:
- Around line 1640-1690: The poller’s topic filter currently assumes the event
key is in topic[0], but the session-start event emitted by the contract uses the
two-topic shape (“session”, “started”). Update the poller’s event decoding and
filter configuration to match the emitted topic positions, ensuring
session-start events are routed correctly.

In `@docs/INDEXER_SETUP.md`:
- Around line 23-24: Update the ESCROW_CONTRACT_ID and DISPUTE_CONTRACT_ID
entries in the environment-variable table to replace the truncated example
values with either their complete default contract addresses or an explicit
indication that no default exists and the variables are required.

In `@package.json`:
- Around line 10-11: Add tsx to the package.json devDependencies alongside the
existing TypeScript tooling, keeping the indexer and test:indexer scripts
unchanged so their tsx commands resolve from node_modules/.bin.
- Line 23: Update the node-version setting in the frontend CI workflow to 22 so
dependency installation runs on the Node version required by
`@stellar/stellar-sdk` 16.x. Leave the existing dependency configuration
unchanged.

In `@src/indexer/db/client.ts`:
- Around line 69-126: Update PostgresDBClient.init() so its runtime schema
matches schema.sql by consolidating initialization around one source of truth.
Either load and execute schema.sql from init(), or extend the inline DDL to
create indexes on processed_events(contract_id),
processed_events(ledger_sequence), and transactions(session_id), using CREATE
INDEX IF NOT EXISTS.

In `@src/indexer/decoder.ts`:
- Around line 87-90: Update Decoder.normalizeNativeValue so bare bigint values
are not unconditionally returned under the amount key; preserve or derive the
correct semantic key needed by session-id extraction, including
session_id/sessionId/id when the native value represents a session identifier.
Keep amount labeling only for values known to represent amounts, and preserve
existing handling for null, undefined, and other scalar values.
- Around line 109-114: Update the fallback event ID logic in Decoder.decodeEvent
so events without rawEvent.id or rawEvent.eventId receive a deterministic
identifier derived from stable event fields, rather than Date.now() and
Math.random(). Ensure repeated decoding of the same underlying event produces
the same eventId while retaining native IDs when present.

In `@src/indexer/poller.ts`:
- Around line 158-236: Derive the network from the configured RPC target instead
of hardcoding `'testnet'` in each transaction record. Update the transaction
construction branches in the event-processing logic to use that derived network,
and include the same network on every `sessionUpdate` object so persisted
sessions and transactions preserve the actual RPC provenance.
- Around line 134-146: Move the lastProcessedLedger update out of the
pre-persistence section and apply it only after recordEventAndStateUpdate
completes successfully, while preserving the duplicate-event checkpoint
handling. Wrap each processSingleEvent invocation in the batch loop with an
independent try/catch so a failed event does not advance the in-memory
checkpoint or prevent later events from processing.

In `@tests/indexer.spec.ts`:
- Around line 56-92: The indexer test suite must not make live Soroban RPC
requests. Update the Test 3 setup around SorobanEventPoller and its RPC
dependency so getEvents is injected or stubbed locally while preserving
simulated-event processing and existing assertions; apply the same test-local
RPC isolation to other poller instances in this suite as needed.

---

Nitpick comments:
In `@docs/INDEXER_SETUP.md`:
- Line 8: Update the XDR Event Decoder event list in the introductory
documentation to include the aliases start_session, cancel_session, and
settle_session, matching the “Supported Contract Events” list later in the
document.
- Around line 56-100: The inline SQL schema in the documentation is missing the
indexes defined in src/indexer/db/schema.sql. Update the documentation block to
include the CREATE INDEX statements for processed_events and transactions, or
replace the copied schema with a link or generated reference to the canonical
schema.sql.

In `@src/indexer/config.ts`:
- Around line 11-21: Update loadIndexerConfig so missing ESCROW_CONTRACT_ID or
DISPUTE_CONTRACT_ID configuration is detected explicitly instead of silently
using hardcoded placeholder values. Fail fast or emit a prominent warning before
returning the IndexerConfig, while preserving valid environment-provided IDs and
the existing configuration behavior for other fields.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0d309ba3-5824-437f-8378-bc5d13a75da8

📥 Commits

Reviewing files that changed from the base of the PR and between 3f9e758 and a4cb138.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (81)
  • contracts/test_snapshots/test/test_1_second_session.1.json
  • contracts/test_snapshots/test/test_1_year_session_overflow_check.1.json
  • contracts/test_snapshots/test/test_admin_can_update_fee_tiers.1.json
  • contracts/test_snapshots/test/test_auto_resolve_expiry_refunds_seeker_after_30_days.1.json
  • contracts/test_snapshots/test/test_batch_settle_settles_multiple_sessions.1.json
  • contracts/test_snapshots/test/test_batch_settle_skips_nonexistent_sessions.1.json
  • contracts/test_snapshots/test/test_batch_settle_skips_sessions_belonging_to_other_expert.1.json
  • contracts/test_snapshots/test/test_calculate_claimable_amount_same_time_returns_zero.1.json
  • contracts/test_snapshots/test/test_calculate_platform_fee_uses_default_tiers.1.json
  • contracts/test_snapshots/test/test_collect_fee_increases_treasury_balance.1.json
  • contracts/test_snapshots/test/test_collect_fee_rejects_negative_amount.1.json
  • contracts/test_snapshots/test/test_collect_fee_rejects_zero_amount.1.json
  • contracts/test_snapshots/test/test_collect_multiple_fees_accumulates_balance.1.json
  • contracts/test_snapshots/test/test_expert_registration_and_availability.1.json
  • contracts/test_snapshots/test/test_expert_stake_above_tier_3_gets_tier_3_reduction.1.json
  • contracts/test_snapshots/test/test_expert_stake_between_tier_1_and_2_gets_tier_1_reduction.1.json
  • contracts/test_snapshots/test/test_expert_stake_between_tier_2_and_3_gets_tier_2_reduction.1.json
  • contracts/test_snapshots/test/test_expert_stake_just_below_tier_1_pays_full_fee.1.json
  • contracts/test_snapshots/test/test_expert_with_no_stake_pays_full_fee.1.json
  • contracts/test_snapshots/test/test_expert_with_tier_1_stake_gets_100_bps_reduction.1.json
  • contracts/test_snapshots/test/test_expert_with_tier_2_stake_gets_200_bps_reduction.1.json
  • contracts/test_snapshots/test/test_expert_with_tier_3_stake_gets_300_bps_reduction.1.json
  • contracts/test_snapshots/test/test_expiry_timestamp_uses_remaining_balance_and_rate.1.json
  • contracts/test_snapshots/test/test_fee_reduction_respects_base_fee_changes.1.json
  • contracts/test_snapshots/test/test_flag_dispute_rejects_invalid_cid.1.json
  • contracts/test_snapshots/test/test_flag_dispute_stores_evidence_cid.1.json
  • contracts/test_snapshots/test/test_get_current_earnings_caps_at_session_balance.1.json
  • contracts/test_snapshots/test/test_get_current_earnings_reflects_elapsed_time.1.json
  • contracts/test_snapshots/test/test_get_current_earnings_returns_zero_at_start.1.json
  • contracts/test_snapshots/test/test_get_current_earnings_zero_when_paused.1.json
  • contracts/test_snapshots/test/test_get_expert_staked_balance_returns_zero_for_new_expert.1.json
  • contracts/test_snapshots/test/test_get_staking_contract_returns_none_when_not_set.1.json
  • contracts/test_snapshots/test/test_get_treasury_address_returns_none_when_not_set.1.json
  • contracts/test_snapshots/test/test_get_treasury_balance_returns_zero_initially.1.json
  • contracts/test_snapshots/test/test_linear_streaming_caps_at_remaining_balance.1.json
  • contracts/test_snapshots/test/test_min_session_deposit_defaults_and_can_be_updated_by_admin.1.json
  • contracts/test_snapshots/test/test_multiple_settlements_track_milestones_without_ending_session.1.json
  • contracts/test_snapshots/test/test_only_participants_can_pause_or_resume.1.json
  • contracts/test_snapshots/test/test_partial_withdrawals_maintain_correct_balance.1.json
  • contracts/test_snapshots/test/test_pause_and_resume_preserve_accrued_amount.1.json
  • contracts/test_snapshots/test/test_protocol_pause_blocks_new_sessions.1.json
  • contracts/test_snapshots/test/test_protocol_pause_blocks_settlement_but_allows_refund_session.1.json
  • contracts/test_snapshots/test/test_resolve_dispute_splits_funds_by_percentage.1.json
  • contracts/test_snapshots/test/test_set_admin_and_fee_round_trip.1.json
  • contracts/test_snapshots/test/test_set_and_get_expert_referrer.1.json
  • contracts/test_snapshots/test/test_set_and_get_expert_staked_balance.1.json
  • contracts/test_snapshots/test/test_set_and_get_treasury_address.1.json
  • contracts/test_snapshots/test/test_set_expert_staked_balance_rejects_negative_amount.1.json
  • contracts/test_snapshots/test/test_set_staking_contract_address.1.json
  • contracts/test_snapshots/test/test_settle_session_after_funded_window_drains_and_finishes.1.json
  • contracts/test_snapshots/test/test_settle_session_pays_referrer_from_platform_fee.1.json
  • contracts/test_snapshots/test/test_settle_session_transfers_partial_milestone_payment.1.json
  • contracts/test_snapshots/test/test_start_session_accepts_cid_v1.1.json
  • contracts/test_snapshots/test/test_start_session_allows_expert_when_reputation_is_met.1.json
  • contracts/test_snapshots/test/test_start_session_fails_if_expert_not_registered.1.json
  • contracts/test_snapshots/test/test_start_session_fails_if_expert_unavailable.1.json
  • contracts/test_snapshots/test/test_start_session_fails_on_insufficient_balance.1.json
  • contracts/test_snapshots/test/test_start_session_fails_when_amount_is_below_minimum_deposit.1.json
  • contracts/test_snapshots/test/test_start_session_locks_tokens_and_creates_session.1.json
  • contracts/test_snapshots/test/test_start_session_rejects_invalid_metadata_cid.1.json
  • contracts/test_snapshots/test/test_start_session_rejects_low_reputation_expert.1.json
  • contracts/test_snapshots/test/test_start_session_stores_metadata_cid.1.json
  • contracts/test_snapshots/test/test_treasury_balance_survives_multiple_collect_and_withdraw_cycles.1.json
  • contracts/test_snapshots/test/test_treasury_tracks_multiple_tokens_separately.1.json
  • contracts/test_snapshots/test/test_update_session_notes.1.json
  • contracts/test_snapshots/test/test_withdraw_all_treasury_empties_balance.1.json
  • contracts/test_snapshots/test/test_withdraw_all_treasury_returns_zero_when_empty.1.json
  • contracts/test_snapshots/test/test_withdraw_treasury_fails_with_insufficient_balance.1.json
  • contracts/test_snapshots/test/test_withdraw_treasury_rejects_negative_amount.1.json
  • contracts/test_snapshots/test/test_withdraw_treasury_rejects_zero_amount.1.json
  • contracts/test_snapshots/test/test_withdraw_treasury_transfers_funds_and_updates_balance.1.json
  • docs/INDEXER_SETUP.md
  • package.json
  • pages/test.tsx
  • src/indexer/config.ts
  • src/indexer/db/client.ts
  • src/indexer/db/schema.sql
  • src/indexer/decoder.ts
  • src/indexer/index.ts
  • src/indexer/poller.ts
  • tests/indexer.spec.ts

Comment on lines +1640 to +1690
{
"event": {
"ext": "v0",
"contract_id": "0000000000000000000000000000000000000000000000000000000000000001",
"type_": "contract",
"body": {
"v0": {
"topics": [
{
"symbol": "session"
},
{
"symbol": "started"
}
],
"data": {
"vec": [
{
"u64": 1
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
},
{
"i128": {
"hi": 0,
"lo": 10
}
},
{
"i128": {
"hi": 0,
"lo": 300
}
},
{
"u64": 1000
},
{
"string": "QmYwAPJzv5CZsnAzt8auVZRnGzrYxkM4Tveoxu48UUfGz8"
}
]
}
}
}
},
"failed_call": false
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect topic emission in contract source vs indexer filter list
rg -nP --type=rust -C3 'publish\s*\(' contracts | head -80
rg -nP -C5 'isTargetEvent|decodeTopic' src/indexer

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 3728


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- indexer poller relevant slice ---\n'
sed -n '18,190p' src/indexer/poller.ts

printf '\n--- EventDecoder relevant slice ---\n'
sed -n '1,160p' src/indexer/decoder.ts

printf '\n--- contract files mentioning session_started / session_started / session_started ---\n'
rg -n --type=rust -C3 'started|session_started|started_session|publish|Event\(|Topic|topic' contracts | head -240

printf '\n--- references to event routing in src ---\n'
rg -n -C3 'fund_session|start_session|session_started|started_session|settle_session|refund_session' src

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 259


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- indexer poller relevant slice ---"
sed -n '18,190p' src/indexer/poller.ts

echo
echo "--- EventDecoder relevant slice ---"
sed -n '1,160p' src/indexer/decoder.ts

echo
echo "--- contract files mentioning session_started / session_started / session_started ---"
rg -n --type=rust -C3 'started|session_started|started_session|publish|Event\(|Topic|topic' contracts | head -240

echo
echo "--- references to event routing in src ---"
rg -n -C3 'fund_session|start_session|session_started|started_session|settle_session|refund_session' src

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 11841


Confirm the indexer topic filter matches the emitted event shape.

The poller decodes only topic[0], while these contract events are emitted with ("session", "started"). Align the poller/filter key to the contract’s actual topic shape before relying on the event routing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@contracts/test_snapshots/test/test_start_session_locks_tokens_and_creates_session.1.json`
around lines 1640 - 1690, The poller’s topic filter currently assumes the event
key is in topic[0], but the session-start event emitted by the contract uses the
two-topic shape (“session”, “started”). Update the poller’s event decoding and
filter configuration to match the emitted topic positions, ensuring
session-start events are routed correctly.

Comment thread docs/INDEXER_SETUP.md
Comment on lines +23 to +24
| `ESCROW_CONTRACT_ID` | Soroban Escrow Contract Address | `CC3W26Q6Q...` |
| `DISPUTE_CONTRACT_ID` | Soroban Dispute Contract Address | `CD3W26Q6Q...` |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Truncated contract-ID defaults are not usable as documentation.

CC3W26Q6Q... / CD3W26Q6Q... can't be copied or verified. Either print the full default address or state that these are required with no default.

📝 Proposed doc fix
-| `ESCROW_CONTRACT_ID` | Soroban Escrow Contract Address | `CC3W26Q6Q...` |
-| `DISPUTE_CONTRACT_ID` | Soroban Dispute Contract Address | `CD3W26Q6Q...` |
+| `ESCROW_CONTRACT_ID` | Soroban Escrow contract address (56-char `C...` strkey) | _required_ |
+| `DISPUTE_CONTRACT_ID` | Soroban Dispute contract address (56-char `C...` strkey) | _required_ |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| `ESCROW_CONTRACT_ID` | Soroban Escrow Contract Address | `CC3W26Q6Q...` |
| `DISPUTE_CONTRACT_ID` | Soroban Dispute Contract Address | `CD3W26Q6Q...` |
| `ESCROW_CONTRACT_ID` | Soroban Escrow contract address (56-char `C...` strkey) | _required_ |
| `DISPUTE_CONTRACT_ID` | Soroban Dispute contract address (56-char `C...` strkey) | _required_ |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/INDEXER_SETUP.md` around lines 23 - 24, Update the ESCROW_CONTRACT_ID
and DISPUTE_CONTRACT_ID entries in the environment-variable table to replace the
truncated example values with either their complete default contract addresses
or an explicit indication that no default exists and the variables are required.

Comment thread package.json
Comment on lines +10 to +11
"indexer": "tsx src/indexer/index.ts",
"test:indexer": "tsx tests/indexer.spec.ts"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
grep -n '"tsx"' package.json
grep -n '"ts-node"' package.json
sed -n '1,60p' package.json

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 1840


🏁 Script executed:

#!/bin/bash
set -u
sed -n '60,85p' package.json
node - <<'JS'
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));

function find(name) {
  for (const bucket of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
    if (pkg[bucket]?.[name] != null) return { bucket, version: pkg[bucket][name] };
  }
  return null;
}

console.log({
  scripts: {
    indexer: pkg.scripts?.indexer,
    testIndexer: pkg.scripts?.['test:indexer'],
  },
  tsxEntry: find('tsx'),
  tsNodeEntry: find('ts-node'),
  binScriptShallLookFor: 'node_modules/.bin/tsx'
});
JS

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 409


Declare tsx as a dev dependency.

npm run indexer and npm run test:indexer call tsx, but package.json only declares ts-node. Add tsx to devDependencies so the scripts run from node_modules/.bin.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 10 - 11, Add tsx to the package.json
devDependencies alongside the existing TypeScript tooling, keeping the indexer
and test:indexer scripts unchanged so their tsx commands resolve from
node_modules/.bin.

Comment thread package.json
@@ -18,6 +20,7 @@
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@stellar/freighter-api": "^6.0.1",
"@stellar/stellar-sdk": "^16.1.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
curl -s https://registry.npmjs.org/@stellar/stellar-sdk | jq '.["dist-tags"], (.versions | keys | .[-10:])'

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 419


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package.json relevant fields =="
jq '{scripts, engines, dependencies: .dependencies["`@stellar/stellar-sdk`"], devDependencies: (.devDependencies // {})["type"]}' package.json 2>/dev/null || cat package.json

echo
echo "== README/package metadata for Node version =="
if [ -f README.md ]; then
  rg -n "Node|node|Runtime|requirement|environment" README.md || true
fi
git ls-files | rg -i '(^|/)(\.github/workflows/.*\.ya?ml|README\.md|package\.json|Dockerfile|.*setup.*)$' | sort | sed -n '1,120p'

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 670


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Node runtime declarations =="
rg -n "node-version|setup-node|engines|Next.js|Next\.js|Node\.js|node" .github package.json README.md docs 2>/dev/null || true

echo
echo "== workflow files =="
for f in .github/workflows/*.yml .github/workflows/*.yaml; do
  [ -f "$f" ] || continue
  echo "--- $f ---"
  sed -n '1,220p' "$f"
done

echo
echo "== lockfile files =="
git ls-files | rg '(^|/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$' || true

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 1446


Align the CI Node runtime with the Stellar SDK requirement.

@stellar/stellar-sdk@16.1.0 is published, but .github/workflows/frontend.yml installs dependencies on Node 20 while Stellar SDK 16.x requires Node >=22. Upgrade node-version to 22 so npm install matches the project’s dependencies.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 23, Update the node-version setting in the frontend CI
workflow to 22 so dependency installation runs on the Node version required by
`@stellar/stellar-sdk` 16.x. Leave the existing dependency configuration
unchanged.

Comment thread src/indexer/db/client.ts
Comment on lines +69 to +126
async init(): Promise<void> {
if (!this.connectionString) return;
if (!PoolClass) throw new Error('pg module is not installed');

this.pool = new PoolClass({
connectionString: this.connectionString,
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
});

const client = await this.pool.connect();
try {
await client.query(`
CREATE TABLE IF NOT EXISTS ledger_checkpoints (
id VARCHAR(64) PRIMARY KEY,
last_ledger_sequence BIGINT NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS processed_events (
event_id VARCHAR(128) PRIMARY KEY,
contract_id VARCHAR(128) NOT NULL,
topic VARCHAR(64) NOT NULL,
ledger_sequence BIGINT NOT NULL,
transaction_hash VARCHAR(128),
payload JSONB NOT NULL,
processed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS sessions (
id VARCHAR(128) PRIMARY KEY,
title VARCHAR(256),
expert_id VARCHAR(128),
seeker_id VARCHAR(128),
status VARCHAR(64) NOT NULL,
price VARCHAR(64),
transaction_hash VARCHAR(128),
network VARCHAR(32) DEFAULT 'testnet',
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS transactions (
id VARCHAR(128) PRIMARY KEY,
hash VARCHAR(128) NOT NULL,
type VARCHAR(32) NOT NULL,
amount VARCHAR(64) NOT NULL,
date VARCHAR(32) NOT NULL,
status VARCHAR(32) NOT NULL,
network VARCHAR(32) DEFAULT 'testnet',
session_id VARCHAR(128),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
`);
} finally {
client.release();
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

init()'s inline DDL has already drifted from db/schema.sql.

schema.sql declares indexes on processed_events(contract_id), processed_events(ledger_sequence), and transactions(session_id), but the actual migration path used at runtime (PostgresDBClient.init()) never creates them and never loads schema.sql. This means schema.sql is effectively dead documentation, and the live tables lack the indexes needed for efficient lookups as processed_events/transactions grow.

Consolidate to a single source of truth — either load and execute schema.sql from init(), or add the missing CREATE INDEX IF NOT EXISTS statements to the inline DDL here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/indexer/db/client.ts` around lines 69 - 126, Update
PostgresDBClient.init() so its runtime schema matches schema.sql by
consolidating initialization around one source of truth. Either load and execute
schema.sql from init(), or extend the inline DDL to create indexes on
processed_events(contract_id), processed_events(ledger_sequence), and
transactions(session_id), using CREATE INDEX IF NOT EXISTS.

Comment thread src/indexer/decoder.ts
Comment on lines +87 to +90
private static normalizeNativeValue(native: any): Record<string, any> {
if (native === null || native === undefined) return {};
if (typeof native === 'bigint') return { amount: native.toString() };
if (typeof native !== 'object') return { value: native };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Bare bigint values are unconditionally labeled amount.

normalizeNativeValue wraps any top-level scalar bigint native value as { amount: ... } (line 89), regardless of what the value actually represents. poller.ts's session-id extraction (decoded.payload.session_id || decoded.payload.sessionId || decoded.payload.id || 'session_1') depends on the correct key being present; if a contract emits a bare integer that is really a session id (not an amount), this mislabeling causes it to fall through to the 'session_1' fallback, silently merging unrelated sessions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/indexer/decoder.ts` around lines 87 - 90, Update
Decoder.normalizeNativeValue so bare bigint values are not unconditionally
returned under the amount key; preserve or derive the correct semantic key
needed by session-id extraction, including session_id/sessionId/id when the
native value represents a session identifier. Keep amount labeling only for
values known to represent amounts, and preserve existing handling for null,
undefined, and other scalar values.

Comment thread src/indexer/decoder.ts
Comment on lines +109 to +114
static decodeEvent(rawEvent: any): DecodedEvent {
const eventId = rawEvent?.id || rawEvent?.eventId || `evt_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
const contractId = rawEvent?.contractId || rawEvent?.contract_id || '';
const ledgerSequence = parseInt(String(rawEvent?.ledger || rawEvent?.ledgerSequence || 0), 10);
const transactionHash = rawEvent?.txHash || rawEvent?.transactionHash || rawEvent?.hash || '';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Fallback event ID generation undermines the idempotency guarantee.

When rawEvent.id/rawEvent.eventId is absent, a fresh random ID (Date.now() + Math.random()) is generated every call. If the same underlying event is decoded twice (restart re-polling an overlapping ledger range, retried RPC response, etc.) without a native id, poller.ts's isEventProcessed check will never recognize it as a duplicate, since it relies entirely on decoded.eventId staying stable across decodes of the same event.

🛡️ Suggested deterministic fallback
-    const eventId = rawEvent?.id || rawEvent?.eventId || `evt_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
+    const eventId = rawEvent?.id || rawEvent?.eventId ||
+      `evt_${rawEvent?.ledger || rawEvent?.ledgerSequence || 0}_${rawEvent?.contractId || rawEvent?.contract_id || 'unknown'}_${rawEvent?.txHash || rawEvent?.transactionHash || rawEvent?.hash || 'notx'}`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static decodeEvent(rawEvent: any): DecodedEvent {
const eventId = rawEvent?.id || rawEvent?.eventId || `evt_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
const contractId = rawEvent?.contractId || rawEvent?.contract_id || '';
const ledgerSequence = parseInt(String(rawEvent?.ledger || rawEvent?.ledgerSequence || 0), 10);
const transactionHash = rawEvent?.txHash || rawEvent?.transactionHash || rawEvent?.hash || '';
static decodeEvent(rawEvent: any): DecodedEvent {
const eventId = rawEvent?.id || rawEvent?.eventId ||
`evt_${rawEvent?.ledger || rawEvent?.ledgerSequence || 0}_${rawEvent?.contractId || rawEvent?.contract_id || 'unknown'}_${rawEvent?.txHash || rawEvent?.transactionHash || rawEvent?.hash || 'notx'}`;
const contractId = rawEvent?.contractId || rawEvent?.contract_id || '';
const ledgerSequence = parseInt(String(rawEvent?.ledger || rawEvent?.ledgerSequence || 0), 10);
const transactionHash = rawEvent?.txHash || rawEvent?.transactionHash || rawEvent?.hash || '';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/indexer/decoder.ts` around lines 109 - 114, Update the fallback event ID
logic in Decoder.decodeEvent so events without rawEvent.id or rawEvent.eventId
receive a deterministic identifier derived from stable event fields, rather than
Date.now() and Math.random(). Ensure repeated decoding of the same underlying
event produces the same eventId while retaining native IDs when present.

Comment thread src/indexer/poller.ts
Comment on lines +134 to +146
// Update last ledger checkpoint if this event has a higher sequence
if (decoded.ledgerSequence > this.lastProcessedLedger) {
this.lastProcessedLedger = decoded.ledgerSequence;
}

// Idempotency check: skip if event ID already processed
const alreadyProcessed = await this.dbClient.isEventProcessed(decoded.eventId);
if (alreadyProcessed) {
console.log(`[SorobanIndexer] Skipping duplicate event ${decoded.eventId}`);
// Still update ledger checkpoint
await this.dbClient.saveLedger('soroban_indexer', this.lastProcessedLedger);
return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Checkpoint advances before persistence succeeds — failed events are permanently skipped.

this.lastProcessedLedger (line 135-137) is mutated in-memory as soon as an event is decoded, well before recordEventAndStateUpdate (line 240) actually commits it. If that DB call throws (transaction rolls back per db/client.ts lines 238-243), the exception propagates out of the unguarded loop at lines 100-104, aborting the rest of the batch — but the in-memory lastProcessedLedger has already advanced past the failed event.

Since getLastLedger is only queried once in start() (line 34), the next poll tick uses the now-desynced in-memory value as startLedger, permanently skipping the failed event (and any co-ledger events after it in that tick) — the exact opposite of the "restart recovery"/"no duplicate processing" guarantees this indexer is meant to provide.

🐛 Suggested fix sketch
-    // Update last ledger checkpoint if this event has a higher sequence
-    if (decoded.ledgerSequence > this.lastProcessedLedger) {
-      this.lastProcessedLedger = decoded.ledgerSequence;
-    }
-
     // Idempotency check: skip if event ID already processed
     const alreadyProcessed = await this.dbClient.isEventProcessed(decoded.eventId);
     if (alreadyProcessed) {
       console.log(`[SorobanIndexer] Skipping duplicate event ${decoded.eventId}`);
-      // Still update ledger checkpoint
-      await this.dbClient.saveLedger('soroban_indexer', this.lastProcessedLedger);
       return false;
     }
+    ...
+    // Only advance the checkpoint after the DB write for this event succeeds
+    const nextLedger = Math.max(decoded.ledgerSequence, this.lastProcessedLedger);
+    await this.dbClient.recordEventAndStateUpdate(eventRecord, sessionUpdate, txRecord, nextLedger);
+    this.lastProcessedLedger = nextLedger;

Also wrap each processSingleEvent call in the batch loop with its own try/catch so one failing event doesn't block the rest of the batch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/indexer/poller.ts` around lines 134 - 146, Move the lastProcessedLedger
update out of the pre-persistence section and apply it only after
recordEventAndStateUpdate completes successfully, while preserving the
duplicate-event checkpoint handling. Wrap each processSingleEvent invocation in
the batch loop with an independent try/catch so a failed event does not advance
the in-memory checkpoint or prevent later events from processing.

Comment thread src/indexer/poller.ts
Comment on lines +158 to +236
let sessionUpdate: SessionUpdate | undefined;
let txRecord: TransactionRecord | undefined;

const sessionId = decoded.payload.session_id || decoded.payload.sessionId || decoded.payload.id || 'session_1';
const amount = decoded.payload.amount || decoded.payload.price || decoded.payload.payout_amount || decoded.payload.refund_amount || '50 XLM';

if (isTargetEvent) {
if (topic.includes('fund_session') || topic.includes('start_session')) {
sessionUpdate = {
sessionId,
status: 'active',
transactionHash: decoded.transactionHash,
price: String(amount),
};
txRecord = {
id: `tx_dep_${decoded.eventId}`,
hash: decoded.transactionHash || `hash_${decoded.eventId}`,
type: 'deposit',
amount: String(amount),
date: new Date().toISOString().split('T')[0],
status: 'completed',
network: 'testnet',
sessionId,
};
} else if (topic.includes('pause_session')) {
sessionUpdate = {
sessionId,
status: 'paused',
transactionHash: decoded.transactionHash,
};
} else if (topic.includes('refund_session') || topic.includes('cancel_session')) {
sessionUpdate = {
sessionId,
status: 'cancelled',
transactionHash: decoded.transactionHash,
};
txRecord = {
id: `tx_ref_${decoded.eventId}`,
hash: decoded.transactionHash || `hash_${decoded.eventId}`,
type: 'refund',
amount: String(amount),
date: new Date().toISOString().split('T')[0],
status: 'completed',
network: 'testnet',
sessionId,
};
} else if (topic.includes('complete_session') || topic.includes('settle_session')) {
sessionUpdate = {
sessionId,
status: 'completed',
transactionHash: decoded.transactionHash,
};
txRecord = {
id: `tx_stl_${decoded.eventId}`,
hash: decoded.transactionHash || `hash_${decoded.eventId}`,
type: 'settlement',
amount: String(amount),
date: new Date().toISOString().split('T')[0],
status: 'completed',
network: 'testnet',
sessionId,
};
} else if (topic.includes('resolve_dispute')) {
sessionUpdate = {
sessionId,
status: 'completed',
transactionHash: decoded.transactionHash,
};
txRecord = {
id: `tx_dsp_${decoded.eventId}`,
hash: decoded.transactionHash || `hash_${decoded.eventId}`,
type: 'settlement',
amount: String(amount),
date: new Date().toISOString().split('T')[0],
status: 'completed',
network: 'testnet',
sessionId,
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

network is hardcoded to 'testnet' regardless of the actual RPC target.

Every txRecord built here sets network: 'testnet' unconditionally, and no sessionUpdate branch sets network at all. If config.rpcUrl is pointed at mainnet (or any non-testnet endpoint), every persisted session/transaction record will still be tagged/defaulted to 'testnet', corrupting the recorded network provenance for real activity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/indexer/poller.ts` around lines 158 - 236, Derive the network from the
configured RPC target instead of hardcoding `'testnet'` in each transaction
record. Update the transaction construction branches in the event-processing
logic to use that derived network, and include the same network on every
`sessionUpdate` object so persisted sessions and transactions preserve the
actual RPC provenance.

Comment thread tests/indexer.spec.ts
Comment on lines +56 to +92
console.log('\n[Test 3] SorobanEventPoller - Simulated Event Trigger DB Update (< 5 seconds)');
{
const config = loadIndexerConfig();
const dbClient = new InMemoryDBClient();
const poller = new SorobanEventPoller(config, dbClient);

await poller.start();

const startTime = Date.now();
poller.pushSimulatedEvent({
id: 'evt_sim_501',
contractId: config.escrowContractId,
topic: ['fund_session'],
ledger: 2001,
txHash: 'hash_sim_501',
value: { session_id: 'session_501', amount: '200 XLM' },
});

const result = await poller.pollOnce();
const durationMs = Date.now() - startTime;

assert.strictEqual(result.eventsProcessed, 1);
assert(durationMs < 5000, `Event processing duration ${durationMs}ms exceeded 5 second threshold`);

const session = await dbClient.getSession('session_501');
assert.ok(session, 'Session record should exist in DB');
assert.strictEqual(session?.status, 'active');
assert.strictEqual(session?.price, '200 XLM');

const tx = await dbClient.getTransaction('tx_dep_evt_sim_501');
assert.ok(tx, 'Deposit transaction should be recorded');
assert.strictEqual(tx?.type, 'deposit');
assert.strictEqual(tx?.status, 'completed');

await poller.stop();
console.log(`✓ Contract event triggered database update in ${durationMs}ms (< 5s).`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -e yml -e yaml . .github | xargs -r grep -l 'test:indexer\|indexer' 2>/dev/null
fd -i '.env' -a

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 248


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package scripts =="
python3 - <<'PY'
import json
from pathlib import Path
p=Path('package.json')
if p.exists():
    data=json.loads(p.read_text())
    print(json.dumps(data.get('scripts',{}), indent=2))
PY

echo
echo "== locator indexer files =="
git ls-files | rg '(^|/)(indexer|poller)\.(ts|js)$|(^|/)tests/indexer\.spec\.ts$|(^|/)src/|(^|/)scripts/'

Repository: LightForgeHub/SkillSphere-Dapp

Length of output: 8416


Avoid calling the live Soroban RPC from tests/indexer.spec.ts.

These tests start the real SorobanEventPoller, which calls rpcServer.getEvents() against config.rpcUrl (SOROBAN_RPC_URL, otherwise the Soroban testnet URL). Test 3 only asserts on the simulated event queue, so the network call adds an unrelated external dependency that can slow or fail test:indexer; inject/stub the RPC client for this suite instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/indexer.spec.ts` around lines 56 - 92, The indexer test suite must not
make live Soroban RPC requests. Update the Test 3 setup around
SorobanEventPoller and its RPC dependency so getEvents is injected or stubbed
locally while preserving simulated-event processing and existing assertions;
apply the same test-local RPC isolation to other poller instances in this suite
as needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants