Skip to content

feat: implement core business logic for TTL extension, restore, discovery, and CLI commands - #1

Merged
AbdulmalikAlayande merged 17 commits into
mainfrom
dev
Jun 13, 2026
Merged

AbdulmalikAlayande merged 17 commits into
mainfrom
dev

Conversation

@AbdulmalikAlayande

@AbdulmalikAlayande AbdulmalikAlayande commented Jun 13, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This PR implements all remaining core business logic for Soroban Sentinel, transforming it from a monitoring-only tool into a fully operational TTL management layer for deployed Soroban smart contracts.

What changed

  • RPC Client: Extended StellarRpcClient with transaction building, simulation, signing, and submission capabilities for ExtendFootprintTTLOp and RestoreFootprintOp Stellar operations
  • Core Extension Module: Full TTL extension lifecycle — simulate (dry-run), extend, auto-extend (policy-driven), and restore archived entries
  • Core Discovery Module: Footprint-based storage key discovery via getEvents RPC scanning
  • CLI Commands: Three production-ready commands (guard, costs, restore) replacing placeholder stubs
  • Daemon Integration: Auto-extension runs as Step 3 of every daemon monitoring cycle
  • Config Utility: Persistent YAML configuration for CLI settings

Detailed Changes

1. RPC Client — Transaction Support (src/rpc/client.ts, +262 lines)

New capabilities added to StellarRpcClient:

Method Purpose
simulateExtension() Simulates ExtendFootprintTTLOp to estimate fees without submitting
submitExtension() Builds, simulates, signs, and submits an extension transaction
submitRestore() Builds, simulates, signs, and submits a RestoreFootprintOp transaction
getNetworkPassphrase() Resolves "testnet" / "mainnet" to Stellar network passphrases
pollTransaction() Polls getTransaction until SUCCESS/FAILED/timeout (30 attempts, 1s interval)

Transaction flow: Build raw tx -> simulate via RPC -> assemble with simulation results -> sign with keypair -> submit -> poll for terminal state.

Key design decisions:

  • Uses rpc.assembleTransaction() to prepare transactions with correct resource parameters from simulation
  • Uses SorobanDataBuilder to set read-only footprints (extension) or read-write footprints (restore)
  • Polling has configurable max attempts and interval with clear timeout error messaging

2. Core Extension Module (src/core/extension.ts, +392 lines)

Four exported functions:

Function Description
simulateExtension() Dry-run fee estimation. Returns estimated fee in stroops without submitting.
extendEntries() Full extension: submit tx, fetch fresh TTLs, record in extension_history, update entries in DB.
runAutoExtensions() Daemon auto-extension: iterates all contracts with enabled policies, extends entries below threshold. Errors per contract are collected (not thrown).
restoreEntries() Restore archived entries: submit RestoreFootprintOp, refresh TTLs, update DB entries.

Private helper - resolveSecretKey(source) resolves keypair sources:

  • "env:VAR_NAME" - reads from environment variable (recommended for production)
  • Direct "S..." secret key (56 chars) - used as-is (for development/testing)

Auto-extension logic (runAutoExtensions):

  1. Filters contracts by network
  2. Checks each contract's extension policy (enabled + thresholds)
  3. Identifies entries where remaining TTL < extend_when_below_ledgers
  4. Resolves secret key from policy's keypair_source
  5. Calls extendEntries() for batch extension
  6. Collects per-contract errors without failing the entire batch

3. Core Discovery Module (src/core/discovery.ts, +258 lines)

Footprint-based storage key discovery using Stellar RPC events:

Function Description
discoverStorageKeys() Scans recent events for a single contract, extracts storage keys from event topics
runBatchDiscovery() Runs discovery across all tracked contracts for a given network

Discovery strategy:

  • Looks back ~1 hour of ledgers (655 ledgers at 5.5s/ledger) via getEvents RPC
  • Extracts potential storage keys from contract event topics
  • Builds LedgerKey.contractData entries from discovered keys
  • Upserts new entries into DB with discovery_source: "event_scan"
  • Deduplicates against existing known entries

4. CLI Commands

