Feat/soroban event polling indexer - #446
Conversation
|
@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. |
📝 WalkthroughWalkthroughAdds 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. ChangesContract regression snapshots
Soroban event indexer
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (3)
src/indexer/config.ts (1)
11-21: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winConsider failing fast (or warning loudly) on missing contract ID configuration.
escrowContractId/disputeContractIdsilently 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 inindex.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 winTopic list here omits the aliases documented in "Supported Contract Events".
Lines 106-119 claim
start_session,cancel_session, andsettle_sessionare 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 winKeep 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 INDEXstatements forprocessed_eventsandtransactions. Link tosrc/indexer/db/schema.sqlor 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
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (81)
contracts/test_snapshots/test/test_1_second_session.1.jsoncontracts/test_snapshots/test/test_1_year_session_overflow_check.1.jsoncontracts/test_snapshots/test/test_admin_can_update_fee_tiers.1.jsoncontracts/test_snapshots/test/test_auto_resolve_expiry_refunds_seeker_after_30_days.1.jsoncontracts/test_snapshots/test/test_batch_settle_settles_multiple_sessions.1.jsoncontracts/test_snapshots/test/test_batch_settle_skips_nonexistent_sessions.1.jsoncontracts/test_snapshots/test/test_batch_settle_skips_sessions_belonging_to_other_expert.1.jsoncontracts/test_snapshots/test/test_calculate_claimable_amount_same_time_returns_zero.1.jsoncontracts/test_snapshots/test/test_calculate_platform_fee_uses_default_tiers.1.jsoncontracts/test_snapshots/test/test_collect_fee_increases_treasury_balance.1.jsoncontracts/test_snapshots/test/test_collect_fee_rejects_negative_amount.1.jsoncontracts/test_snapshots/test/test_collect_fee_rejects_zero_amount.1.jsoncontracts/test_snapshots/test/test_collect_multiple_fees_accumulates_balance.1.jsoncontracts/test_snapshots/test/test_expert_registration_and_availability.1.jsoncontracts/test_snapshots/test/test_expert_stake_above_tier_3_gets_tier_3_reduction.1.jsoncontracts/test_snapshots/test/test_expert_stake_between_tier_1_and_2_gets_tier_1_reduction.1.jsoncontracts/test_snapshots/test/test_expert_stake_between_tier_2_and_3_gets_tier_2_reduction.1.jsoncontracts/test_snapshots/test/test_expert_stake_just_below_tier_1_pays_full_fee.1.jsoncontracts/test_snapshots/test/test_expert_with_no_stake_pays_full_fee.1.jsoncontracts/test_snapshots/test/test_expert_with_tier_1_stake_gets_100_bps_reduction.1.jsoncontracts/test_snapshots/test/test_expert_with_tier_2_stake_gets_200_bps_reduction.1.jsoncontracts/test_snapshots/test/test_expert_with_tier_3_stake_gets_300_bps_reduction.1.jsoncontracts/test_snapshots/test/test_expiry_timestamp_uses_remaining_balance_and_rate.1.jsoncontracts/test_snapshots/test/test_fee_reduction_respects_base_fee_changes.1.jsoncontracts/test_snapshots/test/test_flag_dispute_rejects_invalid_cid.1.jsoncontracts/test_snapshots/test/test_flag_dispute_stores_evidence_cid.1.jsoncontracts/test_snapshots/test/test_get_current_earnings_caps_at_session_balance.1.jsoncontracts/test_snapshots/test/test_get_current_earnings_reflects_elapsed_time.1.jsoncontracts/test_snapshots/test/test_get_current_earnings_returns_zero_at_start.1.jsoncontracts/test_snapshots/test/test_get_current_earnings_zero_when_paused.1.jsoncontracts/test_snapshots/test/test_get_expert_staked_balance_returns_zero_for_new_expert.1.jsoncontracts/test_snapshots/test/test_get_staking_contract_returns_none_when_not_set.1.jsoncontracts/test_snapshots/test/test_get_treasury_address_returns_none_when_not_set.1.jsoncontracts/test_snapshots/test/test_get_treasury_balance_returns_zero_initially.1.jsoncontracts/test_snapshots/test/test_linear_streaming_caps_at_remaining_balance.1.jsoncontracts/test_snapshots/test/test_min_session_deposit_defaults_and_can_be_updated_by_admin.1.jsoncontracts/test_snapshots/test/test_multiple_settlements_track_milestones_without_ending_session.1.jsoncontracts/test_snapshots/test/test_only_participants_can_pause_or_resume.1.jsoncontracts/test_snapshots/test/test_partial_withdrawals_maintain_correct_balance.1.jsoncontracts/test_snapshots/test/test_pause_and_resume_preserve_accrued_amount.1.jsoncontracts/test_snapshots/test/test_protocol_pause_blocks_new_sessions.1.jsoncontracts/test_snapshots/test/test_protocol_pause_blocks_settlement_but_allows_refund_session.1.jsoncontracts/test_snapshots/test/test_resolve_dispute_splits_funds_by_percentage.1.jsoncontracts/test_snapshots/test/test_set_admin_and_fee_round_trip.1.jsoncontracts/test_snapshots/test/test_set_and_get_expert_referrer.1.jsoncontracts/test_snapshots/test/test_set_and_get_expert_staked_balance.1.jsoncontracts/test_snapshots/test/test_set_and_get_treasury_address.1.jsoncontracts/test_snapshots/test/test_set_expert_staked_balance_rejects_negative_amount.1.jsoncontracts/test_snapshots/test/test_set_staking_contract_address.1.jsoncontracts/test_snapshots/test/test_settle_session_after_funded_window_drains_and_finishes.1.jsoncontracts/test_snapshots/test/test_settle_session_pays_referrer_from_platform_fee.1.jsoncontracts/test_snapshots/test/test_settle_session_transfers_partial_milestone_payment.1.jsoncontracts/test_snapshots/test/test_start_session_accepts_cid_v1.1.jsoncontracts/test_snapshots/test/test_start_session_allows_expert_when_reputation_is_met.1.jsoncontracts/test_snapshots/test/test_start_session_fails_if_expert_not_registered.1.jsoncontracts/test_snapshots/test/test_start_session_fails_if_expert_unavailable.1.jsoncontracts/test_snapshots/test/test_start_session_fails_on_insufficient_balance.1.jsoncontracts/test_snapshots/test/test_start_session_fails_when_amount_is_below_minimum_deposit.1.jsoncontracts/test_snapshots/test/test_start_session_locks_tokens_and_creates_session.1.jsoncontracts/test_snapshots/test/test_start_session_rejects_invalid_metadata_cid.1.jsoncontracts/test_snapshots/test/test_start_session_rejects_low_reputation_expert.1.jsoncontracts/test_snapshots/test/test_start_session_stores_metadata_cid.1.jsoncontracts/test_snapshots/test/test_treasury_balance_survives_multiple_collect_and_withdraw_cycles.1.jsoncontracts/test_snapshots/test/test_treasury_tracks_multiple_tokens_separately.1.jsoncontracts/test_snapshots/test/test_update_session_notes.1.jsoncontracts/test_snapshots/test/test_withdraw_all_treasury_empties_balance.1.jsoncontracts/test_snapshots/test/test_withdraw_all_treasury_returns_zero_when_empty.1.jsoncontracts/test_snapshots/test/test_withdraw_treasury_fails_with_insufficient_balance.1.jsoncontracts/test_snapshots/test/test_withdraw_treasury_rejects_negative_amount.1.jsoncontracts/test_snapshots/test/test_withdraw_treasury_rejects_zero_amount.1.jsoncontracts/test_snapshots/test/test_withdraw_treasury_transfers_funds_and_updates_balance.1.jsondocs/INDEXER_SETUP.mdpackage.jsonpages/test.tsxsrc/indexer/config.tssrc/indexer/db/client.tssrc/indexer/db/schema.sqlsrc/indexer/decoder.tssrc/indexer/index.tssrc/indexer/poller.tstests/indexer.spec.ts
| { | ||
| "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 | ||
| }, |
There was a problem hiding this comment.
🎯 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/indexerRepository: 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' srcRepository: 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' srcRepository: 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.
| | `ESCROW_CONTRACT_ID` | Soroban Escrow Contract Address | `CC3W26Q6Q...` | | ||
| | `DISPUTE_CONTRACT_ID` | Soroban Dispute Contract Address | `CD3W26Q6Q...` | |
There was a problem hiding this comment.
📐 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.
| | `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.
| "indexer": "tsx src/indexer/index.ts", | ||
| "test:indexer": "tsx tests/indexer.spec.ts" |
There was a problem hiding this comment.
🎯 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.jsonRepository: 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'
});
JSRepository: 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.
| @@ -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", | |||
There was a problem hiding this comment.
🎯 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)$' || trueRepository: 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.
| 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(); | ||
| } | ||
| } |
There was a problem hiding this comment.
🗄️ 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.
| 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 }; |
There was a problem hiding this comment.
🗄️ 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.
| 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 || ''; | ||
|
|
There was a problem hiding this comment.
🗄️ 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.
| 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.
| // 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; | ||
| } |
There was a problem hiding this comment.
🗄️ 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.
| 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, | ||
| }; | ||
| } |
There was a problem hiding this comment.
🗄️ 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.
| 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).`); | ||
| } |
There was a problem hiding this comment.
🩺 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' -aRepository: 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.
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-sdkand PostgreSQL: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.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.src/indexer/db/client.ts): Records processed event IDs inprocessed_eventsto suppress duplicate events and updatessessionsandtransactionstables within atomic SQL transactions.last_ledger_sequenceinledger_checkpointsso 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,tsxdependencies and addedindexer(npm run indexer) andtest: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 forledger_checkpoints,processed_events,sessions, andtransactions.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
PostgresDBClientfor production database environments andInMemoryDBClientfor isolated testing without requiring an external PostgreSQL instance.EventDecoderto 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.POLL_INTERVAL_MS), ensuring database updates complete within 5 seconds of event emission.5. Tests Added & Validation Performed
fund_session,pause_session,refund_session,complete_session,resolve_dispute.npm run buildcompletes cleanly.Closes #
Summary by CodeRabbit
New Features
Tests