Skip to content

Feat/transfer stream endpoint - Add transfer_stream endpoint to change recipient address - #669

Open
abbys-code-hub wants to merge 6 commits into
ritik4ever:mainfrom
abbys-code-hub:feat/transfer-stream-endpoint
Open

Feat/transfer stream endpoint - Add transfer_stream endpoint to change recipient address#669
abbys-code-hub wants to merge 6 commits into
ritik4ever:mainfrom
abbys-code-hub:feat/transfer-stream-endpoint

Conversation

@abbys-code-hub

@abbys-code-hub abbys-code-hub commented Jul 28, 2026

Copy link
Copy Markdown

Add transfer_stream endpoint to change recipient address

Closes #325

Summary

Adds POST /api/streams/:id/transfer endpoint and a "Transfer Stream" button in StreamDetailDrawer, allowing the sender to transfer a stream to a new recipient on-chain.

Changes

Backend

  • POST /api/streams/:id/transfer — new route accepting { sender, newRecipient } body with Stellar JWT auth. Validates stream state, verifies sender identity, calls transferStream(), returns updated stream with progress.

  • transferStream() in streamStore.ts — validates the stream is not finalized and new recipient differs. Submits a Soroban transfer_stream transaction (skipped if no contract configured), updates the recipient field in SQLite, records a stream_transferred event with old/new recipient in metadata, triggers a webhook.

  • transferStreamSchema in schemas.ts — Zod schema validating sender and newRecipient as valid Stellar account IDs.

  • 8 integration tests covering: success, wrong sender (403), sender mismatch (403), same recipient (400), canceled stream (400), not found (404), invalid ID (400), no auth (401).

  • "transferred" added to VALID_EVENT_TYPES for event filtering and webhook registration.

Frontend

  • Transfer Stream button in StreamDetailDrawer — visible to the sender for non-finalized streams (active/scheduled/paused). Clicking reveals an inline input for the new Stellar recipient address with client-side format validation.

  • transferStream() API function with auth token support and JSON body.

  • handleTransfer() callback in DashboardPage — calls the API with the connected wallet address as sender.

  • CSS styles for the transfer input group with dark mode support.

