Skip to content

Commit 13b166f

Browse files
authored
Merge pull request #1 from LabsCrypt/main
merged
2 parents 5692bfc + 6f02d20 commit 13b166f

23 files changed

Lines changed: 2168 additions & 1495 deletions

.github/dependabot.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,18 @@ updates:
6565
update-types:
6666
- "minor"
6767
- "patch"
68+
69+
# ── Docker ─────────────────────────────────────────────────────────────────
70+
- package-ecosystem: "docker"
71+
directory: "/backend"
72+
schedule:
73+
interval: "weekly"
74+
day: "monday"
75+
open-pull-requests-limit: 5
76+
77+
- package-ecosystem: "docker"
78+
directory: "/"
79+
schedule:
80+
interval: "weekly"
81+
day: "monday"
82+
open-pull-requests-limit: 5

.github/workflows/ci.yml

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ jobs:
6262
runs-on: ubuntu-latest
6363
services:
6464
postgres:
65-
image: postgres:15
65+
image: postgres:16-alpine@sha256:e013e867e712fec275706a6c51c966f0bb0c93cfa8f51000f85a15f9865a28cb
6666
env:
6767
POSTGRES_USER: postgres
6868
POSTGRES_PASSWORD: password
@@ -125,6 +125,40 @@ jobs:
125125
env:
126126
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
127127

128+
backend-docker:
129+
name: Backend Docker Image CI
130+
runs-on: ubuntu-latest
131+
steps:
132+
- name: Checkout code
133+
uses: actions/checkout@v4
134+
135+
- name: Check changed files
136+
id: filter
137+
uses: dorny/paths-filter@v3
138+
with:
139+
filters: |
140+
backend:
141+
- 'backend/**'
142+
- 'docker-compose.yml'
143+
144+
- name: Build Docker Image
145+
if: steps.filter.outputs.backend == 'true' || github.event_name == 'push'
146+
run: docker build -f backend/Dockerfile backend -t flowfi-backend:test
147+
148+
- name: Docker Compose Build
149+
if: steps.filter.outputs.backend == 'true' || github.event_name == 'push'
150+
run: docker compose build
151+
152+
- name: Boot and Check Health
153+
if: steps.filter.outputs.backend == 'true' || github.event_name == 'push'
154+
run: |
155+
docker compose up -d postgres
156+
sleep 10
157+
docker compose run --rm -e DATABASE_URL=postgresql://flowfi:flowfi_dev_password@postgres:5432/flowfi backend npx -y prisma db push --accept-data-loss
158+
docker compose up -d backend
159+
sleep 15
160+
curl --fail http://localhost:3001/health || (docker compose logs backend && exit 1)
161+
128162
contracts:
129163
name: Soroban Contracts CI
130164
runs-on: ubuntu-latest

backend/Dockerfile

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
1-
FROM node:20-alpine AS builder
1+
FROM node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 AS builder
22

33
WORKDIR /app
44

55
COPY package*.json ./
6-
RUN npm ci
6+
RUN npm install
77

88
COPY tsconfig.json ./
99
COPY src ./src
10+
COPY prisma ./prisma
1011

1112
RUN npm run build
1213

13-
FROM node:20-alpine AS runner
14+
FROM node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 AS runner
1415

1516
WORKDIR /app
1617

1718
ENV NODE_ENV=production
1819

1920
COPY package*.json ./
20-
RUN npm ci --only=production
21+
RUN npm install --omit=dev
2122

2223
COPY --from=builder /app/dist ./dist
2324
COPY --from=builder /app/src/generated ./src/generated

docker-compose.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
services:
22
postgres:
3-
image: postgres:16-alpine
3+
image: postgres:16-alpine@sha256:e013e867e712fec275706a6c51c966f0bb0c93cfa8f51000f85a15f9865a28cb
44
container_name: flowfi-postgres
55
environment:
66
POSTGRES_USER: flowfi
@@ -34,6 +34,11 @@ services:
3434
depends_on:
3535
postgres:
3636
condition: service_healthy
37+
healthcheck:
38+
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3001/health"]
39+
interval: 10s
40+
timeout: 5s
41+
retries: 5
3742
restart: unless-stopped
3843

