Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/src/controllers/stream.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@ export const getStreamClaimableAmount = async (req: Request, res: Response) => {
where: { streamId: parsedStreamId },
select: {
streamId: true,
tokenAddress: true,
ratePerSecond: true,
depositedAmount: true,
withdrawnAmount: true,
Expand Down Expand Up @@ -597,6 +598,7 @@ export const getUserStreamSummary = async (
where: { sender: address },
select: {
streamId: true,
tokenAddress: true,
ratePerSecond: true,
depositedAmount: true,
withdrawnAmount: true,
Expand All @@ -613,6 +615,7 @@ export const getUserStreamSummary = async (
where: { recipient: address },
select: {
streamId: true,
tokenAddress: true,
ratePerSecond: true,
depositedAmount: true,
withdrawnAmount: true,
Expand Down
1 change: 1 addition & 0 deletions backend/src/routes/v1/streams/withdraw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export const withdrawHandler = async (req: AuthenticatedRequest, res: Response)
streamId: true,
sender: true,
recipient: true,
tokenAddress: true,
ratePerSecond: true,
depositedAmount: true,
withdrawnAmount: true,
Expand Down
3 changes: 2 additions & 1 deletion backend/src/services/claimable.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const I128_MIN = -(1n << 127n);

export interface ClaimableStreamState {
streamId: number;
tokenAddress: string;
ratePerSecond: string;
depositedAmount: string;
withdrawnAmount: string;
Expand Down Expand Up @@ -102,7 +103,7 @@ export class ClaimableAmountService {
? Math.floor(requestedAt)
: Math.floor(this.nowMs() / 1000);

const cacheKey = `claimable:${stream.streamId}:${getStateFingerprint(stream)}:${calculatedAt}`;
const cacheKey = `claimable:${stream.tokenAddress}:${stream.streamId}:${getStateFingerprint(stream)}:${calculatedAt}`;
const cachedEntry = cache.get<Omit<ClaimableAmountResult, 'cached'>>(cacheKey);

if (cachedEntry) {
Expand Down
45 changes: 45 additions & 0 deletions backend/tests/claimable.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ClaimableAmountService } from '../src/services/claimable.service.js';
function makeStreamState(overrides: Partial<Parameters<ClaimableAmountService['getClaimableAmount']>[0]> = {}) {
return {
streamId: 1,
tokenAddress: 'TEST_TOKEN_ADDRESS',
ratePerSecond: '10',
depositedAmount: '100',
withdrawnAmount: '0',
Expand Down Expand Up @@ -196,4 +197,48 @@ describe('ClaimableAmountService', () => {
expect(cancelRefund + withdrawn + claimable <= deposited, `iteration ${iteration}: cancel settlement exceeded deposit`).toBe(true);
}
});

it('maintains independent cache entries for same streamId across different tokens', () => {
vi.setSystemTime(5_000);
const service = new ClaimableAmountService({
cacheTtlMs: 10_000,
});

// Two streams with the same numeric ID but different token contracts
const streamTokenA = makeStreamState({
streamId: 42,
tokenAddress: 'TOKEN_A',
ratePerSecond: '10',
depositedAmount: '1000',
withdrawnAmount: '0',
});

const streamTokenB = makeStreamState({
streamId: 42,
tokenAddress: 'TOKEN_B',
ratePerSecond: '5',
depositedAmount: '2000',
withdrawnAmount: '500',
});

const resultA = service.getClaimableAmount(streamTokenA, 100);
const resultB = service.getClaimableAmount(streamTokenB, 100);

// Both should be cache misses (different token = different cache key)
expect(resultA.cached).toBe(false);
expect(resultB.cached).toBe(false);

// The values should differ because the streams have different parameters
expect(resultA.claimableAmount).not.toBe(resultB.claimableAmount);

// Second call for Token A should be a cache hit
const resultA2 = service.getClaimableAmount(streamTokenA, 100);
expect(resultA2.cached).toBe(true);
expect(resultA2.claimableAmount).toBe(resultA.claimableAmount);

// Second call for Token B should also be a cache hit (independent of A)
const resultB2 = service.getClaimableAmount(streamTokenB, 100);
expect(resultB2.cached).toBe(true);
expect(resultB2.claimableAmount).toBe(resultB.claimableAmount);
});
});
Loading