Skip to content

Commit 1cc1e1f

Browse files
authored
Merge pull request #2 from LabsCrypt/main
Merged
2 parents 9dbe613 + ed02dc6 commit 1cc1e1f

18 files changed

Lines changed: 138 additions & 71 deletions

File tree

backend/docs/DEPRECATION_POLICY.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,10 @@ X-API-Migration-Path: /v1/streams
148148
- `/streams``/v1/streams`
149149
- `/events``/v1/events`
150150

151-
**Status:** Deprecated (as of 2024-02-21)
151+
**Status:** Removed (deprecated 2024-02-21, sunset 2024-12-31, handlers deleted 2026-06-30)
152152

153-
**Sunset Date:** 2024-12-31
153+
These routes no longer exist in the codebase. Clients still calling the unversioned
154+
paths will receive a 404. Update all callers to use the `/v1/` prefix.
154155

155156
**Migration:**
156157
```javascript

backend/src/app.ts

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -105,35 +105,6 @@ app.use((req: Request, res: Response, next: NextFunction) => {
105105
return next(); // Not versioned, continue to deprecated handlers
106106
});
107107

108-
// Legacy routes (deprecated - redirect to v1)
109-
// These will be removed in a future version
110-
// Only match unversioned requests
111-
app.use('/streams', (req: Request, res: Response, next) => {
112-
res.status(410).json({
113-
error: 'Deprecated endpoint',
114-
message: 'This endpoint has been deprecated. Please use /v1/streams instead.',
115-
deprecated: true,
116-
migration: {
117-
old: '/streams',
118-
new: '/v1/streams',
119-
},
120-
sunsetDate: '2024-12-31',
121-
});
122-
});
123-
124-
app.use('/events', (req: Request, res: Response, next) => {
125-
res.status(410).json({
126-
error: 'Deprecated endpoint',
127-
message: 'This endpoint has been deprecated. Please use /v1/events instead.',
128-
deprecated: true,
129-
migration: {
130-
old: '/events',
131-
new: '/v1/events',
132-
},
133-
sunsetDate: '2024-12-31',
134-
});
135-
});
136-
137108
// Health check routes
138109
app.use('/health', healthRoutes);
139110

backend/src/workers/soroban-event-worker.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { rpc, xdr, StrKey } from '@stellar/stellar-sdk';
2-
import { prisma } from '../lib/prisma.js';
2+
import { prisma, Prisma } from '../lib/prisma.js';
33
import { INDEXER_STATE_ID } from '../lib/indexer-state.js';
44
import { sseService } from '../services/sse.service.js';
55
import logger from '../logger.js';
@@ -146,7 +146,7 @@ export class SorobanEventWorker {
146146
this.pollTimer = setTimeout(() => this.poll(), this.pollIntervalMs);
147147
}
148148

149-
private async ensureSystemStream(tx: any): Promise<void> {
149+
private async ensureSystemStream(tx: Prisma.TransactionClient): Promise<void> {
150150
const systemUser = 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF';
151151
await tx.user.upsert({
152152
where: { publicKey: systemUser },
@@ -351,7 +351,7 @@ export class SorobanEventWorker {
351351
const newFeeRateBps = decodeU32(body['new_fee_rate_bps']);
352352
const timestamp = Math.floor(Date.now() / 1000);
353353

354-
await prisma.$transaction(async (tx: any) => {
354+
await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
355355
await this.ensureSystemStream(tx);
356356

357357
await tx.streamEvent.upsert({
@@ -399,7 +399,7 @@ export class SorobanEventWorker {
399399
const newAdmin = decodeAddress(body['new_admin']);
400400
const timestamp = Math.floor(Date.now() / 1000);
401401

402-
await prisma.$transaction(async (tx: any) => {
402+
await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
403403
await this.ensureSystemStream(tx);
404404

405405
await tx.streamEvent.upsert({
@@ -462,7 +462,7 @@ export class SorobanEventWorker {
462462
? null
463463
: startTime + Number(BigInt(depositedAmount) / ratePerSecondBigInt);
464464

465-
await prisma.$transaction(async (tx: any) => {
465+
await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
466466
await tx.user.upsert({
467467
where: { publicKey: sender },
468468
create: { publicKey: sender },
@@ -551,7 +551,7 @@ export class SorobanEventWorker {
551551
const newDepositedAmount = decodeI128(body['new_deposited_amount']);
552552
const timestamp = Math.floor(Date.now() / 1000);
553553

554-
await prisma.$transaction(async (tx: any) => {
554+
await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
555555
const stream = await tx.stream.findUniqueOrThrow({
556556
where: { streamId },
557557
select: { ratePerSecond: true, startTime: true, totalPausedDuration: true }
@@ -622,7 +622,7 @@ export class SorobanEventWorker {
622622
const amount = decodeI128(body['amount']);
623623
const timestamp = Number(decodeU64(body['timestamp']));
624624

625-
await prisma.$transaction(async (tx: any) => {
625+
await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
626626
const stream = await tx.stream.findUniqueOrThrow({
627627
where: { streamId },
628628
select: { withdrawnAmount: true },
@@ -688,7 +688,7 @@ export class SorobanEventWorker {
688688
const refundedAmount = decodeI128(body['refunded_amount']);
689689
const timestamp = Math.floor(Date.now() / 1000);
690690

691-
await prisma.$transaction(async (tx: any) => {
691+
await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
692692
await tx.stream.update({
693693
where: { streamId },
694694
data: {
@@ -746,7 +746,7 @@ export class SorobanEventWorker {
746746
const totalWithdrawn = decodeI128(body['total_withdrawn']);
747747
const timestamp = Math.floor(Date.now() / 1000);
748748

749-
await prisma.$transaction(async (tx: any) => {
749+
await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
750750
await tx.stream.update({
751751
where: { streamId },
752752
data: {
@@ -853,7 +853,7 @@ export class SorobanEventWorker {
853853
const pausedAt = Number(decodeU64(body['paused_at']));
854854
const timestamp = Math.floor(Date.now() / 1000);
855855

856-
await prisma.$transaction(async (tx: any) => {
856+
await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
857857
await tx.stream.update({
858858
where: { streamId },
859859
data: {
@@ -910,7 +910,7 @@ export class SorobanEventWorker {
910910
const newEndTime = Number(decodeU64(body['new_end_time']));
911911
const timestamp = Math.floor(Date.now() / 1000);
912912

913-
await prisma.$transaction(async (tx: any) => {
913+
await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
914914
// Get current stream to calculate paused duration
915915
const currentStream = await tx.stream.findUniqueOrThrow({
916916
where: { streamId },

contracts/stream_contract/src/lib.rs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -342,12 +342,12 @@ impl StreamContract {
342342

343343
let elapsed = effective_now.saturating_sub(stream.last_update_time);
344344

345-
// Use checked_sub for deposited - withdrawn calculation
346-
// Underflow (withdrawn > deposited) falls back to 0.
345+
// Clamp to 0: withdrawn_amount should never exceed deposited_amount in
346+
// normal flow, but guard defensively so the function never returns negative.
347347
let remaining = stream
348348
.deposited_amount
349-
.checked_sub(stream.withdrawn_amount)
350-
.unwrap_or_default();
349+
.saturating_sub(stream.withdrawn_amount)
350+
.max(0);
351351

352352
// Use checked_mul to prevent overflow when multiplying rate * elapsed.
353353
// If overflow would occur, cap at the remaining balance.
@@ -602,11 +602,8 @@ impl StreamContract {
602602
let remaining = stream
603603
.deposited_amount
604604
.saturating_sub(stream.withdrawn_amount);
605-
let new_end_time = if stream.rate_per_second > 0 {
606-
now + (remaining / stream.rate_per_second) as u64
607-
} else {
608-
now
609-
};
605+
// rate_per_second is guaranteed >= 1 due to create_stream's InvalidRate guard
606+
let new_end_time = now + (remaining / stream.rate_per_second) as u64;
610607

611608
stream.paused = false;
612609
stream.paused_at = None;

contracts/stream_contract/src/test.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,6 +1042,36 @@ fn test_claimable_max_i128_rate_overflow() {
10421042
assert_eq!(withdrawn, 1_000);
10431043
}
10441044

1045+
// ─── #795 calculate_claimable underflow guard ─────────────────────────────────
1046+
1047+
#[test]
1048+
fn test_calculate_claimable_underflow_returns_zero() {
1049+
let env = Env::default();
1050+
env.mock_all_auths();
1051+
1052+
let (token, _) = create_token(&env);
1053+
let sender = Address::generate(&env);
1054+
let recipient = Address::generate(&env);
1055+
mint(&env, &token, &sender, 1_000);
1056+
1057+
let client = create_contract(&env);
1058+
let stream_id = client.create_stream(&sender, &recipient, &token, &1_000, &1_000);
1059+
1060+
// Forcibly set withdrawn_amount > deposited_amount to exercise the underflow guard.
1061+
let mut stream = client.get_stream(&stream_id).unwrap();
1062+
stream.withdrawn_amount = stream.deposited_amount + 1;
1063+
env.as_contract(&client.address, || {
1064+
env.storage()
1065+
.persistent()
1066+
.set(&types::DataKey::Stream(stream_id), &stream);
1067+
});
1068+
1069+
// calculate_claimable uses checked_sub(...).unwrap_or_default(), so the
1070+
// underflow must return 0 rather than panicking or wrapping.
1071+
let claimable = client.get_claimable_amount(&stream_id).unwrap();
1072+
assert_eq!(claimable, 0);
1073+
}
1074+
10451075
// ─── #232 create_stream edge cases ───────────────────────────────────────────
10461076

10471077
#[test]
@@ -1245,6 +1275,35 @@ fn test_withdraw_full_balance() {
12451275
assert_eq!(s.status, StreamStatus::Completed);
12461276
}
12471277

1278+
#[test]
1279+
fn test_withdraw_rejects_double_withdraw_after_completion() {
1280+
let env = Env::default();
1281+
env.mock_all_auths();
1282+
let (token, _) = create_token(&env);
1283+
let sender = Address::generate(&env);
1284+
let recipient = Address::generate(&env);
1285+
mint(&env, &token, &sender, 500);
1286+
1287+
let client = create_contract(&env);
1288+
let id = client.create_stream(&sender, &recipient, &token, &500, &100);
1289+
1290+
// Fully drain the stream via withdraw.
1291+
env.ledger().with_mut(|l| l.timestamp += 200);
1292+
let claimed = client.withdraw(&recipient, &id);
1293+
assert_eq!(claimed, 500);
1294+
1295+
// Verify stream is now inactive and completed.
1296+
let s = client.get_stream(&id).unwrap();
1297+
assert!(!s.is_active);
1298+
assert_eq!(s.status, StreamStatus::Completed);
1299+
1300+
// Try to withdraw again — should return StreamInactive error.
1301+
assert_eq!(
1302+
client.try_withdraw(&recipient, &id),
1303+
Err(Ok(StreamError::StreamInactive))
1304+
);
1305+
}
1306+
12481307
#[test]
12491308
fn test_top_up_extends_stream() {
12501309
let env = Env::default();

frontend/eslint.config.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ const eslintConfig = defineConfig([
1313
"build/**",
1414
"next-env.d.ts",
1515
]),
16+
{
17+
// Forbid raw console.* calls outside the logger module.
18+
// Use src/lib/logger.ts instead.
19+
files: ["src/**/*.{ts,tsx}"],
20+
ignores: ["src/lib/logger.ts"],
21+
rules: {
22+
"no-console": "error",
23+
},
24+
},
1625
]);
1726

1827
export default eslintConfig;

frontend/src/app/activity/activity-content.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import { Button } from "@/components/ui/Button";
88
import { Loader2, Download } from "lucide-react";
99
import { formatAmount } from "@/lib/amount";
1010
import { downloadCSV } from "@/utils/csvExport";
11+
import { getApiBaseUrl } from "@/lib/api/_shared";
12+
import { logger } from "@/lib/logger";
1113

1214
const PAGE_SIZE = 10;
13-
const API_BASE_URL = (
14-
process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001"
15-
).replace(/\/+$/, "");
15+
const API_BASE_URL = getApiBaseUrl();
1616

1717
const TABS = [
1818
{ id: "ALL", label: "All" },
@@ -62,7 +62,7 @@ export default function ActivityContent() {
6262
}
6363
} catch (error) {
6464
if (error instanceof DOMException && error.name === "AbortError") return;
65-
console.error("Failed to fetch activity:", error);
65+
logger.error("Failed to fetch activity:", error);
6666
if (!append) setEvents([]);
6767
setHasMore(false);
6868
} finally {

frontend/src/app/settings/settings-content.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import { useRouter } from "next/navigation";
88
import Link from "next/link";
99
import { formatNetwork } from "@/lib/wallet";
1010
import toast from "react-hot-toast";
11+
import { getApiBaseUrl } from "@/lib/api/_shared";
1112

1213
type DisplayCurrency = "USD" | "XLM" | "USDC";
1314
type AmountFormat = "full" | "compact";
1415
type DecimalPlaces = 2 | 4 | 7;
1516

1617
const APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || "1.0.0";
1718
const CONTRACT_ADDRESS = process.env.NEXT_PUBLIC_STREAMING_CONTRACT || "CDV4K...7ZQY";
18-
const INDEXER_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/v1";
19+
const INDEXER_URL = `${getApiBaseUrl()}/v1`;
1920

2021
export default function SettingsContent() {
2122
const router = useRouter();

frontend/src/app/streams/[id]/stream-details-content.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import { useEffect, useState, useCallback, useMemo } from "react";
44
import Link from "next/link";
5+
import { getApiBaseUrl } from "@/lib/api/_shared";
6+
import { logger } from "@/lib/logger";
57
import { ArrowLeft, Pause, Play, X, Plus, Download, AlertTriangle } from "lucide-react";
68
import { Button } from "@/components/ui/Button";
79
import toast from "react-hot-toast";
@@ -42,7 +44,7 @@ interface StreamDetail {
4244
updatedAt: string;
4345
}
4446

45-
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/v1";
47+
const API_BASE_URL = `${getApiBaseUrl()}/v1`;
4648
const EVENTS_PER_PAGE = 10;
4749

4850
const TOKEN_SYMBOLS: Record<string, string> = {
@@ -116,7 +118,7 @@ export default function StreamDetailsContent({ streamId }: { streamId: string })
116118
}
117119
} catch (err) {
118120
if (err instanceof Error && err.name === "AbortError") return;
119-
console.error("Failed to fetch events:", err);
121+
logger.error("Failed to fetch events:", err);
120122
}
121123
}, [streamId]);
122124

frontend/src/app/streams/create/create-stream-content.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

33
import React, { useState } from "react";
4+
import { logger } from "@/lib/logger";
45
import {
56
createStream,
67
toBaseUnits,
@@ -67,7 +68,7 @@ export default function CreateStreamContent() {
6768
}, 2000);
6869
}
6970
} catch (error) {
70-
console.error("Stream creation failed:", error);
71+
logger.error("Stream creation failed:", error);
7172
toast.error(toSorobanErrorMessage(error));
7273
} finally {
7374
setLoading(false);

0 commit comments

Comments
 (0)