guard <contractId> (src/commands/guard.ts, +194 lines)

Configures auto-extension policies for contracts.

Option Description
--target-ttl <ledgers> Target TTL to extend entries to
--threshold <ledgers> Extend when remaining TTL drops below this
--keypair <secret> Direct secret key for signing (dev use)
--keypair-env <varName> Environment variable containing secret key (production)
--auto-extend Enable automatic extension via daemon
--dry-run Simulate extension and show estimated fees
--disable Disable auto-extension policy

Security: Never stores raw secret keys in the database. When --keypair is provided, it stores the derived public key reference. When --keypair-env is used, it stores "env:VAR_NAME".

costs <contractId> (src/commands/costs.ts, +103 lines)

Displays extension history with cost analytics.

Option Description
--period <days> Filter history to last N days (default: 30)
--all Show all history without time filter

Output includes:

  • Recent 10 extensions with old->new TTL and cost per extension
  • Total estimated cost aggregate
  • Per-entry-type cost breakdown
  • TTL projection based on historical extension patterns

restore <contractId> (src/commands/restore.ts, +97 lines)

Restores archived (expired) ledger entries.

Option Description
--keypair <secret> Secret key for signing the restore transaction
--keypair-env <varName> Environment variable containing secret key
--entry <keyXdr> Specific entry key XDR to restore (repeatable)
--all Restore all known entries for the contract

5. CLI Entry Point (src/index.ts, +7/-17 lines)

Replaced 3 placeholder command stubs with real implementations:

  • registerGuardCommand(program)
  • registerCostsCommand(program)
  • registerRestoreCommand(program)

6. Daemon Integration (src/daemon/loop.ts, +16 lines)

Added Step 3 to executeCycle():

Monitor cycle -> Alert delivery -> Auto-extensions
  • Runs runAutoExtensions() after alert delivery
  • Errors are caught and logged without killing the daemon
  • Logs summary when contracts were checked (checked, extended, entries, errors)

7. Config Utility (src/utils/config.ts, +84 lines)

YAML-based persistent configuration:

interface SentinelConfig {
    network: string;                // default: "testnet"
    rpcUrl?: string;                // optional custom RPC endpoint
    pollingIntervalSeconds: number; // default: 300
    slackToken?: string;            // optional Slack integration
}
  • loadConfig(path?) - Loads from ~/.soroban-sentinel/config.yaml, returns defaults if missing/invalid
  • saveConfig(config, path?) - Writes config to YAML, creates directories if needed

Tests Added

tests/core/extension.test.ts (+522 lines)

Suite Tests Coverage
extendEntries 4 tests Success with DB updates, contract not found, empty entries, tx failure
simulateExtension 2 tests Success with fee estimation, simulation failure
restoreEntries 3 tests Success with DB updates, contract not found, tx failure
runAutoExtensions 5 tests Full auto-extend flow, skips disabled policies, skips contracts above threshold, handles keypair resolution failure, isolates errors across contracts

All mocks use proper class-based constructors for StellarRpcClient to ensure Vitest compatibility.

tests/utils/config.test.ts (+85 lines)

Suite Tests Coverage
loadConfig 4 tests Defaults when missing, full YAML load, partial YAML with defaults, invalid YAML fallback
saveConfig 2 tests Write + read roundtrip, nested directory creation

Test Results

Test Files  13 passed (13)
Tests       225 passed | 5 skipped (230)
Duration    7.05s

TypeScript compilation: zero errors (npx tsc --noEmit clean).


Architecture Decisions

  1. Static imports over dynamic: runAutoExtensions uses static imports for all DB repositories, ensuring clean mocking in tests and avoiding module resolution issues at runtime.

  2. Error isolation in daemon: Auto-extension errors are caught per-contract and collected into an errors[] array. A failure in one contract never blocks extension of another. The daemon cycle itself wraps auto-extensions in a try/catch so even unexpected errors cannot kill the loop.

  3. Keypair source abstraction: The resolveSecretKey() helper supports env:VAR_NAME format (recommended) and direct keys. This keeps secret key management flexible while the DB only stores references, never raw keys.

  4. Post-transaction TTL refresh: After every successful extension or restore, fresh TTLs are fetched from the network and written back to the DB. This ensures the local state accurately reflects on-chain reality.

  5. Transaction polling pattern: After sendTransaction, the client polls getTransaction up to 30 times at 1-second intervals. This handles Stellar's async transaction processing without blocking indefinitely.