Infrastructure fixes (pre-existing issues)

  • Migration 005 — adds missing cliff_seconds column to streams and stream_archive tables.
  • initDb() — restored runMigrations() call lost during the PostgreSQL backend refactor (feat(database): migrate SQLite schema to support optional PostgreSQL … #640).
  • syncFtsIndex / searchStreamsFts — restored stub functions removed during the PostgreSQL refactor.
  • calculateProgress() — fixed duplicate elapsed/ratio declarations.
  • validateEnv.ts — fixed duplicate validateEnv() function and duplicate isProduction declaration.
  • Recipient streams route — removed duplicate parsedQuery/query/data declarations in the route handler.

Testing

cd backend && npm test -- -t "POST /api/streams/:id/transfer"

All 8 integration tests pass ✅

Commits

Commit Description
ab84d75 feat(backend): add transferStream service function and schema
5b4f30b feat(backend): add POST /api/streams/:id/transfer route and integration tests
365422c feat(frontend): add Transfer Stream button in StreamDetailDrawer
9abdfd9 fix(backend): add missing cliff_seconds migration and restore db init

Summary by CodeRabbit

  • New Features

    • Added sender-only “Transfer Stream” flow with a recipient update UI action and dashboard wiring.
    • Introduced an authenticated POST /api/streams/:id/transfer endpoint and matching client helper.
    • Added transferred activity events with webhook support.
    • Added cliff_seconds to stream records (defaulting to 0).
  • Bug Fixes

    • Improved stream progress stability for paused and zero-duration cases.
    • Made recipient stream listings apply consistent sorting and progress timing.
    • Hardened transfer eligibility and state updates for canceled/completed streams.
  • Tests

    • Added integration tests covering transfer success, auth, validation, and error handling.

- Add transferStream() to streamStore: validates stream state,
  submits Soroban transfer_stream tx, updates recipient in SQLite,
  records 'transferred' event, triggers webhook
- Add transferStreamSchema: validates sender and newRecipient
  as Stellar account IDs
- Add 'transferred' to VALID_EVENT_TYPES in schemas
- Fix duplicate elapsed/ratio declarations in calculateProgress
- Fix indentation in cancelStream triggerWebhook call
…on tests

- Add transfer route: validates stream ID, sender auth,
  body schema, calls transferStream, returns updated stream
- Add 8 integration tests covering: success, wrong sender,
  sender mismatch, same recipient, canceled stream,
  not found, invalid ID, no auth
- Fix duplicate query/data declarations in recipient streams
  route handler
- Add transferStream() API function with auth token support
- Add 'transferred' to StreamEvent eventType union
- Add Transfer Stream button in StreamDetailDrawer for sender
  with inline Stellar address input and validation
- Add handleTransfer callback in DashboardPage
- Add CSS styles for transfer input group with dark mode
- Add migration 005 to create cliff_seconds column on streams
  and stream_archive tables
- Restore runMigrations() call in initDb() (lost in Postgres PR)
- Add syncFtsIndex and searchStreamsFts stub functions that
  were removed during Postgres backend refactor
- Fix duplicate validateEnv() function and duplicate
  isProduction declaration in validateEnv.ts
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

@abbys-code-hub is attempting to deploy a commit to the ritik4ever'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 end-to-end stream recipient transfers with Soroban execution, authenticated API handling, SQLite event updates, frontend controls, and integration tests. It also updates migrations, FTS helpers, progress calculations, recipient sorting, persistence, and environment validation.

Changes

Stream transfer workflow

Layer / File(s) Summary
Transfer contracts and event types
backend/src/validation/schemas.ts, frontend/src/services/api.ts
Validates transfer recipients and propagates the transferred event type.
Transfer service and API route
backend/src/services/streamStore.ts, backend/src/index.ts, backend/src/integration.test.ts
Submits transfer_stream, updates SQLite, records transfer metadata, invalidates caches, exposes the authenticated route, and tests success and error cases.
Transfer controls and API wiring
frontend/src/components/StreamDetailDrawer.tsx, frontend/src/pages/DashboardPage.tsx, frontend/src/services/api.ts, frontend/src/index.css
Adds sender-only transfer controls, recipient validation, API wiring, refreshes, notifications, event display, and styling.

Backend consistency updates

Layer / File(s) Summary
SQLite migrations and FTS compatibility
backend/migrations/*, backend/src/services/db.ts, backend/src/services/migrations.ts
Adds and reverts cliff_seconds, runs SQLite migrations at startup, handles legacy migration baselines, and provides SQLite/PostgreSQL FTS helpers.
Progress and stream persistence behavior
backend/src/services/streamStore.ts, backend/src/index.ts
Updates progress calculations, removes archived_at from SQLite upserts, hardens stream ID parsing, improves search errors, and applies query sorting with a consistent progress timestamp.
Environment validation compatibility
backend/src/config/validateEnv.ts
Synchronizes legacy and canonical variable names before validation and uses parsed environment data after successful validation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant StreamDetailDrawer
  participant DashboardPage
  participant API
  participant TransferRoute
  participant transferStream
  participant Soroban
  participant SQLite
  User->>StreamDetailDrawer: Enter new recipient
  StreamDetailDrawer->>DashboardPage: Invoke onTransfer
  DashboardPage->>API: POST transfer request
  API->>TransferRoute: Send authenticated request
  TransferRoute->>transferStream: Validate and transfer stream
  transferStream->>Soroban: Submit transfer_stream
  Soroban-->>transferStream: Return transaction result
  transferStream->>SQLite: Update recipient and record event
  SQLite-->>TransferRoute: Return updated stream
  TransferRoute-->>API: Return transfer response
  API-->>DashboardPage: Return updated stream
  DashboardPage-->>StreamDetailDrawer: Refresh and show status
Loading

Possibly related PRs

Suggested reviewers: chkm001, osagiecynthia

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The transfer endpoint and UI are implemented, but the request body no longer accepts the required sender field from issue #325. Include and validate sender in the transfer request body, or update the issue spec and frontend contract to match the auth flow.
Out of Scope Changes check ⚠️ Warning Several migration, validation, FTS, progress, and search fixes are unrelated to issue #325's transfer-stream scope. Move the non-transfer fixes into a separate PR so this change set stays focused on the transfer endpoint and drawer UI.
Docstring Coverage ⚠️ Warning Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the transfer-stream endpoint change and matches the main PR theme.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
🔧 Fix failing CI
  • Fix failing CI in branch feat/transfer-stream-endpoint

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.

@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 (6)
backend/src/integration.test.ts (2)

1436-1462: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Seeded rows are never cleaned up.

Each beforeEach inserts a stream that persists for the rest of the file's run; suites asserting on list totals or stats could become order-dependent. Add an afterEach deleting the seeded stream and its events.

🤖 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 `@backend/src/integration.test.ts` around lines 1436 - 1462, In the test setup
surrounding beforeEach, add an afterEach cleanup that deletes the stream
identified by transferStreamId and removes any associated events from the
database. Ensure cleanup runs after every test so list totals and stats remain
isolated and order-independent.

1544-1558: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider covering the paused and scheduled cases.

The suite covers canceled (400) but not the "paused/scheduled transfers are allowed" behavior that transferStream explicitly permits.

🤖 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 `@backend/src/integration.test.ts` around lines 1544 - 1558, Add integration
coverage alongside the canceled-stream test for paused and scheduled streams,
verifying that transferStream permits both cases and returns the expected
successful response. Reuse the existing stream setup, authentication, keypairs,
and transfer request pattern, while updating each stream’s state to represent
paused or scheduled behavior.
backend/src/services/streamStore.ts (2)

1308-1308: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Floating promise on triggerWebhook.

triggerWebhook is async; leaving it unawaited without a .catch risks an unhandled rejection. Prefer void triggerWebhook(...).catch(...) or await it.

🤖 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 `@backend/src/services/streamStore.ts` at line 1308, Update the triggerWebhook
call in the surrounding stream transfer flow to handle its returned promise
explicitly: await it when the caller supports asynchronous control flow, or use
void with a catch handler to report failures. Preserve the existing
"transferred" event and stream arguments.

1210-1213: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Service accepts no sender, so authorization lives only in the route.

The doc comment states "Only the sender may transfer", but the function cannot enforce it. Any future caller (worker, script, another route) bypasses the check. Consider taking sender and asserting stream.sender === sender here as defense in depth.

🤖 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 `@backend/src/services/streamStore.ts` around lines 1210 - 1213, Update
transferStream to accept a sender identity and enforce that the stream’s sender
matches it before transferring. Add the authorization check inside
transferStream so all callers are protected, and update its callers to pass the
authenticated sender while preserving the existing transfer behavior for
authorized requests.
frontend/src/components/StreamDetailDrawer.tsx (1)

74-74: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

cliff_reached has no icon/label mapping.

StreamEvent["eventType"] includes cliff_reached, which falls through to the raw snake_case string in the history list. Worth adding alongside transferred.

Also applies to: 87-87

🤖 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 `@frontend/src/components/StreamDetailDrawer.tsx` at line 74, Add an icon/label
mapping for the cliff_reached event type alongside the transferred mapping in
the event display configuration, so StreamEvent history renders a friendly label
instead of the raw snake_case value.
frontend/src/index.css (1)

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

outline: none drops the focus indicator in forced-colors mode.

The box-shadow replacement is stripped in Windows High Contrast Mode, leaving no visible focus. Keep a transparent outline on focus so the UA can render one.

♻️ Proposed tweak
 .transfer-recipient-input:focus {
   border-color: `#0ea5a5`;
   box-shadow: 0 0 0 3px rgba(14, 165, 165, 0.15);
+  outline: 2px solid transparent;
+  outline-offset: 2px;
 }
🤖 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 `@frontend/src/index.css` around lines 553 - 560, Update the
.transfer-recipient-input:focus styles to avoid removing the browser focus
indicator in forced-colors mode: replace the outline:none behavior with a
transparent outline that allows the user agent to render its forced-colors focus
indicator, while preserving the existing border-color and box-shadow styles.
🤖 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 `@backend/src/config/validateEnv.ts`:
- Around line 125-126: Use one canonical Stellar environment contract throughout
validateEnv: update validation and returned configuration to consistently read
STELLAR_CONTRACT_ID, SOROBAN_RPC_URL, and STELLAR_NETWORK instead of legacy
keys, or restore the removed mapping. Ensure deployments supplying the canonical
variables validate successfully and legacy-only values do not bypass validation,
then update adjacent tests to cover the canonical names.

In `@backend/src/integration.test.ts`:
- Around line 1484-1489: Update the transfer-event assertions near the
`stream_events` query to parse `events[0].metadata` and verify it contains both
the previous recipient and `newRecipientKeypair.publicKey()`. Preserve the
existing single-event assertion and use the test’s existing symbol for the old
recipient.

In `@backend/src/services/db.ts`:
- Around line 342-344: In the FTS handling around the shown catch blocks in the
database service, only suppress the expected missing-table condition; log and
rethrow all other SQL, schema, malformed-MATCH, and I/O errors so search
failures propagate to the API instead of returning empty success results. Apply
this to both the catch near lines 342 and 357, preserving normal handling when
the FTS table is genuinely absent.
- Around line 347-348: Update searchStreamsFts so PostgreSQL does not silently
return an empty successful result: implement equivalent PostgreSQL full-text
search, or explicitly reject/disable the search request with the endpoint’s
established error behavior. Preserve the existing non-PostgreSQL search path and
ensure /api/streams/search communicates the unsupported or implemented
PostgreSQL outcome.
- Around line 338-355: Update the SQLite statements in the streams FTS write
path and searchStreamsFts to replace positional ? placeholders with named `@name`
bindings, and pass matching named-property objects to run() and all(). Preserve
the existing values, query behavior, and result handling.
- Line 328: Update the migration flow invoked by runMigrations so an existing
database with streams but an empty schema_migrations table does not return after
baseline seeding; execute the pending migration up scripts, including additions
such as cliff_seconds, while preserving baseline behavior for already-current
schemas.

In `@backend/src/services/streamStore.ts`:
- Around line 1299-1305: The transfer transaction around upsertStream must not
clear an archived stream’s archived_at value. Update upsertStream or the
transfer flow to preserve the existing archivedAt when transferring, while
retaining null for genuinely unarchived streams; alternatively reject transfers
of archived streams before the transaction.
- Around line 1287-1305: Move the cache invalidation calls and stats/metrics
cache resets in the stream transfer flow to after the `db.transaction` commit,
so readers cannot repopulate stale values before persistence completes. Also
update the `stream.recipient` mutation in this flow to occur only after a
successful transaction, or restore the previous recipient when the transaction
fails, ensuring in-memory state remains consistent with SQLite.
- Line 1245: Validate the stream ID before the nativeToScVal call in the
surrounding stream operation, requiring the entire id to be a valid numeric
value rather than allowing parseInt to partially parse it. Reject invalid IDs,
including values like “12abc” or “abc”, before conversion, and use an explicit
radix when converting the validated ID to the u64 argument.

In `@frontend/src/components/StreamDetailDrawer.tsx`:
- Around line 550-561: Update the transfer UI in StreamDetailDrawer to retain a
ref to the “⇄ Transfer Stream” trigger and return focus to it whenever the
transfer input closes, including the Cancel handler and successful submission
path. Focus the trigger after the input unmounts so keyboard navigation remains
anchored within the drawer.
- Around line 122-125: Reset showTransferInput, newRecipient, and transferError
when streamId changes, alongside the existing fetchData reset logic in
StreamDetailDrawer. Ensure the drawer starts each stream with the transfer input
hidden, an empty recipient, and no transfer error.

In `@frontend/src/pages/DashboardPage.tsx`:
- Around line 272-289: Update the page-level handleTransfer function to rethrow
transfer failures after displaying the toast, including the disconnected-wallet
early-return path, so StreamDetailDrawer.handleTransfer can enter its failure
branch and keep the form open with transferError. Preserve the existing success
flow only for completed transfers.

---

Nitpick comments:
In `@backend/src/integration.test.ts`:
- Around line 1436-1462: In the test setup surrounding beforeEach, add an
afterEach cleanup that deletes the stream identified by transferStreamId and
removes any associated events from the database. Ensure cleanup runs after every
test so list totals and stats remain isolated and order-independent.
- Around line 1544-1558: Add integration coverage alongside the canceled-stream
test for paused and scheduled streams, verifying that transferStream permits
both cases and returns the expected successful response. Reuse the existing
stream setup, authentication, keypairs, and transfer request pattern, while
updating each stream’s state to represent paused or scheduled behavior.

In `@backend/src/services/streamStore.ts`:
- Line 1308: Update the triggerWebhook call in the surrounding stream transfer
flow to handle its returned promise explicitly: await it when the caller
supports asynchronous control flow, or use void with a catch handler to report
failures. Preserve the existing "transferred" event and stream arguments.
- Around line 1210-1213: Update transferStream to accept a sender identity and
enforce that the stream’s sender matches it before transferring. Add the
authorization check inside transferStream so all callers are protected, and
update its callers to pass the authenticated sender while preserving the
existing transfer behavior for authorized requests.

In `@frontend/src/components/StreamDetailDrawer.tsx`:
- Line 74: Add an icon/label mapping for the cliff_reached event type alongside
the transferred mapping in the event display configuration, so StreamEvent
history renders a friendly label instead of the raw snake_case value.

In `@frontend/src/index.css`:
- Around line 553-560: Update the .transfer-recipient-input:focus styles to
avoid removing the browser focus indicator in forced-colors mode: replace the
outline:none behavior with a transparent outline that allows the user agent to
render its forced-colors focus indicator, while preserving the existing
border-color and box-shadow styles.
🪄 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: de38cce2-bcc2-4955-875e-e2a047f7ca3c

📥 Commits

Reviewing files that changed from the base of the PR and between b3d32c1 and 9abdfd9.

⛔ Files ignored due to path filters (1)
  • backend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • backend/migrations/005_add_cliff_seconds.down.sql
  • backend/migrations/005_add_cliff_seconds.sql
  • backend/src/config/validateEnv.ts
  • backend/src/index.ts
  • backend/src/integration.test.ts
  • backend/src/services/db.ts
  • backend/src/services/streamStore.ts
  • backend/src/validation/schemas.ts
  • frontend/src/components/StreamDetailDrawer.tsx
  • frontend/src/index.css
  • frontend/src/pages/DashboardPage.tsx
  • frontend/src/services/api.ts

Comment thread backend/src/config/validateEnv.ts
Comment thread backend/src/integration.test.ts Outdated
Comment thread backend/src/services/db.ts
Comment thread backend/src/services/db.ts
Comment thread backend/src/services/db.ts Outdated
Comment on lines +1287 to +1305
const now = nowInSeconds();
stream.recipient = newRecipient;

// Invalidate cache
await invalidateCache(`stream:${id}`);
await invalidateCache("streams:list:");
await invalidateCache("streams:export:");
resetStatsCache();
resetStreamMetricsCache();

// Atomically write the updated stream row and the transfer event.
const db = getDb();
db.transaction(() => {
upsertStream(stream);
recordEventWithDb(db, stream.id, "transferred", now, stream.sender, undefined, {
oldRecipient,
newRecipient,
});
})();

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

Cache is invalidated before the DB write commits.

Between the invalidateCache calls and the db.transaction(...) execution there is an await-free but still interleavable window (the invalidations are awaited, so other requests run in between) during which a concurrent read repopulates stream:<id> / list caches with the old recipient. That stale entry then survives the commit. Move the invalidation after the transaction. Also note stream.recipient is mutated before persistence, so a failed transaction leaves an in-memory record that disagrees with SQLite.

♻️ Proposed reordering
   const now = nowInSeconds();
   stream.recipient = newRecipient;
 
-  // Invalidate cache
-  await invalidateCache(`stream:${id}`);
-  await invalidateCache("streams:list:");
-  await invalidateCache("streams:export:");
-  resetStatsCache();
-  resetStreamMetricsCache();
-
   // Atomically write the updated stream row and the transfer event.
   const db = getDb();
   db.transaction(() => {
     upsertStream(stream);
     recordEventWithDb(db, stream.id, "transferred", now, stream.sender, undefined, {
       oldRecipient,
       newRecipient,
     });
   })();
+
+  // Invalidate cache only after the write is durable.
+  await invalidateCache(`stream:${id}`);
+  await invalidateCache("streams:list:");
+  await invalidateCache("streams:export:");
+  resetStatsCache();
+  resetStreamMetricsCache();
📝 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
const now = nowInSeconds();
stream.recipient = newRecipient;
// Invalidate cache
await invalidateCache(`stream:${id}`);
await invalidateCache("streams:list:");
await invalidateCache("streams:export:");
resetStatsCache();
resetStreamMetricsCache();
// Atomically write the updated stream row and the transfer event.
const db = getDb();
db.transaction(() => {
upsertStream(stream);
recordEventWithDb(db, stream.id, "transferred", now, stream.sender, undefined, {
oldRecipient,
newRecipient,
});
})();
const now = nowInSeconds();
stream.recipient = newRecipient;
// Atomically write the updated stream row and the transfer event.
const db = getDb();
db.transaction(() => {
upsertStream(stream);
recordEventWithDb(db, stream.id, "transferred", now, stream.sender, undefined, {
oldRecipient,
newRecipient,
});
})();
// Invalidate cache only after the write is durable.
await invalidateCache(`stream:${id}`);
await invalidateCache("streams:list:");
await invalidateCache("streams:export:");
resetStatsCache();
resetStreamMetricsCache();
🤖 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 `@backend/src/services/streamStore.ts` around lines 1287 - 1305, Move the cache
invalidation calls and stats/metrics cache resets in the stream transfer flow to
after the `db.transaction` commit, so readers cannot repopulate stale values
before persistence completes. Also update the `stream.recipient` mutation in
this flow to occur only after a successful transaction, or restore the previous
recipient when the transaction fails, ensuring in-memory state remains
consistent with SQLite.

Comment thread backend/src/services/streamStore.ts
Comment thread frontend/src/components/StreamDetailDrawer.tsx
Comment thread frontend/src/components/StreamDetailDrawer.tsx
Comment thread frontend/src/pages/DashboardPage.tsx

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/services/api.ts (1)

257-268: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Include the required sender field in the transfer request.

The transfer endpoint contract requires { sender, newRecipient }, but this wrapper serializes only newRecipient. Pass the authenticated wallet/sender address from the caller and update the related call sites and tests; otherwise the request can fail validation or lack the sender identity required for authorization.

🤖 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 `@frontend/src/services/api.ts` around lines 257 - 268, Update transferStream
to accept the authenticated sender address from its caller and serialize both
sender and newRecipient in the transfer request body. Update all transferStream
call sites and related tests to provide and verify the sender field while
preserving the existing authorization-header behavior.
🤖 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 `@backend/src/config/validateEnv.ts`:
- Around line 126-130: Update the environment normalization logic in validateEnv
so STELLAR_NETWORK values are converted to their corresponding Stellar network
passphrases before populating NETWORK_PASSPHRASE. Preserve the existing
passphrase input behavior, and ensure values such as “public” resolve to the
established actual passphrase expected by the returned configuration.
- Around line 114-124: Update the environment normalization logic in validateEnv
to detect when both legacy and canonical variables are set with different values
for CONTRACT_ID/STELLAR_CONTRACT_ID or RPC_URL/SOROBAN_RPC_URL. Fail fast on
each conflict, or consistently normalize both names to one canonical value
before validation and config construction so validation and runtime use
identical settings.

In `@backend/src/services/migrations.ts`:
- Around line 81-95: Update the migration execution around loadMigrationSql and
db.exec so a whole-script “already exists” or “duplicate column” error is not
treated as successful. Split and execute statements with per-statement
idempotency handling, or validate all required legacy-schema postconditions
before recording the migration; only insert the migration record after every
statement is applied or confirmed present, while rethrowing unrelated errors.

---

Outside diff comments:
In `@frontend/src/services/api.ts`:
- Around line 257-268: Update transferStream to accept the authenticated sender
address from its caller and serialize both sender and newRecipient in the
transfer request body. Update all transferStream call sites and related tests to
provide and verify the sender field while preserving the existing
authorization-header behavior.
🪄 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: fa5e78e6-0e37-45ad-976f-9745a0bedb70

📥 Commits

Reviewing files that changed from the base of the PR and between 9abdfd9 and db9d640.

📒 Files selected for processing (10)
  • backend/src/config/validateEnv.ts
  • backend/src/index.ts
  • backend/src/integration.test.ts
  • backend/src/services/db.ts
  • backend/src/services/migrations.ts
  • backend/src/services/streamStore.ts
  • backend/src/validation/schemas.ts
  • frontend/src/components/StreamDetailDrawer.tsx
  • frontend/src/pages/DashboardPage.tsx
  • frontend/src/services/api.ts
💤 Files with no reviewable changes (2)
  • backend/src/validation/schemas.ts
  • backend/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • frontend/src/pages/DashboardPage.tsx
  • backend/src/integration.test.ts
  • backend/src/services/db.ts
  • frontend/src/components/StreamDetailDrawer.tsx
  • backend/src/services/streamStore.ts

Comment on lines +114 to +124
if (!process.env.STELLAR_CONTRACT_ID && process.env.CONTRACT_ID) {
process.env.STELLAR_CONTRACT_ID = process.env.CONTRACT_ID;
}
if (!process.env.CONTRACT_ID && process.env.STELLAR_CONTRACT_ID) {
process.env.CONTRACT_ID = process.env.STELLAR_CONTRACT_ID;
}
if (!process.env.SOROBAN_RPC_URL && process.env.RPC_URL) {
process.env.SOROBAN_RPC_URL = process.env.RPC_URL;
}
if (!process.env.RPC_URL && process.env.SOROBAN_RPC_URL) {
process.env.RPC_URL = process.env.SOROBAN_RPC_URL;

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

Reject conflicting legacy and canonical values.

When both names are set with different values, this block leaves both unchanged. Validation then uses env.CONTRACT_ID/env.RPC_URL, while the returned config uses STELLAR_CONTRACT_ID/SOROBAN_RPC_URL, allowing startup to validate one contract or RPC endpoint and operate against another. Normalize to one canonical value before validation, or fail fast on conflicts.

🤖 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 `@backend/src/config/validateEnv.ts` around lines 114 - 124, Update the
environment normalization logic in validateEnv to detect when both legacy and
canonical variables are set with different values for
CONTRACT_ID/STELLAR_CONTRACT_ID or RPC_URL/SOROBAN_RPC_URL. Fail fast on each
conflict, or consistently normalize both names to one canonical value before
validation and config construction so validation and runtime use identical
settings.

Comment on lines +126 to +130
if (!process.env.STELLAR_NETWORK && process.env.NETWORK_PASSPHRASE) {
process.env.STELLAR_NETWORK = process.env.NETWORK_PASSPHRASE;
}
if (!process.env.NETWORK_PASSPHRASE && process.env.STELLAR_NETWORK) {
process.env.NETWORK_PASSPHRASE = process.env.STELLAR_NETWORK;

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

Do not copy STELLAR_NETWORK directly into NETWORK_PASSPHRASE.

For STELLAR_NETWORK=public, this assigns "public" as the passphrase; the returned config consequently exposes "public" instead of "Public Global Stellar Network ; October 2015", contrary to backend/src/config/validateEnv.test.ts:706-715. Map network names to their actual passphrases before returning the config.

🤖 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 `@backend/src/config/validateEnv.ts` around lines 126 - 130, Update the
environment normalization logic in validateEnv so STELLAR_NETWORK values are
converted to their corresponding Stellar network passphrases before populating
NETWORK_PASSPHRASE. Preserve the existing passphrase input behavior, and ensure
values such as “public” resolve to the established actual passphrase expected by
the returned configuration.

Comment on lines +81 to +95
const upSql = loadMigrationSql(migration.upPath);
try {
db.exec(upSql);
} catch (err: any) {
// Only suppress "already exists" errors from legacy schemas
// (e.g., CREATE TABLE, ALTER TABLE ADD COLUMN). Rethrow others.
if (err?.message && (
err.message.includes("already exists") ||
err.message.includes("duplicate column")
)) {
// Expected — migration already reflected in legacy schema.
} else {
throw err;
}
}

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 | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

while IFS= read -r file; do
  count=$(( (rg -o ';' "$file" || true) | wc -l ))
  if [ "$count" -gt 1 ]; then
    printf '%s contains multiple SQL statements\n' "$file"
  fi
done < <(fd --type f --extension sql . backend/migrations | sort)

Repository: ritik4ever/stellar-stream

Length of output: 215


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf 'Repository files matching migrations.sql/migrations.ts:\n'
git ls-files | rg '(^backend/src/services/migrations\.ts$|^backend/migrations/.+\.sql$|migrations)' | sed -n '1,200p'

printf '\nbackend/src/services/migrations.ts relevant section:\n'
if [ -f backend/src/services/migrations.ts ]; then
  nl -ba backend/src/services/migrations.ts | sed -n '1,150p'
fi

printf '\nMigration SQL files with statement counts:\n'
python3 - <<'PY'
import subprocess, pathlib
files = subprocess.check_output(['git','ls-files'], text=True).splitlines()
sql = [f for f in files if f.startswith('backend/migrations/') and f.endswith('.sql')]
for f in sql:
    txt = pathlib.Path(f).read_text(errors='replace')
    print(f"{f}: {sum(1 for i,c in enumerate(txt) if c==';' and not txt[i-7:i+3].lower().endswith('begin;'))}")
PY

Repository: ritik4ever/stellar-stream

Length of output: 974


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf 'Relevant migration service implementation:\n'
awk '{printf "%6d\t%s\n", NR, $0}' backend/src/services/migrations.ts | sed -n '1,160p'

printf '\nMigration SQL statement counts:\n'
python3 - <<'PY'
import subprocess, pathlib
files = subprocess.check_output(['git','ls-files'], text=True).splitlines()
for f in files:
    if f.startswith('backend/migrations/') and f.endswith('.sql'):
        txt = pathlib.Path(f).read_text(errors='replace')
        stmts = txt.count(';')
        print(f"{f}: {stmts}")
PY

Repository: ritik4ever/stellar-stream

Length of output: 6391


Do not record a migration applied after suppressing a whole-script failure.

backend/migrations/001_initial_schema.sql contains multiple SQL statements, and db.exec(upSql) will execute them all. Suppressing “already exists” / “duplicate column” errors after a failed statement can record a partially applied schema as complete. Apply idempotency per statement or check legacy schema postconditions before inserting the migration record.

🧰 Tools
🪛 OpenGrep (1.25.0)

[ERROR] 83-83: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🤖 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 `@backend/src/services/migrations.ts` around lines 81 - 95, Update the
migration execution around loadMigrationSql and db.exec so a whole-script
“already exists” or “duplicate column” error is not treated as successful. Split
and execute statements with per-statement idempotency handling, or validate all
required legacy-schema postconditions before recording the migration; only
insert the migration record after every statement is applied or confirmed
present, while rethrowing unrelated errors.

Source: MCP tools

@abbys-code-hub

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/src/services/streamStore.ts (2)

1235-1277: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not persist a transfer when Soroban is unavailable.

All Soroban dependencies and CONTRACT_ID are optional here; if any are missing, execution falls through to the SQLite update as though the on-chain transfer succeeded. Fail with a configuration/service error instead, preserving the required on-chain-before-local-update invariant.

🤖 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 `@backend/src/services/streamStore.ts` around lines 1235 - 1277, Update the
transfer_stream transaction flow around getSorobanContext, rpcServer,
serverKeypair, and CONTRACT_ID so any missing Soroban dependency or contract
configuration throws a configuration/service error instead of continuing to the
SQLite update. Preserve the existing on-chain submission and confirmation
behavior, and only allow the local transfer persistence after the on-chain
transaction succeeds.

468-479: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make zero-duration progress start-aware and finite.

The durationSeconds <= 0 branch reports 100% vested even when at < startAt, producing a scheduled stream with its full amount vested. It also returns Infinity, which serializes to null in JSON. Use a ratio of 0 before startAt, 1 afterward, and a finite/API-supported rate value.

🤖 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 `@backend/src/services/streamStore.ts` around lines 468 - 479, Update the
progress calculations in the stream status return block to make zero-duration
streams start-aware: use a ratio of 0 when at is before stream.startAt and 1 at
or after it, so elapsed and vested amounts remain unstarted before the start
time. Replace the zero-duration ratePerSecond Infinity result with a finite
API-supported value while preserving the existing behavior for positive
durations.
🤖 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 `@backend/src/services/streamStore.ts`:
- Around line 175-181: Update parseStreamIdAsNumber to preserve valid unsigned
64-bit stream IDs without Number conversion: return a typed bigint and pass that
value directly to Soroban u64 arguments. Apply the same change at the call sites
around lines 499, 540, 1153, and 1245, while retaining numeric-format validation
and the existing invalid-ID error behavior.

---

Outside diff comments:
In `@backend/src/services/streamStore.ts`:
- Around line 1235-1277: Update the transfer_stream transaction flow around
getSorobanContext, rpcServer, serverKeypair, and CONTRACT_ID so any missing
Soroban dependency or contract configuration throws a configuration/service
error instead of continuing to the SQLite update. Preserve the existing on-chain
submission and confirmation behavior, and only allow the local transfer
persistence after the on-chain transaction succeeds.
- Around line 468-479: Update the progress calculations in the stream status
return block to make zero-duration streams start-aware: use a ratio of 0 when at
is before stream.startAt and 1 at or after it, so elapsed and vested amounts
remain unstarted before the start time. Replace the zero-duration ratePerSecond
Infinity result with a finite API-supported value while preserving the existing
behavior for positive durations.
🪄 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: f2265c1e-7de0-4175-a8b3-0fbea79eed48

📥 Commits

Reviewing files that changed from the base of the PR and between db9d640 and 32ffad2.

📒 Files selected for processing (3)
  • backend/src/index.ts
  • backend/src/services/db.ts
  • backend/src/services/streamStore.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/src/index.ts

Comment on lines +175 to +181
function parseStreamIdAsNumber(id: string): number {
if (!/^\d+$/.test(id)) {
const err: any = new Error("Invalid stream ID: must be a numeric value.");
err.statusCode = 400;
throw err;
}
return parseInt(id, 10);

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

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
rg -n 'parseStreamIdAsNumber|nativeToScVal' backend/src/services/streamStore.ts
npm view `@stellar/stellar-sdk`@14.5.0 version

Repository: ritik4ever/stellar-stream

Length of output: 1136


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== streamStore relevant sections =="
sed -n '160,190p;520,555p;1135,1165p;1230,1260p' backend/src/services/streamStore.ts

echo
echo "== static bigint/scVal evidence from npm package =="
tmp="$(mktemp -d)"
(
  cd "$tmp"
  npm pack `@stellar/stellar-sdk`@14.5.0 >/dev/null
  tar -xzf stellar-stellar-sdk-14.5.0.tgz
  rg -n "nativeToScVal|function nativeToScVal|nativeToScVal:" package -C 4
  node - <<'JS'
const StellarSdk = require('./package');
for (const id of ["9007199254740992", "9007199254740993", "9223372036854775807", "9223372036854775808"]) {
  for (const arg of [id, BigInt(id), parseInt(id, 10)]) {
    try {
      const scVal = StellarSdk.xdr.ScVal.nativeToScVal(arg, { type: "u64" });
      console.log(`id=${id} nativeToScVal(${arg === BigInt(id) ? `BigInt(${id})` : arg}) type=${scVal()?.type?.value?.toString() || scVal?.value?.toString()}`);
    } catch (e) {
      console.log(`id=${id} nativeToScVal(${arg === BigInt(id) ? `BigInt(${id})` : arg}) ERROR ${String(e.message ?? e)}`);
    }
  }
}
JS
)
rm -rf "$tmp"

Repository: ritik4ever/stellar-stream

Length of output: 50382


Keep Soroban ID arguments as strings/bigints.

parseStreamIdAsNumber() and parseInt(id, 10) can round valid unsigned 64-bit stream IDs above Number.MAX_SAFE_INTEGER, such as "9007199254740993", causing Soroban calls to target the wrong stream. Return a typed bigint and pass it directly as the u64 argument, or reject IDs above the safe-integer limit before converting.

Also applies to lines 499, 540, 1153, and 1245.

🤖 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 `@backend/src/services/streamStore.ts` around lines 175 - 181, Update
parseStreamIdAsNumber to preserve valid unsigned 64-bit stream IDs without
Number conversion: return a typed bigint and pass that value directly to Soroban
u64 arguments. Apply the same change at the call sites around lines 499, 540,
1153, and 1245, while retaining numeric-format validation and the existing
invalid-ID error behavior.

@ritik4ever

Copy link
Copy Markdown
Owner

Hi @abbys-code-hub,

This PR could not be merged because it has merge conflicts with the target branch.

Please resolve the merge conflicts, push the updated changes, and the PR can be reviewed and merged.

Thank you!

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.

Add transfer_stream endpoint to change recipient address

2 participants