3944
volumes:
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
"use client";
2+
3+
import { useState, useEffect, useCallback } from "react";
4+
import { useWallet } from "@/context/wallet-context";
5+
import { ActivityHistory } from "@/components/dashboard/ActivityHistory";
6+
import { BackendStreamEvent } from "@/lib/api-types";
7+
import { Button } from "@/components/ui/Button";
8+
import { Loader2, Download } from "lucide-react";
9+
import { formatAmount } from "@/lib/amount";
10+
import { downloadCSV } from "@/utils/csvExport";
11+
12+
const PAGE_SIZE = 10;
13+
const API_BASE_URL = (
14+
process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001"
15+
).replace(/\/+$/, "");
16+
17+
const TABS = [
18+
{ id: "ALL", label: "All" },
19+
{ id: "CREATED", label: "Created" },
20+
{ id: "WITHDRAWN", label: "Withdrawals" },
21+
{ id: "TOPPED_UP", label: "Top-ups" },
22+
{ id: "CANCELLED", label: "Cancellations" },
23+
{ id: "PAUSED", label: "Paused/Resumed" },
24+
];
25+
26+
export default function ActivityContent() {
27+
const { session, status } = useWallet();
28+
const [events, setEvents] = useState<BackendStreamEvent[]>([]);
29+
const [activeTab, setActiveTab] = useState("ALL");
30+
const [loading, setLoading] = useState(false);
31+
const [page, setPage] = useState(1);
32+
const [hasMore, setHasMore] = useState(true);
33+
34+
const fetchActivity = useCallback(
35+
async (pageNum: number, tab: string, append: boolean = false, signal?: AbortSignal) => {
36+
if (!session?.publicKey) return;
37+
setLoading(true);
38+
39+
try {
40+
const typeFilter = tab === "PAUSED" ? "PAUSED,RESUMED" : tab;
41+
const typeQuery = tab === "ALL" ? "" : `&type=${encodeURIComponent(typeFilter)}`;
42+
const url =
43+
`${API_BASE_URL}/v1/events?address=${encodeURIComponent(session.publicKey)}` +
44+
`&page=${pageNum}&limit=${PAGE_SIZE}${typeQuery}`;
45+
46+
const response = await fetch(url, { signal });
47+
if (!response.ok) {
48+
throw new Error(`Failed to fetch activity (${response.status})`);
49+
}
50+
const data = await response.json();
51+
52+
const next: BackendStreamEvent[] = Array.isArray(data?.events)
53+
? data.events
54+
: [];
55+
56+
setEvents((prev) => (append ? [...prev, ...next] : next));
57+
58+
if (typeof data?.hasMore === "boolean") {
59+
setHasMore(data.hasMore);
60+
} else {
61+
setHasMore(next.length === PAGE_SIZE);
62+
}
63+
} catch (error) {
64+
if (error instanceof DOMException && error.name === "AbortError") return;
65+
console.error("Failed to fetch activity:", error);
66+
if (!append) setEvents([]);
67+
setHasMore(false);
68+
} finally {
69+
setLoading(false);
70+
}
71+
},
72+
[session],
73+
);
74+
75+
useEffect(() => {
76+
if (status !== "connected") return;
77+
const controller = new AbortController();
78+
const initLoad = async () => {
79+
await fetchActivity(1, activeTab, false, controller.signal);
80+
};
81+
void initLoad();
82+
return () => controller.abort();
83+
}, [activeTab, status, fetchActivity]);
84+
85+
const loadMore = () => {
86+
const nextPage = page + 1;
87+
setPage(nextPage);
88+
fetchActivity(nextPage, activeTab, true);
89+
};
90+
91+
const handleExportCSV = () => {
92+
const csvData = events.map(event => ({
93+
'Stream ID': event.streamId,
94+
'Event Type': event.eventType,
95+
'Amount': event.amount ? formatAmount(BigInt(event.amount), 7) : '0',
96+
'Timestamp': new Date(event.timestamp * 1000).toLocaleString(),
97+
'Transaction Hash': event.transactionHash,
98+
'Ledger': event.ledgerSequence,
99+
}));
100+
downloadCSV(csvData, `flowfi-activity-${Date.now()}.csv`);
101+
};
102+
103+
if (status !== "connected") {
104+
return (
105+
<div className="flex flex-col items-center justify-center min-h-[60vh] text-center">
106+
<h1 className="text-2xl font-bold mb-2 text-white">Access Denied</h1>
107+
<p className="text-slate-400">
108+
Please connect your wallet to view your stream history.
109+
</p>
110+
</div>
111+
);
112+
}
113+
114+
return (
115+
<main className="max-w-4xl mx-auto py-8 px-4 sm:px-6">
116+
<header className="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
117+
<div>
118+
<h1 className="text-3xl font-bold text-white mb-2">Stream Activity</h1>
119+
<p className="text-slate-400">
120+
Track all your incoming and outgoing payment stream events.
121+
</p>
122+
</div>
123+
<Button
124+
variant="secondary"
125+
size="sm"
126+
onClick={handleExportCSV}
127+
disabled={events.length === 0}
128+
className="sm:w-auto w-full"
129+
>
130+
<Download className="h-4 w-4 mr-2" />
131+
Export CSV
132+
</Button>
133+
</header>
134+
135+
{/* Tabs */}
136+
<div className="flex gap-2 overflow-x-auto pb-4 mb-6 no-scrollbar">
137+
{TABS.map((tab) => (
138+
<button
139+
key={tab.id}
140+
onClick={() => {
141+
setActiveTab(tab.id);
142+
setPage(1);
143+
}}
144+
className={`px-4 py-2 rounded-full text-sm font-medium transition-all whitespace-nowrap border ${
145+
activeTab === tab.id
146+
? "bg-accent text-white border-accent"
147+
: "bg-white/5 text-slate-400 border-white/10 hover:bg-white/10"
148+
}`}
149+
>
150+
{tab.label}
151+
</button>
152+
))}
153+
</div>
154+
155+
<ActivityHistory events={events} isLoading={loading && events.length === 0} />
156+
157+
{hasMore && (
158+
<div className="mt-12 flex justify-center">
159+
<Button
160+
variant="secondary"
161+
onClick={loadMore}
162+
disabled={loading}
163+
className="w-full sm:w-auto min-w-[200px]"
164+
>
165+
{loading ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : null}
166+
Load More Activity
167+
</Button>
168+
</div>
169+
)}
170+
</main>
171+
);
172+
}

0 commit comments

Comments
 (0)