Commit History (9 commits)

# Commit Scope
1 feat(utils): implement YAML config loader and saver Config utility + tests
2 feat(rpc): add TTL extension and restore transaction support RPC client methods
3 feat(core): implement TTL extension, auto-extension, and restore logic Core extension + tests
4 feat(core): implement footprint-based storage key discovery Discovery module
5 feat(cli): implement guard command for auto-extension policies Guard command
6 feat(cli): implement costs command for extension history reporting Costs command
7 feat(cli): implement restore command for archived entry recovery Restore command
8 feat(cli): wire up guard, costs, and restore commands CLI wiring
9 feat(daemon): integrate auto-extension into monitoring cycle Daemon integration

How to Review

  1. Start with src/rpc/client.ts — the foundation (transaction building and submission)
  2. Then src/core/extension.ts — business logic built on top of RPC client
  3. Then src/core/discovery.ts — independent module for storage key scanning
  4. Then the 3 CLI commands (guard.ts, costs.ts, restore.ts) — user-facing wrappers
  5. Then src/index.ts and src/daemon/loop.ts — integration points
  6. Finally, src/utils/config.ts — standalone utility

Test plan

  • All 230 tests pass (225 passed, 5 skipped)
  • TypeScript compiles with zero errors
  • Extension tests cover success, failure, and edge cases
  • Config tests cover load/save roundtrips and error handling
  • Manual testing with Stellar testnet (future — requires funded account)
  • Discovery module integration test with real contract events (future)

Generated with Claude Code

AbdulmalikAlayande and others added 9 commits June 13, 2026 05:19
Add loadConfig/saveConfig functions for persistent CLI configuration.
Supports custom config paths, defaults for missing fields, and
graceful handling of invalid YAML. Includes full test coverage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add simulateExtension, submitExtension, and submitRestore methods to
StellarRpcClient. Includes SimulateExtensionResult and SubmitTransactionResult
interfaces, network passphrase resolution, and transaction polling.

Supports ExtendFootprintTTLOp (with simulation for fee estimation and
full submit flow) and RestoreFootprintOp for archived entry recovery.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add core extension module with:
- simulateExtension: dry-run fee estimation for TTL extensions
- extendEntries: full extension with DB history recording
- runAutoExtensions: daemon-driven auto-extension for policy-enabled contracts
- restoreEntries: restore archived entries with fresh TTL refresh
- resolveSecretKey: supports env:VAR_NAME and direct secret key formats

Includes comprehensive test coverage for all functions covering success
paths, error handling, DB updates, and fault isolation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add discovery module that scans recent contract events to discover
storage keys. Uses getEvents RPC to find contract interactions and
extracts potential storage keys from event topics.

Includes discoverStorageKeys for single contracts and runBatchDiscovery
for network-wide scanning across all tracked contracts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add full CLI command for configuring and managing auto-extension
policies. Supports setting target TTL, threshold, keypair source
(env var or direct), dry-run simulation, manual one-time extension,
and policy disabling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add CLI command to display extension history with cost aggregates,
per-entry-type breakdowns, and TTL projections. Supports filtering
by time period with --period flag.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add CLI command to restore archived (expired) ledger entries.
Supports restoring specific entries via --entry flags or all
entries for a contract via --all. Accepts keypair via --keypair
or --keypair-env for transaction signing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace placeholder command stubs with real implementations.
Register guard, costs, and restore commands via their dedicated
modules.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Step 3 to the daemon cycle: after monitoring and alert delivery,
run auto-extensions for all contracts with enabled policies. Errors
are isolated and logged without killing the daemon.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 13, 2026 04:29
@coderabbitai

coderabbitai Bot commented Jun 13, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2bb2f23d-7346-4c46-a115-0ff8ae591948

