Skip to content

Add GET /api/streams/:id/claimable#485

Merged
ritik4ever merged 3 commits into
ritik4ever:mainfrom
mikevill20:end-point
May 28, 2026
Merged

Add GET /api/streams/:id/claimable#485
ritik4ever merged 3 commits into
ritik4ever:mainfrom
mikevill20:end-point

Conversation

@mikevill20
Copy link
Copy Markdown

@mikevill20 mikevill20 commented May 27, 2026

I Added GET /api/streams/:id/claimable that calls Soroban claimable(stream_id, now) and returns the live value.

This PR closes #455

Summary by CodeRabbit

  • New Features

    • Added a claimable-amount API to show real-time payout availability for streams, with rate limiting.
  • Bug Fixes

    • Corrected elapsed-time calculation for paused streams.
  • Documentation

    • Updated API docs/spec to describe the new claimable endpoint and its responses.
  • Tests

    • Added integration tests covering successful responses, paused/canceled streams, error cases, and rate-limit behavior.
  • Refactor

    • Removed duplicate implementations and consolidated stream-related service logic.

Review Change Stack

@vercel
Copy link
Copy Markdown

vercel Bot commented May 27, 2026

@mikevill20 is attempting to deploy a commit to the ritik4ever's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave
Copy link
Copy Markdown

drips-wave Bot commented May 27, 2026

@mikevill20 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 27, 2026

Warning

Review limit reached

@ritik4ever, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 52 minutes and 19 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 22f99894-4756-4d76-b184-fb14b67f28a4

📥 Commits

Reviewing files that changed from the base of the PR and between d80fe8d and 1b6049d.

📒 Files selected for processing (3)
  • backend/src/index.ts
  • backend/src/integration.test.ts
  • backend/src/services/streamStore.ts
📝 Walkthrough

Walkthrough

This PR implements GET /api/streams/:id/claimable to return a stream's real-time claimable amount via Soroban contract simulation; it adds on-chain helpers, a per-IP rate limiter, Swagger docs, integration tests with RPC mocking, and a small dependency/devDependency update.

Changes

On-chain claimable amount endpoint

Layer / File(s) Summary
Soroban contract integration
backend/src/services/streamStore.ts
Adds getOnChainClaimableAmount(id) and getLatestLedgerTime() to simulate the contract's claimable method at the latest ledger close time; fixes calculateProgress elapsed-time computation for paused streams; removes duplicate synchronous pauseStream/resumeStream exports.
API endpoint with rate limiting
backend/src/index.ts
Adds claimableLimiter (configurable CLAIMABLE_RATE_LIMIT, default 30) and the GET /api/streams/:id/claimable endpoint that validates the stream, returns 0 for paused/canceled streams using getLatestLedgerTime, or queries getOnChainClaimableAmount for active streams; errors are normalized via sendApiError.
OpenAPI specification
backend/src/swagger.ts
Documents /api/streams/{id}/claimable with path parameter id, 200 success schema (streamId, claimableAmount, assetCode, at), and 404/500 error responses referencing existing Error schema.
Integration testing and dependencies
backend/src/integration.test.ts, backend/package.json
Mocks @stellar/stellar-sdk RPC (getLatestLedger, simulateTransaction, prepareTransaction) and Api.isSimulationSuccess in Vitest; adds comprehensive tests for the claimable endpoint (success, paused/canceled, not-found/invalid-id, and rate-limiting after 30 requests); adds prom-client and reorders a devDependency.

Sequence Diagram

sequenceDiagram
  participant Client
  participant Endpoint as GET /api/streams/:id/claimable
  participant streamStore
  participant SorobanRPC as Soroban RPC
  Client->>Endpoint: GET /api/streams/:id/claimable
  Endpoint->>streamStore: getOnChainClaimableAmount(id)
  streamStore->>streamStore: getLatestLedgerTime()
  streamStore->>SorobanRPC: simulateTransaction(claimable at ledger time)
  SorobanRPC-->>streamStore: simulation result (ScVal)
  streamStore-->>Endpoint: {claimableAmount, at}
  Endpoint-->>Client: {streamId, claimableAmount, assetCode, at}
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

