From 307e8f6f795623a994e09441dcf3c7657c68c4cb Mon Sep 17 00:00:00 2001
From: Greedy_Biggie <99267251+Austinaminu2@users.noreply.github.com>
Date: Wed, 8 Jul 2026 09:24:55 +0000
Subject: [PATCH 1/2] fix: repair broken CI and clean up merge corruption
across the app
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
CI has been red on main: the test build failed on a duplicate formatUsd
export, eslint-plugin-security was incompatible with ESLint 10 (breaking
lint and the pre-commit hook for every contributor), and Next 16's
Turbopack default conflicts with Sentry's webpack config.
Several files also had unresolved merge conflicts silently merged in —
duplicate declarations, dead code after early returns, and in two cases
an entire abandoned feature branch pasted on top of the real
(already-split) page implementation. Restored the intended
page.tsx/dashboard.tsx and page.tsx/create-form.tsx server/client
splits, finished the create-form.tsx split that had been left as a
placeholder stub, and removed five orphaned files that never compiled
and were never imported anywhere.
tsc --noEmit was fully masked by `ignoreBuildErrors: true`; removing it
turned up real bugs now fixed: Sentry's beforeSend was silently
crashing (targeted the pre-v8 breadcrumb shape, so wallet addresses
were never actually being redacted), fee estimation read a `.cost`
field the current SDK type doesn't have, and a stream card had a
nested inside a
causing a hydration warning. Removing
ignoreBuildErrors also means the build now fails loudly on this class
of bug instead of merging silently.
Co-Authored-By: Claude Sonnet 5
---
.../components/connect-wallet-button.test.tsx | 189 ++-
app/app/analytics/page.tsx | 244 +--
app/app/create/create-form.tsx | 1328 ++++++++++++++++-
app/app/create/page.tsx | 1109 +-------------
app/app/page.tsx | 487 +-----
app/app/stream/[id]/layout.tsx | 11 +
app/app/stream/[id]/metadata.ts | 66 +-
app/app/stream/[id]/page.tsx | 678 +++++----
components/CommandPalette.tsx | 234 ---
components/NetworkMismatchModal.tsx | 102 --
components/layout/testnet-faucet-banner.tsx | 133 +-
components/providers/wallet-provider.tsx | 152 --
components/streams/dashboard-stats.tsx | 108 +-
components/streams/stream-card.tsx | 131 +-
e2e/visual.spec.ts | 383 +++--
hooks/use-auto-withdraw.ts | 206 +--
hooks/use-contract.ts | 192 +--
hooks/use-stream-polling.ts | 270 ----
hooks/use-token-price.ts | 170 +--
hooks/useNetworkGuard.ts | 36 -
lib/airdrop.ts | 221 ---
lib/contract.ts | 691 +++++----
lib/stream-utils.ts | 202 +--
next.config.mjs | 6 +-
package-lock.json | 420 +++---
package.json | 14 +-
sentry.client.config.ts | 25 +-
src/lib/network.ts | 18 -
28 files changed, 3529 insertions(+), 4297 deletions(-)
create mode 100644 app/app/stream/[id]/layout.tsx
delete mode 100644 components/CommandPalette.tsx
delete mode 100644 components/NetworkMismatchModal.tsx
delete mode 100644 hooks/use-stream-polling.ts
delete mode 100644 hooks/useNetworkGuard.ts
delete mode 100644 lib/airdrop.ts
delete mode 100644 src/lib/network.ts
diff --git a/__tests__/components/connect-wallet-button.test.tsx b/__tests__/components/connect-wallet-button.test.tsx
index f7de6f1..51ab99d 100644
--- a/__tests__/components/connect-wallet-button.test.tsx
+++ b/__tests__/components/connect-wallet-button.test.tsx
@@ -1,28 +1,32 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest'
-import { render, screen, fireEvent, waitFor } from '@testing-library/react'
-import { ConnectWalletButton } from '@/components/layout/connect-wallet-button'
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { render, screen, fireEvent, waitFor } from "@testing-library/react";
+import { ConnectWalletButton } from "@/components/layout/connect-wallet-button";
const mockUseWallet = vi.hoisted(() =>
vi.fn(() => ({
- address: null,
+ address: null as string | null,
isConnected: false,
connecting: false,
connect: vi.fn(),
disconnect: vi.fn(),
- walletId: null,
- }))
-)
+ walletId: null as string | null,
+ })),
+);
-vi.mock('@/hooks/use-wallet', () => ({ useWallet: mockUseWallet }))
+vi.mock("@/hooks/use-wallet", () => ({ useWallet: mockUseWallet }));
-vi.mock('@/components/providers/wallet-provider', () => ({
+vi.mock("@/components/providers/wallet-provider", () => ({
WALLET_OPTIONS: [
- { id: 'freighter', name: 'Freighter', detail: 'Browser extension · stellar.org' },
- { id: 'xbull', name: 'xBull', detail: 'Extension & web' },
+ {
+ id: "freighter",
+ name: "Freighter",
+ detail: "Browser extension · stellar.org",
+ },
+ { id: "xbull", name: "xBull", detail: "Extension & web" },
],
-}))
+}));
-describe('ConnectWalletButton — disconnected', () => {
+describe("ConnectWalletButton — disconnected", () => {
beforeEach(() => {
mockUseWallet.mockReturnValue({
address: null,
@@ -31,56 +35,68 @@ describe('ConnectWalletButton — disconnected', () => {
connect: vi.fn(),
disconnect: vi.fn(),
walletId: null,
- })
- })
+ });
+ });
it('renders "Connect wallet" button', () => {
- render()
- expect(screen.getByRole('button', { name: /connect wallet/i })).toBeInTheDocument()
- })
+ render();
+ expect(
+ screen.getByRole("button", { name: /connect wallet/i }),
+ ).toBeInTheDocument();
+ });
- it('opens wallet selection dialog on click', async () => {
- render()
- fireEvent.click(screen.getByRole('button', { name: /connect wallet/i }))
+ it("opens wallet selection dialog on click", async () => {
+ render();
+ fireEvent.click(screen.getByRole("button", { name: /connect wallet/i }));
await waitFor(() =>
- expect(screen.getByText(/choose a stellar wallet/i)).toBeInTheDocument()
- )
- })
+ expect(screen.getByText(/choose a stellar wallet/i)).toBeInTheDocument(),
+ );
+ });
- it('lists wallet options in dialog', async () => {
- render()
- fireEvent.click(screen.getByRole('button', { name: /connect wallet/i }))
+ it("lists wallet options in dialog", async () => {
+ render();
+ fireEvent.click(screen.getByRole("button", { name: /connect wallet/i }));
await waitFor(() => {
- expect(screen.getByText('Freighter')).toBeInTheDocument()
- expect(screen.getByText('xBull')).toBeInTheDocument()
- })
- })
+ expect(screen.getByText("Freighter")).toBeInTheDocument();
+ expect(screen.getByText("xBull")).toBeInTheDocument();
+ });
+ });
- it('calls connect with wallet id when option is clicked', async () => {
- const connect = vi.fn()
+ it("calls connect with wallet id when option is clicked", async () => {
+ const connect = vi.fn();
mockUseWallet.mockReturnValue({
- address: null, isConnected: false, connecting: false, connect, disconnect: vi.fn(), walletId: null,
- })
- render()
- fireEvent.click(screen.getByRole('button', { name: /connect wallet/i }))
- await waitFor(() => screen.getByText('Freighter'))
- fireEvent.click(screen.getByText('Freighter').closest('button')!)
- expect(connect).toHaveBeenCalledWith('freighter')
- })
+ address: null,
+ isConnected: false,
+ connecting: false,
+ connect,
+ disconnect: vi.fn(),
+ walletId: null,
+ });
+ render();
+ fireEvent.click(screen.getByRole("button", { name: /connect wallet/i }));
+ await waitFor(() => screen.getByText("Freighter"));
+ fireEvent.click(screen.getByText("Freighter").closest("button")!);
+ await waitFor(() => expect(connect).toHaveBeenCalledWith("freighter"));
+ });
- it('disables wallet buttons while connecting', async () => {
+ it("disables wallet buttons while connecting", async () => {
mockUseWallet.mockReturnValue({
- address: null, isConnected: false, connecting: true, connect: vi.fn(), disconnect: vi.fn(), walletId: null,
- })
- render()
- fireEvent.click(screen.getByRole('button', { name: /connect wallet/i }))
- await waitFor(() => screen.getByText('Freighter'))
- expect(screen.getByText('Freighter').closest('button')).toBeDisabled()
- })
-})
+ address: null,
+ isConnected: false,
+ connecting: true,
+ connect: vi.fn(),
+ disconnect: vi.fn(),
+ walletId: null,
+ });
+ render();
+ fireEvent.click(screen.getByRole("button", { name: /connect wallet/i }));
+ await waitFor(() => screen.getByText("Freighter"));
+ expect(screen.getByText("Freighter").closest("button")).toBeDisabled();
+ });
+});
-describe('ConnectWalletButton — connected', () => {
- const ADDR = 'GABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AB'
+describe("ConnectWalletButton — connected", () => {
+ const ADDR = "GABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890AB";
beforeEach(() => {
mockUseWallet.mockReturnValue({
@@ -89,38 +105,49 @@ describe('ConnectWalletButton — connected', () => {
connecting: false,
connect: vi.fn(),
disconnect: vi.fn(),
- walletId: 'freighter',
- })
- })
+ walletId: "freighter",
+ });
+ });
- it('renders shortened address instead of connect button', () => {
- render()
- expect(screen.queryByRole('button', { name: /connect wallet/i })).not.toBeInTheDocument()
+ it("renders shortened address instead of connect button", () => {
+ render();
+ expect(
+ screen.queryByRole("button", { name: /connect wallet/i }),
+ ).not.toBeInTheDocument();
// Shortened address is shown inside the dropdown trigger button
- expect(screen.getByRole('button')).toBeInTheDocument()
- })
+ expect(screen.getByRole("button")).toBeInTheDocument();
+ });
- it('shows disconnect option in dropdown', async () => {
- render()
- fireEvent.click(screen.getByRole('button'))
- await waitFor(() => expect(screen.getByText(/disconnect/i)).toBeInTheDocument())
- })
+ it("shows disconnect option in dropdown", async () => {
+ render();
+ fireEvent.click(screen.getByRole("button"));
+ await waitFor(() =>
+ expect(screen.getByText(/disconnect/i)).toBeInTheDocument(),
+ );
+ });
- it('calls disconnect when disconnect is clicked', async () => {
- const disconnect = vi.fn()
+ it("calls disconnect when disconnect is clicked", async () => {
+ const disconnect = vi.fn();
mockUseWallet.mockReturnValue({
- address: ADDR, isConnected: true, connecting: false, connect: vi.fn(), disconnect, walletId: 'freighter',
- })
- render()
- fireEvent.click(screen.getByRole('button'))
- await waitFor(() => screen.getByText(/disconnect/i))
- fireEvent.click(screen.getByText(/disconnect/i))
- expect(disconnect).toHaveBeenCalled()
- })
+ address: ADDR,
+ isConnected: true,
+ connecting: false,
+ connect: vi.fn(),
+ disconnect,
+ walletId: "freighter",
+ });
+ render();
+ fireEvent.click(screen.getByRole("button"));
+ await waitFor(() => screen.getByText(/disconnect/i));
+ fireEvent.click(screen.getByText(/disconnect/i));
+ expect(disconnect).toHaveBeenCalled();
+ });
- it('shows copy address option', async () => {
- render()
- fireEvent.click(screen.getByRole('button'))
- await waitFor(() => expect(screen.getByText(/copy address/i)).toBeInTheDocument())
- })
-})
+ it("shows copy address option", async () => {
+ render();
+ fireEvent.click(screen.getByRole("button"));
+ await waitFor(() =>
+ expect(screen.getByText(/copy address/i)).toBeInTheDocument(),
+ );
+ });
+});
diff --git a/app/app/analytics/page.tsx b/app/app/analytics/page.tsx
index 096dd5e..a5d2142 100644
--- a/app/app/analytics/page.tsx
+++ b/app/app/analytics/page.tsx
@@ -1,25 +1,44 @@
-'use client'
-
-import { useEffect, useMemo, useState, Suspense } from 'react'
-import dynamic from 'next/dynamic'
-import Link from 'next/link'
-import { ArrowLeft, BarChart3, Clock3, TrendingUp, Wallet2 } from 'lucide-react'
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
-import { Badge } from '@/components/ui/badge'
-import { SectionErrorBoundary } from '@/components/error-boundary/section-error-boundary'
-import { useStreams } from '@/hooks/use-streams'
-import { useNetwork } from '@/components/providers/network-provider'
-import { getAllTokens } from '@/lib/stellar'
-import type { StreamData } from '@/types/stream'
+"use client";
+
+import { useEffect, useMemo, useState, Suspense } from "react";
+import dynamic from "next/dynamic";
+import Link from "next/link";
+import {
+ ArrowLeft,
+ BarChart3,
+ Clock3,
+ TrendingUp,
+ Wallet2,
+} from "lucide-react";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Badge } from "@/components/ui/badge";
+import { SectionErrorBoundary } from "@/components/error-boundary/section-error-boundary";
+import { useStreams } from "@/hooks/use-streams";
+import { useNetwork } from "@/components/providers/network-provider";
+import { getAllTokens } from "@/lib/stellar";
+import { formatCompactAmount } from "@/lib/stream-utils";
+import type { StreamData } from "@/types/stream";
const AnalyticsCharts = dynamic(
- () => import('@/components/analytics/charts').then((m) => m.AnalyticsCharts),
+ () => import("@/components/analytics/charts").then((m) => m.AnalyticsCharts),
{
loading: () => ,
ssr: false,
},
-)
+);
function ChartSkeleton() {
return (
@@ -33,64 +52,96 @@ function ChartSkeleton() {
- )
+ );
}
interface AnalyticsSnapshot {
- totalVolume: bigint
- activeCount: number
- totalStreams: number
- averageDurationDays: number
- tokenShares: Array<{ symbol: string; amount: bigint; count: number }>
- series: Array<{ label: string; count: number }>
- topTokens: Array<{ symbol: string; amount: bigint; count: number }>
+ totalVolume: bigint;
+ activeCount: number;
+ totalStreams: number;
+ averageDurationDays: number;
+ tokenShares: Array<{ symbol: string; amount: bigint; count: number }>;
+ series: Array<{ label: string; count: number }>;
+ topTokens: Array<{ symbol: string; amount: bigint; count: number }>;
}
const RANGE_OPTIONS = [
- { value: '7d', label: '7 days' },
- { value: '30d', label: '30 days' },
- { value: '90d', label: '90 days' },
- { value: 'all', label: 'All time' },
-] as const
-
-function buildSnapshot(streams: StreamData[], range: string): AnalyticsSnapshot {
- const now = Math.floor(Date.now() / 1000)
- const cutoff = range === 'all' ? 0 : Date.now() - Number.parseInt(range.replace('d', ''), 10) * 24 * 60 * 60 * 1000
- const filtered = streams.filter((stream) => Number(stream.startTime) * 1000 >= cutoff || range === 'all')
-
- const totalVolume = filtered.reduce((sum, stream) => sum + stream.depositedAmount, 0n)
- const activeCount = filtered.filter((stream) => !stream.cancelled && Number(stream.endTime) > now).length
- const totalStreams = filtered.length
- const averageDurationDays = filtered.length > 0
- ? filtered.reduce((sum, stream) => sum + Number(stream.endTime - stream.startTime) / 86400, 0) / filtered.length
- : 0
-
- const tokenGroups = new Map