📥 Commits

Reviewing files that changed from the base of the PR and between f2edb20 and 7ad576c.

📒 Files selected for processing (8)
  • src/commands/costs.ts
  • src/commands/guard.ts
  • src/commands/restore.ts
  • src/core/discovery.ts
  • src/core/extension.ts
  • src/rpc/client.ts
  • src/utils/config.ts
  • tests/core/extension.test.ts
📜 Recent review details
🔇 Additional comments (8)
src/rpc/client.ts (1)

196-241: LGTM!

Also applies to: 247-313, 318-375, 379-443

src/utils/config.ts (1)

1-87: LGTM!

src/core/extension.ts (1)

1-14: LGTM!

Also applies to: 68-106, 108-200, 202-302, 304-379, 381-409

src/core/discovery.ts (1)

1-7: LGTM!

Also applies to: 32-38, 51-188, 190-226, 228-268

tests/core/extension.test.ts (1)

1-94: LGTM!

Also applies to: 95-194, 196-241, 243-316, 318-522

src/commands/costs.ts (1)

1-107: LGTM!

src/commands/guard.ts (1)

1-193: LGTM!

src/commands/restore.ts (1)

1-103: LGTM!


📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Added costs tracking command with 30-day projection estimates for contract extensions
    • Implemented auto-extension policy management with configurable TTL thresholds
    • Added entry restoration capability for archived contract data
    • Integrated automatic extension execution into daemon monitoring loop
    • Added persistent configuration management via config files
    • Enabled on-chain storage key discovery for contracts
  • Tests

    • Added comprehensive test coverage for extension and configuration workflows

Walkthrough

Adds Soroban RPC helpers for extend/restore, core extension/restore/auto-extension and discovery modules, three CLI commands (guard, costs, restore), daemon wiring to run auto-extensions, YAML configuration management, and accompanying tests.

Changes

Soroban Contract Extension & Management

Layer / File(s) Summary
RPC client, passphrases & polling; config and config tests
src/rpc/client.ts, src/utils/config.ts, tests/utils/config.test.ts
Add simulate/submit flows for ExtendFootprintTtl and restoreFootprint, transaction polling, network passphrase resolution, and YAML-backed Sentinel config load/save with tests verifying defaults, parsing, saving, and round-trip.
Core extension, restore, auto-extension & discovery
src/core/extension.ts, src/core/discovery.ts, tests/core/extension.test.ts
Implement simulateExtension, extendEntries, restoreEntries, runAutoExtensions, and on-chain storage-key discovery. Functions validate inputs, interact with RPC and DB, record extension history, upsert discovered entries, resolve signing secrets, and aggregate per-contract results. Tests cover success, failure, policy filtering, threshold logic, env-sourced keys, and network filtering.
CLI: guard, costs, restore
src/commands/guard.ts, src/commands/costs.ts, src/commands/restore.ts
guard manages auto-extension policies (enable/disable, persist public key/source, dry-run simulation, manual extension); costs reports extension history, aggregates, per-entry-type breakdowns, and optional 30-day projection; restore restores archived entries with spinner and result reporting. All validate inputs, resolve keys, and exit on errors.
Daemon loop & CLI entry
src/daemon/loop.ts, src/index.ts
Daemon now runs runAutoExtensions as an isolated step per cycle and logs metrics/errors without aborting the cycle. CLI entry registers the new commands and bumps version.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped through ledgers in the night,

keys unearthed beneath soft moonlight,
TTLs stretched to keep things warm,
guards set watch against the storm,
carrot-cheers for code made right!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main change: implementing core business logic for TTL extension, restore, discovery, and CLI commands, which aligns with the substantial additions across RPC client, core modules, and command implementations.
Description check ✅ Passed The PR description is comprehensive and directly related to the changeset, providing detailed summaries of all major changes, architectural decisions, test coverage, and a review guide.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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

Copilot AI 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.

Pull request overview

This PR upgrades Soroban Sentinel from monitoring-only into an operational TTL management layer by adding Soroban transaction support (extend/restore), implementing core TTL extension/auto-extension + event-based discovery logic, and wiring new production CLI commands into the daemon loop.