I nibble bytes near Soroban's stream,
Counting carrots in a ledger dream.
Thirty hops then I pause to rest,
Returning claimable gems at your request.
A rabbit's cheer for on-chain zest! 🐇✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive The PR includes an intentional correction to calculateProgress and removal of duplicate pauseStream/resumeStream exports, which are closely related refactoring supporting the new functionality but not explicitly listed as requirements. Clarify whether the corrections to calculateProgress and removal of duplicate exports were necessary for the claimable endpoint or should be addressed separately.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and concisely describes the main change: adding a new GET endpoint for retrieving claimable amounts from streams.
Linked Issues check ✅ Passed All acceptance criteria from issue #455 are met: the endpoint calls Soroban claimable with current ledger timestamp, returns correct JSON shape, handles paused/canceled streams, implements rate limiting (30/min per IP), documents in Swagger, and includes integration tests.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

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

86-87: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Duplicate object key overwrites null-coalescing fallback.

The second pausedDuration assignment on line 87 overwrites the first on line 86, discarding the ?? 0 fallback. If row.paused_duration is null, the record will have pausedDuration: null instead of 0, causing arithmetic errors downstream.

🐛 Proposed fix
   pausedAt: row.paused_at ?? undefined,
-  pausedDuration: row.paused_duration ?? 0,
   pausedDuration: row.paused_duration,
+  pausedDuration: row.paused_duration ?? 0,
 };
🤖 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 86 - 87, Remove the
duplicate pausedDuration property so the null-coalescing fallback is preserved:
keep the assignment using row.paused_duration ?? 0 and delete the second
pausedDuration: row.paused_duration that overwrites it; update the object
mapping in streamStore.ts (the code that maps DB row to the output record where
pausedDuration and row.paused_duration appear) to only use pausedDuration:
row.paused_duration ?? 0 so downstream arithmetic sees 0 instead of null.
🧹 Nitpick comments (5)
backend/src/index.ts (2)

454-463: 💤 Low value

Consider handling completedAt similarly to paused/canceled.

The endpoint returns claimableAmount: 0 early for paused and canceled streams, avoiding an RPC call. Completed streams still call getOnChainClaimableAmount. If completed streams always have 0 claimable, short-circuiting them here would reduce unnecessary RPC calls.

🤖 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/index.ts` around lines 454 - 463, The early-return checks for
pausedAt and canceledAt should also include completedAt to avoid unnecessary
RPCs: update the condition that checks stream.pausedAt !== undefined ||
stream.canceledAt !== undefined to also check stream.completedAt !== undefined
so completed streams return the same short-circuit response (using
getLatestLedgerTime() and res.json with claimableAmount: 0) instead of calling
getOnChainClaimableAmount.

468-468: 💤 Low value

Redundant Number() conversion.

claimableAmount is already a number from getOnChainClaimableAmount. The Number() wrapper is unnecessary.

♻️ Proposed fix
-      claimableAmount: Number(claimableAmount),
+      claimableAmount,
🤖 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/index.ts` at line 468, The field assignment wraps claimableAmount
in an unnecessary Number() call; remove the Number(...) wrapper and assign
claimableAmount directly (the value returned from getOnChainClaimableAmount is
already a number). Update the object property (the line containing
"claimableAmount: Number(claimableAmount)," in backend/src/index.ts) to use
"claimableAmount: claimableAmount," (or the shorthand "claimableAmount," if the
surrounding object allows) so no runtime conversion is performed.
backend/src/services/streamStore.ts (1)

425-425: 💤 Low value

Refine closeTime handling in getOnChainClaimableAmount

rpcServer.getLatestLedger().closeTime is returned as a string Unix timestamp in @stellar/stellar-sdk v14.x, so parseInt(latestLedger.closeTime, 10) is appropriate and not subject to bigint truncation. Low-effort readability: Number(latestLedger.closeTime) is a cleaner alternative.

🤖 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 425, The code in
getOnChainClaimableAmount uses parseInt on latestLedger.closeTime which is a
string Unix timestamp; replace parseInt(latestLedger.closeTime, 10) with
Number(latestLedger.closeTime) (or equivalent explicit numeric cast) when
computing the variable at to improve readability and clarity while preserving
correct numeric conversion of latestLedger.closeTime.
backend/src/swagger.ts (1)

697-711: 💤 Low value

Consider documenting the rate limit in the description.

The PR objectives specify a 30 calls per minute per IP rate limit, but this isn't mentioned in the endpoint description. While rate limiting details aren't always included in OpenAPI specs, adding a note would improve developer experience.

📝 Suggested description enhancement
-        description: "Retrieves the current real-time claimable amount for a stream using Soroban contract simulation. Returns 0 if paused, canceled, or before the cliff.",
+        description: "Retrieves the current real-time claimable amount for a stream using Soroban contract simulation. Returns 0 if paused, canceled, or before the cliff. Rate limited to 30 requests per minute per IP.",
🤖 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/swagger.ts` around lines 697 - 711, Update the OpenAPI
description for the "/api/streams/{id}/claimable" GET operation to mention the
endpoint's rate limit (30 calls per minute per IP); locate the path object for
"/api/streams/{id}/claimable" in backend/src/swagger.ts and append a concise
note to the existing description (or add a "x-rate-limit" vendor extension) so
consumers see "Rate limit: 30 requests per minute per IP" alongside the existing
caveats about paused/canceled/cliff states.
backend/src/integration.test.ts (1)

440-550: ⚡ Quick win

Add an explicit before-cliff integration case for claimable=0.

The suite covers paused/canceled, but it does not explicitly validate the “before cliff returns 0” contract from the PR objective.

Proposed test addition
+      it("should return 0 before cliff/start conditions", async () => {
+        const db = getDb();
+        const future = Math.floor(Date.now() / 1000) + 7200;
+        db.prepare("UPDATE streams SET start_at = ? WHERE id = ?").run(future, mockStream.id);
+
+        mockGetLatestLedger.mockResolvedValue({
+          sequence: 12345,
+          closeTime: "1716812160",
+        });
+
+        const response = await request(app).get(`/api/streams/${mockStream.id}/claimable`);
+
+        expect(response.status).toBe(200);
+        expect(response.body.claimableAmount).toBe(0);
+      });
🤖 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 440 - 550, Add a new
integration test in the "GET /api/streams/:id/claimable" suite (e.g., it("should
return 0 before cliff", ...)) that updates the test stream via getDb() to set
cliff_at to a timestamp in the future (or set start_at earlier and cliff_at >
now) so the current time is before the cliff; call
mockGetLatestLedger.mockResolvedValue(...) to supply the ledger closeTime; then
hit GET /api/streams/${mockStream.id}/claimable and assert status 200,
response.body.claimableAmount === 0, response.body.streamId === mockStream.id
and response.body.assetCode === mockStream.assetCode, and assert
mockSimulateTransaction was not called. Use the existing patterns in the file
(mockGetLatestLedger, mockSimulateTransaction, getDb, mockStream) to implement
this test.
🤖 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/integration.test.ts`:
- Around line 440-550: The rate-limit test is flaky because claimableLimiter
(created once in backend/src/index.ts) retains consumed tokens across earlier
tests; reset the limiter before the 31-request loop by forcing module reload and
re-creating the app so a fresh claimableLimiter is instantiated (use
vi.resetModules() then re-import the app used by request(app)), or add and call
a clear/reset function on claimableLimiter before the loop; ensure you reference
claimableLimiter and the app instance used in the test so the limiter state is
fresh when asserting 30 OK and 1 429.

In `@backend/src/swagger.ts`:
- Around line 713-728: The 200 response schema for the
/api/streams/{id}/claimable endpoint is missing a required array; update the
object schema (the response block that currently defines properties streamId,
claimableAmount, assetCode, and at) to include a required:
["streamId","claimableAmount","assetCode","at"] entry so the OpenAPI contract
explicitly marks those fields as mandatory.

---

Outside diff comments:
In `@backend/src/services/streamStore.ts`:
- Around line 86-87: Remove the duplicate pausedDuration property so the
null-coalescing fallback is preserved: keep the assignment using
row.paused_duration ?? 0 and delete the second pausedDuration:
row.paused_duration that overwrites it; update the object mapping in
streamStore.ts (the code that maps DB row to the output record where
pausedDuration and row.paused_duration appear) to only use pausedDuration:
row.paused_duration ?? 0 so downstream arithmetic sees 0 instead of null.

---

Nitpick comments:
In `@backend/src/index.ts`:
- Around line 454-463: The early-return checks for pausedAt and canceledAt
should also include completedAt to avoid unnecessary RPCs: update the condition
that checks stream.pausedAt !== undefined || stream.canceledAt !== undefined to
also check stream.completedAt !== undefined so completed streams return the same
short-circuit response (using getLatestLedgerTime() and res.json with
claimableAmount: 0) instead of calling getOnChainClaimableAmount.
- Line 468: The field assignment wraps claimableAmount in an unnecessary
Number() call; remove the Number(...) wrapper and assign claimableAmount
directly (the value returned from getOnChainClaimableAmount is already a
number). Update the object property (the line containing "claimableAmount:
Number(claimableAmount)," in backend/src/index.ts) to use "claimableAmount:
claimableAmount," (or the shorthand "claimableAmount," if the surrounding object
allows) so no runtime conversion is performed.