Changes:

  • Added RPC transaction build/simulate/sign/submit + polling for ExtendFootprintTTLOp and RestoreFootprintOp.
  • Implemented core extension/auto-extension/restore flows (DB updates + extension history) and event-based storage key discovery.
  • Replaced CLI placeholders with guard, costs, and restore commands; added YAML config load/save utilities and tests.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
tests/utils/config.test.ts Adds unit coverage for YAML config load/save behavior and defaults.
tests/core/extension.test.ts Adds unit coverage for extension/restore/simulation/auto-extension flows with mocked RPC.
src/utils/config.ts Implements persistent YAML config load/save with defaults and logging.
src/rpc/client.ts Extends RPC client with Soroban TTL extension/restore transaction support and polling.
src/core/extension.ts Implements simulate/extend/auto-extend/restore business logic with DB updates + history recording.
src/core/discovery.ts Implements event-scan-based storage key discovery and DB upserts.
src/commands/guard.ts Adds CLI for configuring auto-extension policies + manual extend + dry-run simulation.
src/commands/costs.ts Adds CLI for extension history reporting and basic cost aggregation/projection.
src/commands/restore.ts Adds CLI for restoring archived entries (specific keys or all).
src/index.ts Wires new CLI commands into the main CLI entrypoint and bumps version.
src/daemon/loop.ts Integrates auto-extension as a new step in each daemon monitoring cycle.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/core/extension.ts Outdated
Comment thread src/core/extension.ts Outdated
Comment on lines +157 to +159
const oldTTL = dbEntry.live_until_ledger
? dbEntry.live_until_ledger - (contract.last_checked_ledger ?? freshTTLs.latestLedger)
: 0;
Comment thread src/core/discovery.ts Outdated
Comment on lines +250 to +254
// contract addresses, we need to handle the C-prefix encoding
try {
const { StrKey } = require("@stellar/stellar-sdk");
return Buffer.from(StrKey.decodeContract(contractId));
} catch {
Comment thread src/core/discovery.ts Outdated
Comment on lines +107 to +115
// Extract unique transaction IDs to fetch their footprints
const txIds = new Set<string>();
for (const event of events.events) {
if (event.id) {
// Event IDs encode the transaction — extract the ledger entry keys
// from the event topic/value which may reference storage keys
txIds.add(event.id);
}
}
Comment thread src/commands/guard.ts
Comment on lines +81 to +85
if (options.autoExtend) {
if (!keypairSource) {
console.error(chalk.red("--keypair or --keypair-env required for auto-extension"));
process.exit(1);
}
Comment thread src/commands/costs.ts
Comment on lines +26 to +27
const days = options.all ? undefined : parseInt(options.period, 10);
const history = getExtensionHistory(db, contractId, days);
Comment thread src/utils/config.ts
Comment on lines +50 to +55
return {
network: parsed.network ?? DEFAULT_CONFIG.network,
rpcUrl: parsed.rpcUrl,
pollingIntervalSeconds: parsed.pollingIntervalSeconds ?? DEFAULT_CONFIG.pollingIntervalSeconds,
slackToken: parsed.slackToken,
};
Comment thread src/rpc/client.ts Outdated
Comment on lines +201 to +203
const passphrase = this.getNetworkPassphrase();
const account = new Account(sourcePublicKey, "0");

Comment thread src/rpc/client.ts
Comment on lines +298 to +301
txHash: sendResult.hash,
ledger: 0,
error: `Transaction send error: ${sendResult.status}`,
};
Comment thread src/rpc/client.ts
Comment on lines +359 to +362
txHash: sendResult.hash,
ledger: 0,
error: `Transaction send error: ${sendResult.status}`,
};

@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: 12

🧹 Nitpick comments (1)
src/core/extension.ts (1)

221-233: ⚡ Quick win

Fetch the latest ledger once per run, not once per contract.

runAutoExtensions() creates a client and calls getCurrentLedger() inside the per-contract loop. On a larger fleet that turns one daemon tick into N identical RPC reads before any extension work even starts.