In `@backend/src/integration.test.ts`:
- Around line 440-550: Add a new integration test in the "GET
/api/streams/:id/claimable" suite (e.g., it("should return 0 before cliff",
...)) that updates the test stream via getDb() to set cliff_at to a timestamp in
the future (or set start_at earlier and cliff_at > now) so the current time is
before the cliff; call mockGetLatestLedger.mockResolvedValue(...) to supply the
ledger closeTime; then hit GET /api/streams/${mockStream.id}/claimable and
assert status 200, response.body.claimableAmount === 0, response.body.streamId
=== mockStream.id and response.body.assetCode === mockStream.assetCode, and
assert mockSimulateTransaction was not called. Use the existing patterns in the
file (mockGetLatestLedger, mockSimulateTransaction, getDb, mockStream) to
implement this test.

In `@backend/src/services/streamStore.ts`:
- Line 425: The code in getOnChainClaimableAmount uses parseInt on
latestLedger.closeTime which is a string Unix timestamp; replace
parseInt(latestLedger.closeTime, 10) with Number(latestLedger.closeTime) (or
equivalent explicit numeric cast) when computing the variable at to improve
readability and clarity while preserving correct numeric conversion of
latestLedger.closeTime.

In `@backend/src/swagger.ts`:
- Around line 697-711: Update the OpenAPI description for the
"/api/streams/{id}/claimable" GET operation to mention the endpoint's rate limit
(30 calls per minute per IP); locate the path object for
"/api/streams/{id}/claimable" in backend/src/swagger.ts and append a concise
note to the existing description (or add a "x-rate-limit" vendor extension) so
consumers see "Rate limit: 30 requests per minute per IP" alongside the existing
caveats about paused/canceled/cliff states.
🪄 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

Run ID: c8ede560-f14e-455b-bf6b-616ec62faf85

📥 Commits

Reviewing files that changed from the base of the PR and between b6e4bea and 9c838da.

⛔ Files ignored due to path filters (1)
  • backend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • backend/package.json
  • backend/src/index.ts
  • backend/src/integration.test.ts
  • backend/src/services/streamStore.ts
  • backend/src/swagger.ts

Comment on lines +440 to +550
describe("GET /api/streams/:id/claimable", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("should return 200 and the claimable amount from Soroban simulation", async () => {
mockGetLatestLedger.mockResolvedValue({
sequence: 12345,
closeTime: "1716812160",
});

mockSimulateTransaction.mockResolvedValue({
kind: "success",
result: {
retval: 450,
},
});

const response = await request(app).get(`/api/streams/${mockStream.id}/claimable`);

expect(response.status).toBe(200);
expect(response.body).toEqual({
streamId: mockStream.id,
claimableAmount: 450,
assetCode: mockStream.assetCode,
at: 1716812160,
});

expect(mockGetLatestLedger).toHaveBeenCalled();
expect(mockSimulateTransaction).toHaveBeenCalled();
});

it("should return 0 when stream is paused", async () => {
const db = getDb();
db.prepare("UPDATE streams SET paused_at = ? WHERE id = ?").run(Math.floor(Date.now() / 1000) - 1000, mockStream.id);

mockGetLatestLedger.mockResolvedValue({
sequence: 12345,
closeTime: "1716812160",
});

const response = await request(app).get(`/api/streams/${mockStream.id}/claimable`);

expect(response.status).toBe(200);
expect(response.body).toEqual({
streamId: mockStream.id,
claimableAmount: 0,
assetCode: mockStream.assetCode,
at: 1716812160,
});

expect(mockSimulateTransaction).not.toHaveBeenCalled();
});

it("should return 0 when stream is canceled", async () => {
const db = getDb();
db.prepare("UPDATE streams SET canceled_at = ? WHERE id = ?").run(Math.floor(Date.now() / 1000) - 1000, mockStream.id);

mockGetLatestLedger.mockResolvedValue({
sequence: 12345,
closeTime: "1716812160",
});

const response = await request(app).get(`/api/streams/${mockStream.id}/claimable`);

expect(response.status).toBe(200);
expect(response.body).toEqual({
streamId: mockStream.id,
claimableAmount: 0,
assetCode: mockStream.assetCode,
at: 1716812160,
});

expect(mockSimulateTransaction).not.toHaveBeenCalled();
});

it("should return 404 for non-existent stream", async () => {
const response = await request(app).get("/api/streams/999/claimable");

expect(response.status).toBe(404);
expect(response.body.error).toBe("Stream not found.");
});

it("should return 400 for invalid stream ID", async () => {
const response = await request(app).get("/api/streams/invalid-id/claimable");

expect(response.status).toBe(400);
expect(response.body.error).toContain("Stream ID must be");
});

it("should enforce rate limit of 30 requests per minute", async () => {
mockGetLatestLedger.mockResolvedValue({
sequence: 12345,
closeTime: "1716812160",
});
mockSimulateTransaction.mockResolvedValue({
kind: "success",
result: { retval: 10 },
});

for (let i = 0; i < 31; i++) {
const response = await request(app).get(`/api/streams/${mockStream.id}/claimable`);
if (i < 30) {
expect(response.status).toBe(200);
} else {
expect(response.status).toBe(429);
expect(response.body.code).toBe("RATE_LIMIT_EXCEEDED");
}
}
});
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify claimable limiter definition and whether tests reset limiter state.
rg -n -C3 'claimableLimiter|rateLimit\(|/api/streams/:id/claimable|RATE_LIMIT_EXCEEDED' backend/src/index.ts backend/src/integration.test.ts
rg -n -C2 'beforeEach|afterEach|clearAllMocks|resetAllMocks' backend/src/integration.test.ts

Repository: ritik4ever/stellar-stream

Length of output: 5960


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show claimable limiter config and middleware usage
nl -ba backend/src/index.ts | sed -n '160,240p'
nl -ba backend/src/index.ts | sed -n '420,480p'

# Show test suite top-level beforeEach(s) and any rate-limit cleanup
# around the claimable describe and also earlier common beforeEach
nl -ba backend/src/integration.test.ts | sed -n '1,140p'
nl -ba backend/src/integration.test.ts | sed -n '420,620p'

# Search for any explicit reset/clear of express-rate-limit store
rg -n "claimableLimiter|express-rate-limit|rateLimit\(|reset|clear.*rate|limiter" backend/src/integration.test.ts

Repository: ritik4ever/stellar-stream

Length of output: 112


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Claimable limiter definition and route wiring
echo "### backend/src/index.ts: claimableLimiter config"
sed -n '180,220p' backend/src/index.ts
echo "### backend/src/index.ts: /claimable route wiring"
sed -n '420,465p' backend/src/index.ts

# Test suite initialization/fixtures
echo "### backend/src/integration.test.ts: imports + global hooks"
sed -n '1,120p' backend/src/integration.test.ts
echo "### backend/src/integration.test.ts: around claimable describe"
sed -n '400,590p' backend/src/integration.test.ts

# Search for limiter store reset/clear or re-instantiation
echo "### Search for express-rate-limit / limiter reset in tests"
rg -n "express-rate-limit|rateLimit\(|claimableLimiter|limiter|resetKey|reset|clear.*limiter|clear.*rate" backend/src/integration.test.ts

Repository: ritik4ever/stellar-stream

Length of output: 13170


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "vi\.resetModules\(|resetModules|claimableLimiter|rateLimit" backend/src/integration.test.ts

Repository: ritik4ever/stellar-stream

Length of output: 51


Fix flaky /api/streams/:id/claimable rate-limit test caused by shared limiter state.

  • claimableLimiter is created once in backend/src/index.ts and attached as middleware for /api/streams/:id/claimable; the test only does vi.clearAllMocks(), which won’t reset the limiter’s in-memory bucket.
  • The earlier tests in the same GET /api/streams/:id/claimable block already hit the endpoint, consuming quota—so the expectation that the first 30 looped requests are always 200 (and the 31st is always 429) can fail.
  • Reset/clear the claimable limiter bucket before the rate-limit loop (e.g., re-import server/app after vi.resetModules() or expose a way to clear the limiter store), or adjust assertions to account for already-consumed quota.
🤖 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 440 - 550, The rate-limit test
is flaky because claimableLimiter (created once in backend/src/index.ts) retains
consumed tokens across earlier tests; reset the limiter before the 31-request
loop by forcing module reload and re-creating the app so a fresh
claimableLimiter is instantiated (use vi.resetModules() then re-import the app
used by request(app)), or add and call a clear/reset function on
claimableLimiter before the loop; ensure you reference claimableLimiter and the
app instance used in the test so the limiter state is fresh when asserting 30 OK
and 1 429.

Comment thread backend/src/swagger.ts
Comment on lines +713 to +728
"200": {
description: "Real-time claimable amount and query context.",
content: {
"application/json": {
schema: {
type: "object",
properties: {
streamId: { type: "string", example: "1" },
claimableAmount: { type: "number", example: 450.123456 },
assetCode: { type: "string", example: "USDC" },
at: { type: "integer", description: "Ledger timestamp at which query was simulated", example: 1716812160 },
},
},
},
},
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify the claimable endpoint response structure

# Search for the claimable endpoint handler implementation
rg -nP -A 10 "get.*['\"/]api/streams/:id/claimable" --type=ts

# Also check for response construction patterns
rg -nP -C 3 "streamId.*claimableAmount.*assetCode" --type=ts

Repository: ritik4ever/stellar-stream

Length of output: 731


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the full claimable handler block (approx) around the route definition
sed -n '420,520p' backend/src/index.ts

# Narrow search for how the response is sent within the claimable handler
rg -n "claimableLimiter|/api/streams/:id/claimable|claimable" backend/src/index.ts | head -n 50

# Find where the handler sends the 200 JSON (res.json / res.send / helper calls)
rg -n "app\.get\(\"/api/streams/:id/claimable\"|res\.json\(|res\.send\(|sendSuccess|sendApiSuccess" backend/src/index.ts -n

Repository: ritik4ever/stellar-stream

Length of output: 4438


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the swagger schema around the referenced lines
sed -n '690,760p' backend/src/swagger.ts

# Locate and inspect the helper functions that produce `at` and `claimableAmount`
rg -n "function getLatestLedgerTime|getLatestLedgerTime\s*=" backend/src -S
rg -n "function getOnChainClaimableAmount|getOnChainClaimableAmount\s*=" backend/src -S

Repository: ritik4ever/stellar-stream

Length of output: 2491


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '380,520p' backend/src/services/streamStore.ts

Repository: ritik4ever/stellar-stream

Length of output: 4326


Update claimable swagger contract: structure matches implementation; add required fields

The /api/streams/{id}/claimable 200 response is correctly defined as an unwrapped root object (streamId, claimableAmount, assetCode, at) and the endpoint implementation returns the same shape (no data wrapper).
The schema still omits required; add it to make the contract explicit.

Option 2: Add required fields
                 schema: {
                   type: "object",
+                  required: ["streamId", "claimableAmount", "assetCode", "at"],
                   properties: {
                     streamId: { type: "string", example: "1" },
                     claimableAmount: { type: "number", example: 450.123456 },
                     assetCode: { type: "string", example: "USDC" },
                     at: { type: "integer", description: "Ledger timestamp at which query was simulated", example: 1716812160 },
                   },
                 },
📝 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
"200": {
description: "Real-time claimable amount and query context.",
content: {
"application/json": {
schema: {
type: "object",
properties: {
streamId: { type: "string", example: "1" },
claimableAmount: { type: "number", example: 450.123456 },
assetCode: { type: "string", example: "USDC" },
at: { type: "integer", description: "Ledger timestamp at which query was simulated", example: 1716812160 },
},
},
},
},
},
"200": {
description: "Real-time claimable amount and query context.",
content: {
"application/json": {
schema: {
type: "object",
required: ["streamId", "claimableAmount", "assetCode", "at"],
properties: {
streamId: { type: "string", example: "1" },
claimableAmount: { type: "number", example: 450.123456 },
assetCode: { type: "string", example: "USDC" },
at: { type: "integer", description: "Ledger timestamp at which query was simulated", example: 1716812160 },
},
},
},
},
},
🤖 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/swagger.ts` around lines 713 - 728, The 200 response schema for
the /api/streams/{id}/claimable endpoint is missing a required array; update the
object schema (the response block that currently defines properties streamId,
claimableAmount, assetCode, and at) to include a required:
["streamId","claimableAmount","assetCode","at"] entry so the OpenAPI contract
explicitly marks those fields as mandatory.

@ritik4ever ritik4ever merged commit f392f45 into ritik4ever:main May 28, 2026
0 of 2 checks passed
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 GET /api/streams/:id/claimable endpoint for real-time claimable amount

2 participants