🤖 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/core/extension.ts` around lines 221 - 233, The code is calling new
StellarRpcClient(network, rpcUrl) and await client.getCurrentLedger() inside the
per-contract loop in runAutoExtensions(), causing N identical RPC reads; move
the StellarRpcClient instantiation and the await client.getCurrentLedger() call
to run once before the for (const contract of contracts) loop so a single
latestLedger value is reused for all contracts, then use that latestLedger
inside the loop (and dispose/close the client afterwards if applicable).
🤖 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 `@src/commands/costs.ts`:
- Around line 26-31: The code parses options.period into days but never
validates it, so invalid values (NaN, 0, negative) silently change behavior;
before calling getExtensionHistory and building periodLabel, validate the parsed
days from parseInt(options.period, 10) and if it is NaN or <= 0, surface a clear
user error (throw or process.exit with an explanatory message) asking for a
positive integer, otherwise use the validated days value; keep the existing
options.all override (i.e., if options.all is true, pass undefined to
getExtensionHistory and set periodLabel to "all time").

In `@src/commands/guard.ts`:
- Around line 75-78: The current branch that handles options.keypair
unconditionally assigns keypairSource = options.keypair which causes non-env
secrets to later be persisted as env:SENTINEL_SECRET_KEY and fail at daemon
runtime; modify the handling in src/commands/guard.ts so it checks whether
options.keypair starts with "env:" and only then set keypairSource to the env
reference, otherwise set keypairSource to a literal indicator (or preserve the
literal value) and set secretKey to the provided value so the policy save
persists a literal source for non-env inputs; update the logic that writes the
policy to use keypairSource/secretKey accordingly (referencing the keypairSource
and secretKey variables in the same function).

In `@src/commands/restore.ts`:
- Around line 51-56: The code currently prefers options.entry over options.all
when both are provided; change the selection logic in the restore command so it
rejects the conflicting flags instead of silently choosing one: inside the block
where entryKeys is determined (the branch using options.entry, options.all and
getEntriesForContract) add an explicit check that if both options.entry
(non-empty) and options.all are set, throw or return a user-visible error (e.g.,
throw new Error or call the command error helper) explaining that --entry and
--all are mutually exclusive; only proceed to set entryKeys from options.entry
or from getEntriesForContract when the other flag is not set. Ensure the error
path prevents further execution.

In `@src/core/discovery.ts`:
- Around line 248-257: The function decodeContractId currently calls
require("`@stellar/stellar-sdk`") at runtime which fails in ESM; replace that with
a module-scope import of StrKey (e.g., add "import { StrKey } from
'`@stellar/stellar-sdk`';" at top of the file) and then call
StrKey.decodeContract(contractId) inside decodeContractId; keep a try/catch only
around StrKey.decodeContract to fall back to Buffer.from(contractId, "hex") if
decoding throws, but do not use require() inside the function and ensure you
reference the StrKey symbol and StrKey.decodeContract in the implementation.
- Around line 89-98: The events fetch stops after the first page and
decodeContractId uses require(...) which fails in ESM; fix both by paginating
getEvents and making decodeContractId ESM-safe: replace the single call to
server.getEvents(...) with a loop that calls server.getEvents repeatedly using
the returned pagination.cursor as the start cursor (or cursor param) until no
more cursor, accumulating all events (preserve the same filters and limit per
request); and change decodeContractId to use a dynamic ESM import of
'`@stellar/stellar-sdk`' (e.g. const { StrKey } = await
import('`@stellar/stellar-sdk`')) and decode the contract ID as a StrKey (validate
it starts with 'C' and throw or return an error for invalid values) instead of
falling back to Buffer.from(hex), ensuring the function returns the correct
32-byte buffer for contract IDs.

In `@src/core/extension.ts`:
- Around line 235-239: The current filter that builds needsExtension in
extension.ts includes entries with negative or zero TTLs, mixing already-expired
rows into the extendEntries() path; update the predicate that computes remaining
(e.live_until_ledger - latestLedger) to explicitly exclude expired entries
(e.g., require remaining > 0) before comparing to
policy.extend_when_below_ledgers so only still-live rows are sent to
extendEntries(). Reference: the needsExtension filter in src/core/extension.ts
and the extendEntries() call that consumes it.
- Around line 153-183: Wrap the post-transaction DB updates into a single
database transaction so they either all commit or all roll back: call
recordExtension, upsertEntry, and updateLastCheckedLedger inside one
transactional unit (e.g., db.transaction(async tx => { ... })), and modify those
helpers to accept a transaction/context parameter (or add transactional
variants) so they execute using the same tx; ensure the tx is committed on
success and rolled back on any throw. Apply the same transactional refactor to
the other similar block that updates entries/extensions and last-checked ledger
(the second occurrence referenced in the review) so both places use the same
pattern.

In `@src/rpc/client.ts`:
- Around line 370-377: Update getNetworkPassphrase to derive the signing
passphrase from the RPC server instead of the hardcoded table: call
this.server.getNetwork() and return its .passphrase if present, falling back to
NETWORK_PASSPHRASES[this.network] only as a secondary fallback and throw only if
neither is available; and in pollTransaction, return ledger: txResponse.ledger
(using txResponse.ledger when present and handling the optional/undefined case)
instead of ledger: txResponse.latestLedger so the reported ledger is the
transaction-specific ledger from the RPC response.
- Around line 391-405: In pollTransaction, change the returned
SubmitTransactionResult.ledger field to use txResponse.ledger (the inclusion
ledger) instead of txResponse.latestLedger for both the SUCCESS and FAILED
branches; update the two return objects in pollTransaction so ledger:
txResponse.ledger is returned (keep txHash, success flag and error text
unchanged).

In `@src/utils/config.ts`:
- Around line 74-76: The config write currently uses
fs.writeFileSync(configPath, yamlStr, "utf-8") which can leave credentials
world-readable; change the write to enforce owner-only permissions by using
fs.writeFileSync(configPath, yamlStr, { encoding: "utf-8", mode: 0o600 }) or
write then call fs.chmodSync(configPath, 0o600); update the code that sets
yamlStr and calls fs.writeFileSync (references: yamlStr, configPath,
fs.writeFileSync) so the saved file has 0600 permissions immediately after
creation.

In `@tests/core/extension.test.ts`:
- Line 366: Replace any seed-like test fixtures passed to setEnv (e.g., the
TEST_SECRET_KEY literal) with an obviously fake deterministic placeholder that
still satisfies resolveSecretKey()’s shape check; for example, use a generated
value like `S${"A".repeat(55)}` instead of embedding a real-looking secret seed
string wherever setEnv("TEST_SECRET_KEY", "...") (and similar TEST_* secret
fixtures) appear in the tests so the tests remain valid but no real-looking
secrets are present.

---

Nitpick comments:
In `@src/core/extension.ts`:
- Around line 221-233: The code is calling new StellarRpcClient(network, rpcUrl)
and await client.getCurrentLedger() inside the per-contract loop in
runAutoExtensions(), causing N identical RPC reads; move the StellarRpcClient
instantiation and the await client.getCurrentLedger() call to run once before
the for (const contract of contracts) loop so a single latestLedger value is
reused for all contracts, then use that latestLedger inside the loop (and
dispose/close the client afterwards if applicable).
🪄 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: 0b2efb11-2d3b-4374-a6d6-3f57f5a92c13

📥 Commits

Reviewing files that changed from the base of the PR and between 57e9a27 and f2edb20.

📒 Files selected for processing (11)
  • src/commands/costs.ts
  • src/commands/guard.ts
  • src/commands/restore.ts
  • src/core/discovery.ts
  • src/core/extension.ts
  • src/daemon/loop.ts
  • src/index.ts
  • src/rpc/client.ts
  • src/utils/config.ts
  • tests/core/extension.test.ts
  • tests/utils/config.test.ts

Comment thread src/commands/costs.ts
Comment thread src/commands/costs.ts
Comment thread src/commands/guard.ts
Comment thread src/commands/restore.ts
Comment thread src/core/discovery.ts Outdated
Comment thread src/core/extension.ts
Comment thread src/rpc/client.ts Outdated
Comment thread src/rpc/client.ts
Comment thread src/utils/config.ts
Comment thread tests/core/extension.test.ts Outdated
AbdulmalikAlayande and others added 8 commits June 13, 2026 10:58
- Fix ESM import: use "../logging/index.js" instead of "../logging"
- Wrap post-transaction DB updates in db.transaction() for atomicity
  (both extendEntries and restoreEntries)
- Compute old_ttl_ledgers against freshTTLs.latestLedger instead of
  stale last_checked_ledger
- Exclude already-expired entries (remaining <= 0) from auto-extension
- Fetch latest ledger once per run, not once per contract

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use txResponse.ledger (inclusion ledger) instead of latestLedger
  in pollTransaction for accurate ledger reporting
- Make getNetworkPassphrase async and fetch from RPC server first,
  falling back to hardcoded table
- Fetch real account sequence for simulateExtension instead of "0"
- Include diagnostic details in send error messages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace require() with static StrKey import for ESM compatibility
- Add cursor-based pagination for getEvents to handle busy contracts
- Remove unused txIds variable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Require --keypair-env for --auto-extend (not bare --keypair) so the
  daemon can resolve the key at runtime
- Remove unused --network option
- Store keypairSource directly instead of fallback to env:SENTINEL_SECRET_KEY

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Validate --period is a positive integer before querying
- Show all entries when --all is used instead of truncating to 10

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fail fast with a clear error when both --entry and --all are provided
instead of silently preferring --entry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Write config with 0o600 permissions to protect credentials
- Validate pollingIntervalSeconds is a positive number, fallback to default

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use deterministic placeholder (S + 55 A's) instead of real-looking
Stellar secret seeds to avoid tripping secret scanner rules.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai coderabbitai Bot mentioned this pull request Jul 30, 2026
6 tasks done
AbdulmalikAlayande added a commit that referenced this pull request Aug 2, 2026
feat: implement core business logic for TTL extension, restore, discovery, and CLI commands
AbdulmalikAlayande added a commit that referenced this pull request Aug 2, 2026
feat: implement core business logic for TTL extension, restore, discovery, and CLI commands
AbdulmalikAlayande pushed a commit that referenced this pull request Aug 7, 2026
Adds a new periodic fleet-wide health digest that teams can configure
instead of (or alongside) per-entry threshold alerts.

Schema
- New digest_configs table (separate from alert_configs — no threshold_ledgers,
  no alert_config_id FK; has interval_ms instead). Justified separately because
  a digest is semantically different from a per-entry TTL alert.

src/core/digest.ts (new)
- DigestPayload type — deliberately NOT part of the AlertEvent discriminated
  union in alerts/types.ts, per the issue's explicit instruction.
- buildFleetDigest(db, network, currentLedger, options?) — reads live DB state
  synchronously, classifying every tracked entry by severity (critical/warning/ok),
  computing topExpiring contracts sorted by min remaining TTL, and summing
  extension costs for the period.

src/db/repositories.ts
- insertDigestConfig / getDigestConfigs repository helpers.
- DigestConfig interface.

src/daemon/loop.ts
- digestIntervalMs option added to DaemonOptions.
- Module-level lastDigestAt timestamp gate, following runScheduledVacuum pattern.
- runScheduledDigest() fires buildFleetDigest + deliverSingleAlert for each
  enabled digest_configs row when the interval has elapsed.
- Called from scheduledTick() before executeCycle(), isolated from cycle errors.

Tests (TDD — tests written before implementation)
- tests/core/digest.test.ts — 23 tests covering payload shape, severity
  classification, network isolation, topExpiring ordering/cap, cost aggregation,
  live fleet-state accuracy (AC #2), inactive contract exclusion, and the
  digest_configs repository functions.
- tests/daemon/digest-loop.test.ts — 8 tests covering AC #1 (fires once per
  interval, not once per monitor cycle), multi-config delivery, delivery failure
  isolation, and network filtering.

All 1342 existing tests continue to pass. Build is clean.
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