diff --git a/.lintstagedrc b/.lintstagedrc
index 9f270bb..3f4a7aa 100644
--- a/.lintstagedrc
+++ b/.lintstagedrc
@@ -1,5 +1,5 @@
{
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md,css}": ["prettier --write"],
- "*.rs": ["cargo fmt --check"]
+ "*.rs": ["rustfmt --check"]
}
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()
+ { 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<
+ string,
+ { amount: bigint; count: number; decimals: number }
+ >();
filtered.forEach((stream) => {
- const key = stream.token.symbol
- const entry = tokenGroups.get(key) ?? { amount: 0n, count: 0, decimals: stream.token.decimals }
- entry.amount += stream.depositedAmount
- entry.count += 1
- tokenGroups.set(key, entry)
- })
-
- const tokenShares = Array.from(tokenGroups.entries()).map(([symbol, entry]) => ({
- symbol,
- amount: entry.amount,
- count: entry.count,
- }))
-
- const seriesMap = new Map()
+ const key = stream.token.symbol;
+ const entry = tokenGroups.get(key) ?? {
+ amount: 0n,
+ count: 0,
+ decimals: stream.token.decimals,
+ };
+ entry.amount += stream.depositedAmount;
+ entry.count += 1;
+ tokenGroups.set(key, entry);
+ });
+
+ const tokenShares = Array.from(tokenGroups.entries()).map(
+ ([symbol, entry]) => ({
+ symbol,
+ amount: entry.amount,
+ count: entry.count,
+ }),
+ );
+
+ const seriesMap = new Map();
filtered.forEach((stream) => {
- const day = new Date(Number(stream.startTime) * 1000).toISOString().slice(0, 10)
- seriesMap.set(day, (seriesMap.get(day) ?? 0) + 1)
- })
+ const day = new Date(Number(stream.startTime) * 1000)
+ .toISOString()
+ .slice(0, 10);
+ seriesMap.set(day, (seriesMap.get(day) ?? 0) + 1);
+ });
const series = Array.from(seriesMap.entries())
.sort(([a], [b]) => a.localeCompare(b))
- .map(([label, count]) => ({ label, count }))
+ .map(([label, count]) => ({ label, count }));
- const topTokens = [...tokenShares].sort((a, b) => Number(b.amount - a.amount)).slice(0, 4)
+ const topTokens = [...tokenShares]
+ .sort((a, b) => Number(b.amount - a.amount))
+ .slice(0, 4);
return {
totalVolume,
@@ -100,36 +151,46 @@ function buildSnapshot(streams: StreamData[], range: string): AnalyticsSnapshot
tokenShares,
series,
topTokens,
- }
+ };
}
export default function AnalyticsPage() {
- const { all } = useStreams({ enablePolling: false })
- const { network } = useNetwork()
- const [range, setRange] = useState('30d')
- const [mounted, setMounted] = useState(false)
+ const { all } = useStreams({ enablePolling: false });
+ const { network } = useNetwork();
+ const [range, setRange] = useState("30d");
+ const [mounted, setMounted] = useState(false);
- useEffect(() => setMounted(true), [])
+ useEffect(() => setMounted(true), []);
- const snapshot = useMemo(() => buildSnapshot(all, range), [all, range])
+ const snapshot = useMemo(() => buildSnapshot(all, range), [all, range]);
- const tokens = useMemo(() => getAllTokens(network), [network])
+ const tokens = useMemo(() => getAllTokens(network), [network]);
- if (!mounted) return null
+ if (!mounted) return null;
return (
-
+
Back to dashboard
-
Platform analytics
-
Public signals that highlight traction, usage, and stream growth.
+
+ Platform analytics
+
+
+ Public signals that highlight traction, usage, and stream growth.
+
-
+ v !== null && setRange(v)}
+ >
@@ -148,7 +209,11 @@ export default function AnalyticsPage() {
Total volume streamed
- {snapshot.totalVolume > 0n ? formatCompactAmount(snapshot.totalVolume, 7) : '0'}
+
+ {snapshot.totalVolume > 0n
+ ? formatCompactAmount(snapshot.totalVolume, 7)
+ : "0"}
+
Across the visible stream history
@@ -157,7 +222,9 @@ export default function AnalyticsPage() {
Active streams
- {snapshot.activeCount}
+
+ {snapshot.activeCount}
+
Currently streaming now
@@ -166,7 +233,9 @@ export default function AnalyticsPage() {
Total streams created
- {snapshot.totalStreams}
+
+ {snapshot.totalStreams}
+
All-time stream count
@@ -175,7 +244,9 @@ export default function AnalyticsPage() {
Average duration
- {snapshot.averageDurationDays.toFixed(1)}d
+
+ {snapshot.averageDurationDays.toFixed(1)}d
+
Average stream length
@@ -195,17 +266,24 @@ export default function AnalyticsPage() {
Network context
- Current public view and available tokens.
+
+ Current public view and available tokens.
+
- This dashboard is built from the current app data and will be backed by on-chain aggregation once a public index is available.
+
+ This dashboard is built from the current app data and will be backed
+ by on-chain aggregation once a public index is available.
+
{tokens.map((token) => (
- {token.symbol}
+
+ {token.symbol}
+
))}
- )
+ );
}
diff --git a/app/app/create/create-form.tsx b/app/app/create/create-form.tsx
index 118a4cb..9ddb611 100644
--- a/app/app/create/create-form.tsx
+++ b/app/app/create/create-form.tsx
@@ -1,42 +1,1226 @@
-'use client'
-
-import { useState, useCallback, useEffect, useRef } from 'react'
-import { useRouter, useSearchParams } from 'next/navigation'
-import { AlertTriangle, ArrowLeft, ArrowRight, Info, Loader2, Copy, Clock } from 'lucide-react'
-import Link from 'next/link'
-import { toast } from 'sonner'
-import { StrKey } from '@stellar/stellar-sdk'
-import { RequireWallet } from '@/components/layout/require-wallet'
-import { Button } from '@/components/ui/button'
-import { Input } from '@/components/ui/input'
-import { Label } from '@/components/ui/label'
+"use client";
+
+import { useState, useCallback, useEffect, useRef } from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import {
+ AlertTriangle,
+ ArrowLeft,
+ ArrowRight,
+ Info,
+ Loader2,
+ Copy,
+ Clock,
+} from "lucide-react";
+import Link from "next/link";
+import { toast } from "sonner";
+import { StrKey } from "@stellar/stellar-sdk";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
-} from '@/components/ui/select'
-import { useContract } from '@/hooks/use-contract'
-import { useWallet } from '@/hooks/use-wallet'
-import { getAllTokens, saveCustomToken, checkAccountInfo } from '@/lib/stellar'
-import { getTokenMetadata, getTokenBalance } from '@/lib/contract'
-import { parseTokenAmount, formatTokenAmount } from '@/lib/stream-utils'
-import { StreamPreview } from '@/components/streams/stream-preview'
-import { CreateConfirmation } from '@/components/streams/create-confirmation'
-import { TxPreviewDialog } from '@/components/ui/tx-preview-dialog'
-import { addAddressBookEntry, getAddressBookEntries, touchAddressBookEntry } from '@/lib/address-book'
-import { buildNextRunAt, saveRecurringRule, type RecurrenceCadence } from '@/lib/recurring'
-import { useFormDraft, clearExpiredDrafts } from '@/hooks/use-form-draft'
-import { StreamTemplates, type StreamTemplate } from '@/components/streams/stream-templates'
-import type { TokenInfo } from '@/types/stream'
-
-// ... rest of the create form code from page.tsx
-// This is a placeholder - the actual component is too large to copy here
-// In a real implementation, you would move the entire CreateForm component here
+} from "@/components/ui/select";
+import { useContract } from "@/hooks/use-contract";
+import { useWallet } from "@/hooks/use-wallet";
+import { getAllTokens, saveCustomToken, checkAccountInfo } from "@/lib/stellar";
+import { getTokenMetadata, getTokenBalance } from "@/lib/contract";
+import { parseTokenAmount, formatTokenAmount } from "@/lib/stream-utils";
+import { StreamPreview } from "@/components/streams/stream-preview";
+import { CreateConfirmation } from "@/components/streams/create-confirmation";
+import { TxPreviewDialog } from "@/components/ui/tx-preview-dialog";
+import {
+ addAddressBookEntry,
+ getAddressBookEntries,
+ touchAddressBookEntry,
+} from "@/lib/address-book";
+import {
+ buildNextRunAt,
+ saveRecurringRule,
+ type RecurrenceCadence,
+} from "@/lib/recurring";
+import { useFormDraft, clearExpiredDrafts } from "@/hooks/use-form-draft";
+import {
+ StreamTemplates,
+ type StreamTemplate,
+} from "@/components/streams/stream-templates";
+import { useTokenPrice } from "@/hooks/use-token-price";
+import type { TokenInfo } from "@/types/stream";
+import { useNetwork } from "@/components/providers/network-provider";
+
+const CUSTOM_VALUE = "__custom__";
+
+function toUnixSeconds(localDatetimeValue: string): bigint {
+ return BigInt(Math.floor(new Date(localDatetimeValue).getTime() / 1000));
+}
+
+function localDatetimeMin(offsetSeconds = 0): string {
+ const d = new Date(Date.now() + offsetSeconds * 1000);
+ // Format as YYYY-MM-DDTHH:mm in local time (not UTC)
+ const pad = (n: number) => String(n).padStart(2, "0");
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
+}
+
+function addDuration(baseDatetime: string, seconds: number): string {
+ const base = new Date(baseDatetime);
+ const d = new Date(base.getTime() + seconds * 1000);
+ const pad = (n: number) => String(n).padStart(2, "0");
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
+}
+
+function detectTimezone(): string {
+ try {
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
+ } catch {
+ return "UTC";
+ }
+}
+
+function getTimezoneOffset(): string {
+ const offset = -new Date().getTimezoneOffset();
+ const sign = offset >= 0 ? "+" : "-";
+ const h = Math.floor(Math.abs(offset) / 60);
+ const m = Math.abs(offset) % 60;
+ return `UTC${sign}${h}${m > 0 ? `:${String(m).padStart(2, "0")}` : ""}`;
+}
+
+const COMMON_TIMEZONES = [
+ "UTC",
+ "America/New_York",
+ "America/Chicago",
+ "America/Denver",
+ "America/Los_Angeles",
+ "America/Sao_Paulo",
+ "Europe/London",
+ "Europe/Paris",
+ "Europe/Berlin",
+ "Asia/Dubai",
+ "Asia/Kolkata",
+ "Asia/Singapore",
+ "Asia/Tokyo",
+ "Australia/Sydney",
+] as const;
+
+const DURATION_PRESETS = [
+ { label: "1 week", seconds: 7 * 24 * 3600 },
+ { label: "1 month", seconds: 30 * 24 * 3600 },
+ { label: "3 months", seconds: 90 * 24 * 3600 },
+ { label: "6 months", seconds: 180 * 24 * 3600 },
+ { label: "1 year", seconds: 365 * 24 * 3600 },
+] as const;
+
+const CLIFF_PRESETS = [
+ { label: "No cliff", seconds: 0 },
+ { label: "1 month", seconds: 30 * 24 * 3600 },
+ { label: "3 months", seconds: 90 * 24 * 3600 },
+] as const;
+
+interface FormState {
+ recipient: string;
+ tokenAddress: string;
+ amount: string;
+ startDate: string;
+ endDate: string;
+ hasCliff: boolean;
+ cliffDate: string;
+ cliffAmount: string;
+}
export function CreateForm() {
- // This would contain all the CreateForm logic from the original page.tsx
- // For now, returning a placeholder to avoid breaking the build
- return
Create Form Component
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const cloneId = searchParams.get("clone");
+ const { address: walletAddress } = useWallet();
+ const { network } = useNetwork();
+ const { createStream, estimateFee, pending, error } = useContract();
+ const [feeEstimate, setFeeEstimate] = useState
(null);
+ const [estimatingFee, setEstimatingFee] = useState(false);
+ const [showTxPreview, setShowTxPreview] = useState(false);
+ const [showConfirmation, setShowConfirmation] = useState(false);
+
+ const [tokens, setTokens] = useState(() =>
+ getAllTokens(network).map((t) => ({ ...t })),
+ );
+ const [isCustom, setIsCustom] = useState(false);
+ const [customAddress, setCustomAddress] = useState("");
+ const [customLoading, setCustomLoading] = useState(false);
+ const [customError, setCustomError] = useState(null);
+ const [customToken, setCustomToken] = useState(null);
+ const [addressBookEntries, setAddressBookEntries] = useState(() =>
+ getAddressBookEntries(),
+ );
+ const [recurrenceCadence, setRecurrenceCadence] =
+ useState("none");
+
+ // Issue #29: balance state
+ const [tokenBalance, setTokenBalance] = useState(null);
+ const [balanceLoading, setBalanceLoading] = useState(false);
+
+ // Issue #103: recipient account validation
+ const [recipientAccountInfo, setRecipientAccountInfo] = useState<{
+ exists: boolean;
+ funded: boolean;
+ transactionCount: number;
+ } | null>(null);
+ const [recipientChecking, setRecipientChecking] = useState(false);
+ const [recipientWarningAcknowledged, setRecipientWarningAcknowledged] =
+ useState(false);
+
+ const defaultStart = localDatetimeMin(60);
+ const defaultEnd = localDatetimeMin(60 + 30 * 24 * 3600);
+
+ const [form, setForm] = useState(() => {
+ const newStart = localDatetimeMin(60);
+ const durationSecs = searchParams.get("duration");
+ const cliffSecs = searchParams.get("cliff");
+ const hasCliff = cliffSecs !== null && cliffSecs !== "0";
+ const newEnd = durationSecs
+ ? addDuration(newStart, Number(durationSecs))
+ : localDatetimeMin(60 + 30 * 24 * 3600);
+ const newCliff =
+ hasCliff && cliffSecs
+ ? addDuration(newStart, Number(cliffSecs))
+ : newStart;
+
+ return {
+ recipient: searchParams.get("recipient") ?? "",
+ tokenAddress: searchParams.get("token") ?? tokens[0]?.address ?? "",
+ amount: searchParams.get("amount") ?? "",
+ startDate: newStart,
+ endDate: newEnd,
+ hasCliff,
+ cliffDate: newCliff,
+ cliffAmount: searchParams.get("cliffAmount") ?? "",
+ };
+ });
+
+ const [errors, setErrors] = useState<
+ Partial>
+ >({});
+ const [selectedTemplateId, setSelectedTemplateId] = useState<
+ string | undefined
+ >(undefined);
+
+ // Issue #168: draft state
+ const [showDraftBanner, setShowDraftBanner] = useState(false);
+ const [draftSavedAt, setDraftSavedAt] = useState(null);
+ const isFirstMount = useRef(true);
+
+ // Issue #170: timezone state
+ const [selectedTimezone, setSelectedTimezone] = useState(() =>
+ detectTimezone(),
+ );
+ const timezoneOffset = getTimezoneOffset();
+
+ const selectedToken =
+ isCustom && customToken
+ ? customToken
+ : (tokens.find((t) => t.address === form.tokenAddress) ?? tokens[0]);
+
+ // Issue #186: USD conversion
+ const [usdInputMode, setUsdInputMode] = useState(false);
+ const [usdAmount, setUsdAmount] = useState("");
+ const {
+ usdPrice,
+ stale: priceStale,
+ loading: priceLoading,
+ } = useTokenPrice(selectedToken?.symbol ?? "");
+
+ const supportsUsd = usdPrice !== null;
+ const tokenAmountNum = parseFloat(form.amount) || 0;
+ const usdEquivalent =
+ supportsUsd && tokenAmountNum > 0
+ ? (tokenAmountNum * usdPrice).toFixed(2)
+ : null;
+
+ const amountPerSecondUsd =
+ usdEquivalent &&
+ (() => {
+ const dur =
+ (new Date(form.endDate).getTime() -
+ new Date(form.startDate).getTime()) /
+ 1000;
+ if (dur <= 0) return null;
+ const usdPerSec = parseFloat(usdEquivalent) / dur;
+ return usdPerSec < 0.01
+ ? usdPerSec.toExponential(2)
+ : usdPerSec.toFixed(4);
+ })();
+
+ function handleUsdAmountChange(val: string) {
+ setUsdAmount(val);
+ if (usdPrice && val) {
+ const tokenAmt = parseFloat(val) / usdPrice;
+ if (!isNaN(tokenAmt))
+ set("amount", tokenAmt.toFixed(selectedToken?.decimals ?? 7));
+ } else {
+ set("amount", "");
+ }
+ }
+
+ // Fetch balance when token or wallet changes
+ useEffect(() => {
+ if (!walletAddress || !selectedToken) return;
+ setTokenBalance(null);
+ setBalanceLoading(true);
+ getTokenBalance(selectedToken.address, walletAddress)
+ .then(setTokenBalance)
+ .catch(() => setTokenBalance(null))
+ .finally(() => setBalanceLoading(false));
+ }, [selectedToken?.address, walletAddress]);
+
+ // Issue #103: Check recipient account info when address changes
+ useEffect(() => {
+ const recipient = form.recipient.trim();
+ if (!recipient || !StrKey.isValidEd25519PublicKey(recipient)) {
+ setRecipientAccountInfo(null);
+ setRecipientWarningAcknowledged(false);
+ return;
+ }
+
+ setRecipientChecking(true);
+ checkAccountInfo(recipient, network)
+ .then((info) => {
+ setRecipientAccountInfo(info);
+ setRecipientWarningAcknowledged(false);
+ })
+ .catch(() => {
+ setRecipientAccountInfo(null);
+ })
+ .finally(() => {
+ setRecipientChecking(false);
+ });
+ }, [form.recipient, network]);
+
+ // Issue #168: wire up draft hook
+ const { loadDraft, restore, discard } = useFormDraft(
+ `create-stream-${walletAddress ?? "anonymous"}`,
+ form,
+ (draft) => {
+ setForm(draft);
+ },
+ true,
+ );
+
+ // Check for existing draft on first mount
+ useEffect(() => {
+ if (!isFirstMount.current) return;
+ isFirstMount.current = false;
+ clearExpiredDrafts();
+ const entry = loadDraft();
+ if (entry) {
+ setDraftSavedAt(entry.savedAt);
+ setShowDraftBanner(true);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ async function handleCustomTokenLookup() {
+ if (!customAddress || customAddress.length < 56) {
+ setCustomError(
+ "Enter a valid Stellar contract address (56 chars, starts with C)",
+ );
+ return;
+ }
+ setCustomLoading(true);
+ setCustomError(null);
+ setCustomToken(null);
+ try {
+ const meta = await getTokenMetadata(customAddress);
+ if (!meta) {
+ setCustomError(
+ "Could not fetch token metadata. Verify this is a valid SEP-41 token contract.",
+ );
+ return;
+ }
+ setCustomToken(meta);
+ saveCustomToken(network, meta);
+ setTokens(getAllTokens(network).map((t) => ({ ...t })));
+ set("tokenAddress", meta.address);
+ } catch {
+ setCustomError("Failed to query token contract");
+ } finally {
+ setCustomLoading(false);
+ }
+ }
+
+ function set(key: K, value: FormState[K]) {
+ setForm((prev) => ({ ...prev, [key]: value }));
+ setErrors((prev) => ({ ...prev, [key]: undefined }));
+ }
+
+ function validate(): boolean {
+ const newErrors: Partial> = {};
+
+ // Issue #28: use StrKey for proper Stellar address validation
+ if (
+ !form.recipient.trim() ||
+ !StrKey.isValidEd25519PublicKey(form.recipient.trim())
+ ) {
+ newErrors.recipient = "Invalid Stellar address format";
+ }
+ // Issue #103: require warning acknowledgment for unfunded accounts
+ if (
+ recipientAccountInfo &&
+ !recipientAccountInfo.exists &&
+ !recipientWarningAcknowledged
+ ) {
+ newErrors.recipient =
+ "Please acknowledge the warning about this recipient address";
+ }
+ if (
+ !form.amount ||
+ isNaN(Number(form.amount)) ||
+ Number(form.amount) <= 0
+ ) {
+ newErrors.amount = "Enter a valid amount greater than 0";
+ }
+ // Issue #29: validate against balance
+ if (form.amount && tokenBalance !== null) {
+ const parsed = parseTokenAmount(form.amount, selectedToken.decimals);
+ if (parsed > tokenBalance) {
+ newErrors.amount = `Amount exceeds your balance (${formatTokenAmount(tokenBalance, selectedToken.decimals, 4)} ${selectedToken.symbol})`;
+ }
+ }
+ if (isCustom && !customToken) {
+ newErrors.tokenAddress = "Look up a valid custom token first";
+ }
+ const start = new Date(form.startDate).getTime();
+ const end = new Date(form.endDate).getTime();
+ if (!form.endDate || end <= start) {
+ newErrors.endDate = "End date must be after start date";
+ }
+ if (form.hasCliff) {
+ const cliff = new Date(form.cliffDate).getTime();
+ if (!form.cliffDate || cliff < start || cliff > end) {
+ newErrors.cliffDate = "Cliff must be between start and end date";
+ }
+ if (form.cliffAmount && Number(form.cliffAmount) > Number(form.amount)) {
+ newErrors.cliffAmount = "Cliff amount cannot exceed total amount";
+ }
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ }
+
+ const buildInput = useCallback(() => {
+ const startTime = toUnixSeconds(form.startDate);
+ const endTime = toUnixSeconds(form.endDate);
+ const cliffTime = form.hasCliff ? toUnixSeconds(form.cliffDate) : startTime;
+ const cliffAmount =
+ form.hasCliff && form.cliffAmount
+ ? parseTokenAmount(form.cliffAmount, selectedToken.decimals)
+ : 0n;
+ return {
+ recipient: form.recipient.trim(),
+ token: selectedToken,
+ totalAmount: parseTokenAmount(form.amount, selectedToken.decimals),
+ startTime,
+ endTime,
+ cliffTime,
+ cliffAmount,
+ };
+ }, [form, selectedToken]);
+
+ async function handleEstimateFee() {
+ if (!validate()) return;
+ setEstimatingFee(true);
+ setFeeEstimate(null);
+ try {
+ const estimate = await estimateFee(buildInput());
+ if (estimate) {
+ setFeeEstimate(estimate.estimatedFeeXlm);
+ }
+ } catch {
+ setFeeEstimate(null);
+ } finally {
+ setEstimatingFee(false);
+ }
+ }
+
+ function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ if (!validate()) return;
+ // Show tx simulation preview first; user proceeds to Freighter from there.
+ setShowTxPreview(true);
+ }
+
+ async function handleConfirmedCreate() {
+ try {
+ const input = buildInput();
+ const id = await createStream(input);
+ touchAddressBookEntry(input.recipient, input.recipient);
+ setAddressBookEntries(getAddressBookEntries());
+
+ if (recurrenceCadence !== "none") {
+ saveRecurringRule({
+ cadence: recurrenceCadence,
+ nextRunAt: buildNextRunAt(Date.now(), recurrenceCadence),
+ lastCreatedAt: Date.now(),
+ streamId: id,
+ recipient: input.recipient,
+ tokenSymbol: input.token.symbol,
+ amount: input.totalAmount.toString(),
+ });
+ }
+
+ discard();
+ setShowConfirmation(false);
+ toast.success("Stream created", {
+ description: `Stream #${id} is live.`,
+ });
+ router.push(`/app/stream/${id}`);
+ } catch {
+ // error is exposed via useContract
+ }
+ }
+
+ function handleTemplateSelect(template: StreamTemplate) {
+ setSelectedTemplateId(template.id);
+ const newStart = localDatetimeMin(60);
+ const newEnd = addDuration(newStart, template.durationSeconds);
+ const hasCliff = template.cliffSeconds > 0;
+ const newCliff = hasCliff
+ ? addDuration(newStart, template.cliffSeconds)
+ : newStart;
+
+ setForm((prev) => {
+ const amount = prev.amount;
+ const cliffAmount =
+ hasCliff && template.cliffPercent > 0 && amount
+ ? String(Math.floor((Number(amount) * template.cliffPercent) / 100))
+ : "";
+ return {
+ ...prev,
+ startDate: newStart,
+ endDate: newEnd,
+ hasCliff,
+ cliffDate: newCliff,
+ cliffAmount,
+ };
+ });
+ setErrors({});
+ }
+
+ const input = showTxPreview || showConfirmation ? buildInput() : null;
+ const durationSeconds =
+ (new Date(form.endDate).getTime() - new Date(form.startDate).getTime()) /
+ 1000;
+ const amountPerSecond =
+ input && durationSeconds > 0
+ ? input.totalAmount / BigInt(Math.floor(durationSeconds))
+ : 0n;
+
+ return (
+
+ {/* Back */}
+
+
+ Back to dashboard
+
+
+
+
+ Create a stream
+
+
+ Tokens unlock continuously to the recipient from start to end.
+
+
+
+
+
+
+ {cloneId && (
+
+
+
+ Duplicating Stream #{cloneId} — form pre-filled with its parameters.
+
+
+ )}
+
+ {/* Issue #168: Draft restore banner */}
+ {showDraftBanner && (
+
+
+
+
+ You have an unsaved draft from{" "}
+ {draftSavedAt
+ ? new Date(draftSavedAt).toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ })
+ : "earlier"}
+ .
+
+
+
+ {
+ restore();
+ setShowDraftBanner(false);
+ toast.success("Draft restored");
+ }}
+ >
+ Restore
+
+ {
+ discard();
+ setShowDraftBanner(false);
+ }}
+ >
+ Discard
+
+
+
+ )}
+
+
+
+
+ {/* Live preview sidebar */}
+
+
+
+ {/* Step 1: dry-run simulation preview */}
+ {input && (
+
{
+ setShowTxPreview(false);
+ setShowConfirmation(true);
+ }}
+ onCancel={() => setShowTxPreview(false)}
+ pending={false}
+ />
+ )}
+
+ {/* Step 2: confirmation + fee details → Freighter signing */}
+ {input && (
+ setShowConfirmation(false)}
+ pending={pending}
+ feeEstimate={feeEstimate}
+ recipient={input.recipient}
+ token={input.token}
+ totalAmount={input.totalAmount}
+ startTime={input.startTime}
+ endTime={input.endTime}
+ cliffTime={input.cliffTime}
+ cliffAmount={input.cliffAmount}
+ amountPerSecond={amountPerSecond}
+ />
+ )}
+
+ );
}
diff --git a/app/app/create/page.tsx b/app/app/create/page.tsx
index 7ddf867..afe7ac8 100644
--- a/app/app/create/page.tsx
+++ b/app/app/create/page.tsx
@@ -1,1106 +1,17 @@
-import { Metadata } from 'next'
-import { useState, useCallback, useEffect, useRef } from 'react'
-import { useRouter, useSearchParams } from 'next/navigation'
-import { AlertTriangle, ArrowLeft, ArrowRight, Info, Loader2, Copy, Clock } from 'lucide-react'
-import Link from 'next/link'
-import { toast } from 'sonner'
-import { StrKey } from '@stellar/stellar-sdk'
-import { RequireWallet } from '@/components/layout/require-wallet'
-import { Button } from '@/components/ui/button'
-import { Input } from '@/components/ui/input'
-import { Label } from '@/components/ui/label'
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@/components/ui/select'
-import { useContract } from '@/hooks/use-contract'
-import { useWallet } from '@/hooks/use-wallet'
-import { getAllTokens, saveCustomToken, checkAccountInfo } from '@/lib/stellar'
-import { getTokenMetadata, getTokenBalance } from '@/lib/contract'
-import { parseTokenAmount, formatTokenAmount } from '@/lib/stream-utils'
-import { StreamPreview } from '@/components/streams/stream-preview'
-import { CreateConfirmation } from '@/components/streams/create-confirmation'
-import { TxPreviewDialog } from '@/components/ui/tx-preview-dialog'
-import { addAddressBookEntry, getAddressBookEntries, touchAddressBookEntry } from '@/lib/address-book'
-import { buildNextRunAt, saveRecurringRule, type RecurrenceCadence } from '@/lib/recurring'
-import { useFormDraft, clearExpiredDrafts } from '@/hooks/use-form-draft'
-import { StreamTemplates, type StreamTemplate } from '@/components/streams/stream-templates'
-import { useTokenPrice } from '@/hooks/use-token-price'
-import type { TokenInfo } from '@/types/stream'
-import { useNetwork } from '@/components/providers/network-provider'
-export const metadata: Metadata = {
- title: 'Create a Token Stream',
- description: 'Create a new token stream on FlowStar with customizable schedules, cliffs, and cancellation options.',
-}
-
-const CUSTOM_VALUE = '__custom__'
-
-function toUnixSeconds(localDatetimeValue: string): bigint {
- return BigInt(Math.floor(new Date(localDatetimeValue).getTime() / 1000))
-}
-
-function localDatetimeMin(offsetSeconds = 0): string {
- const d = new Date(Date.now() + offsetSeconds * 1000)
- // Format as YYYY-MM-DDTHH:mm in local time (not UTC)
- const pad = (n: number) => String(n).padStart(2, '0')
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
-}
-
-function addDuration(baseDatetime: string, seconds: number): string {
- const base = new Date(baseDatetime)
- const d = new Date(base.getTime() + seconds * 1000)
- const pad = (n: number) => String(n).padStart(2, '0')
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
-}
-
-function detectTimezone(): string {
- try {
- return Intl.DateTimeFormat().resolvedOptions().timeZone
- } catch {
- return 'UTC'
- }
-}
-
-function getTimezoneOffset(): string {
- const offset = -new Date().getTimezoneOffset()
- const sign = offset >= 0 ? '+' : '-'
- const h = Math.floor(Math.abs(offset) / 60)
- const m = Math.abs(offset) % 60
- return `UTC${sign}${h}${m > 0 ? `:${String(m).padStart(2, '0')}` : ''}`
-}
-
-const COMMON_TIMEZONES = [
- 'UTC',
- 'America/New_York',
- 'America/Chicago',
- 'America/Denver',
- 'America/Los_Angeles',
- 'America/Sao_Paulo',
- 'Europe/London',
- 'Europe/Paris',
- 'Europe/Berlin',
- 'Asia/Dubai',
- 'Asia/Kolkata',
- 'Asia/Singapore',
- 'Asia/Tokyo',
- 'Australia/Sydney',
-] as const
-
-const DURATION_PRESETS = [
- { label: '1 week', seconds: 7 * 24 * 3600 },
- { label: '1 month', seconds: 30 * 24 * 3600 },
- { label: '3 months', seconds: 90 * 24 * 3600 },
- { label: '6 months', seconds: 180 * 24 * 3600 },
- { label: '1 year', seconds: 365 * 24 * 3600 },
-] as const
-
-const CLIFF_PRESETS = [
- { label: 'No cliff', seconds: 0 },
- { label: '1 month', seconds: 30 * 24 * 3600 },
- { label: '3 months', seconds: 90 * 24 * 3600 },
-] as const
-
-interface FormState {
- recipient: string
- tokenAddress: string
- amount: string
- startDate: string
- endDate: string
- hasCliff: boolean
- cliffDate: string
- cliffAmount: string
-}
-
-function CreateForm() {
- const router = useRouter()
- const searchParams = useSearchParams()
- const cloneId = searchParams.get('clone')
- const { address: walletAddress } = useWallet()
- const { network } = useNetwork()
- const { createStream, estimateFee, pending, error } = useContract()
- const [feeEstimate, setFeeEstimate] = useState(null)
- const [estimatingFee, setEstimatingFee] = useState(false)
- const [showTxPreview, setShowTxPreview] = useState(false)
- const [showConfirmation, setShowConfirmation] = useState(false)
-
- const [tokens, setTokens] = useState(() => getAllTokens(network).map((t) => ({ ...t })))
- const [isCustom, setIsCustom] = useState(false)
- const [customAddress, setCustomAddress] = useState('')
- const [customLoading, setCustomLoading] = useState(false)
- const [customError, setCustomError] = useState(null)
- const [customToken, setCustomToken] = useState(null)
- const [addressBookEntries, setAddressBookEntries] = useState(() => getAddressBookEntries())
- const [recurrenceCadence, setRecurrenceCadence] = useState('none')
-
- // Issue #166: stream metadata
- const [metaName, setMetaName] = useState('')
- const [metaCategory, setMetaCategory] = useState('')
- const [metaMemo, setMetaMemo] = useState('')
-
- // Issue #29: balance state
- const [tokenBalance, setTokenBalance] = useState(null)
- const [balanceLoading, setBalanceLoading] = useState(false)
-
- // Issue #103: recipient account validation
- const [recipientAccountInfo, setRecipientAccountInfo] = useState<{ exists: boolean; funded: boolean; transactionCount: number } | null>(null)
- const [recipientChecking, setRecipientChecking] = useState(false)
- const [recipientWarningAcknowledged, setRecipientWarningAcknowledged] = useState(false)
-
- const defaultStart = localDatetimeMin(60)
- const defaultEnd = localDatetimeMin(60 + 30 * 24 * 3600)
-
- const [form, setForm] = useState(() => {
- const newStart = localDatetimeMin(60)
- const durationSecs = searchParams.get('duration')
- const cliffSecs = searchParams.get('cliff')
- const hasCliff = cliffSecs !== null && cliffSecs !== '0'
- const newEnd = durationSecs
- ? addDuration(newStart, Number(durationSecs))
- : localDatetimeMin(60 + 30 * 24 * 3600)
- const newCliff = hasCliff && cliffSecs
- ? addDuration(newStart, Number(cliffSecs))
- : newStart
-
- return {
- recipient: searchParams.get('recipient') ?? '',
- tokenAddress: searchParams.get('token') ?? (tokens[0]?.address ?? ''),
- amount: searchParams.get('amount') ?? '',
- startDate: newStart,
- endDate: newEnd,
- hasCliff,
- cliffDate: newCliff,
- cliffAmount: searchParams.get('cliffAmount') ?? '',
- }
- })
-
- const [errors, setErrors] = useState>>({})
- const [selectedTemplateId, setSelectedTemplateId] = useState(undefined)
-
- // Issue #168: draft state
- const [showDraftBanner, setShowDraftBanner] = useState(false)
- const [draftSavedAt, setDraftSavedAt] = useState(null)
- const isFirstMount = useRef(true)
-
- // Issue #170: timezone state
- const [selectedTimezone, setSelectedTimezone] = useState(() => detectTimezone())
- const timezoneOffset = getTimezoneOffset()
-
- const selectedToken = isCustom && customToken
- ? customToken
- : tokens.find((t) => t.address === form.tokenAddress) ?? tokens[0]
-
- // Issue #186: USD conversion
- const [usdInputMode, setUsdInputMode] = useState(false)
- const [usdAmount, setUsdAmount] = useState('')
- const { usdPrice, stale: priceStale, loading: priceLoading } = useTokenPrice(selectedToken?.symbol ?? '')
-
- const supportsUsd = usdPrice !== null
- const tokenAmountNum = parseFloat(form.amount) || 0
- const usdEquivalent = supportsUsd && tokenAmountNum > 0
- ? (tokenAmountNum * usdPrice).toFixed(2)
- : null
-
- const amountPerSecondUsd = usdEquivalent && (() => {
- const dur = (new Date(form.endDate).getTime() - new Date(form.startDate).getTime()) / 1000
- if (dur <= 0) return null
- const usdPerSec = (parseFloat(usdEquivalent) / dur)
- return usdPerSec < 0.01 ? usdPerSec.toExponential(2) : usdPerSec.toFixed(4)
- })()
-
- function handleUsdAmountChange(val: string) {
- setUsdAmount(val)
- if (usdPrice && val) {
- const tokenAmt = parseFloat(val) / usdPrice
- if (!isNaN(tokenAmt)) set('amount', tokenAmt.toFixed(selectedToken?.decimals ?? 7))
- } else {
- set('amount', '')
- }
- }
-
- // Fetch balance when token or wallet changes
- useEffect(() => {
- if (!walletAddress || !selectedToken) return
- setTokenBalance(null)
- setBalanceLoading(true)
- getTokenBalance(selectedToken.address, walletAddress)
- .then(setTokenBalance)
- .catch(() => setTokenBalance(null))
- .finally(() => setBalanceLoading(false))
- }, [selectedToken?.address, walletAddress])
-
- // Issue #103: Check recipient account info when address changes
- useEffect(() => {
- const recipient = form.recipient.trim()
- if (!recipient || !StrKey.isValidEd25519PublicKey(recipient)) {
- setRecipientAccountInfo(null)
- setRecipientWarningAcknowledged(false)
- return
- }
-
- setRecipientChecking(true)
- checkAccountInfo(recipient, network)
- .then((info) => {
- setRecipientAccountInfo(info)
- setRecipientWarningAcknowledged(false)
- })
- .catch(() => {
- setRecipientAccountInfo(null)
- })
- .finally(() => {
- setRecipientChecking(false)
- })
- }, [form.recipient, network])
-
- // Issue #168: wire up draft hook
- const { loadDraft, restore, discard } = useFormDraft(
- `create-stream-${walletAddress ?? 'anonymous'}`,
- form,
- (draft) => {
- setForm(draft)
- },
- true,
- )
-
- // Check for existing draft on first mount
- useEffect(() => {
- if (!isFirstMount.current) return
- isFirstMount.current = false
- clearExpiredDrafts()
- const entry = loadDraft()
- if (entry) {
- setDraftSavedAt(entry.savedAt)
- setShowDraftBanner(true)
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [])
-
- async function handleCustomTokenLookup() {
- if (!customAddress || customAddress.length < 56) {
- setCustomError('Enter a valid Stellar contract address (56 chars, starts with C)')
- return
- }
- setCustomLoading(true)
- setCustomError(null)
- setCustomToken(null)
- try {
- const meta = await getTokenMetadata(customAddress)
- if (!meta) {
- setCustomError('Could not fetch token metadata. Verify this is a valid SEP-41 token contract.')
- return
- }
- setCustomToken(meta)
- saveCustomToken(network, meta)
- setTokens(getAllTokens(network).map((t) => ({ ...t })))
- set('tokenAddress', meta.address)
- } catch {
- setCustomError('Failed to query token contract')
- } finally {
- setCustomLoading(false)
- }
- }
-
- function set(key: K, value: FormState[K]) {
- setForm((prev) => ({ ...prev, [key]: value }))
- setErrors((prev) => ({ ...prev, [key]: undefined }))
- }
-
- function validate(): boolean {
- const newErrors: Partial> = {}
+import { Metadata } from "next";
+import { RequireWallet } from "@/components/layout/require-wallet";
+import { CreateForm } from "./create-form";
- // Issue #28: use StrKey for proper Stellar address validation
- if (!form.recipient.trim() || !StrKey.isValidEd25519PublicKey(form.recipient.trim())) {
- newErrors.recipient = 'Invalid Stellar address format'
- }
- // Issue #103: require warning acknowledgment for unfunded accounts
- if (recipientAccountInfo && !recipientAccountInfo.exists && !recipientWarningAcknowledged) {
- newErrors.recipient = 'Please acknowledge the warning about this recipient address'
- }
- if (!form.amount || isNaN(Number(form.amount)) || Number(form.amount) <= 0) {
- newErrors.amount = 'Enter a valid amount greater than 0'
- }
- // Issue #29: validate against balance
- if (form.amount && tokenBalance !== null) {
- const parsed = parseTokenAmount(form.amount, selectedToken.decimals)
- if (parsed > tokenBalance) {
- newErrors.amount = `Amount exceeds your balance (${formatTokenAmount(tokenBalance, selectedToken.decimals, 4)} ${selectedToken.symbol})`
- }
- }
- if (isCustom && !customToken) {
- newErrors.tokenAddress = 'Look up a valid custom token first'
- }
- const start = new Date(form.startDate).getTime()
- const end = new Date(form.endDate).getTime()
- if (!form.endDate || end <= start) {
- newErrors.endDate = 'End date must be after start date'
- }
- if (form.hasCliff) {
- const cliff = new Date(form.cliffDate).getTime()
- if (!form.cliffDate || cliff < start || cliff > end) {
- newErrors.cliffDate = 'Cliff must be between start and end date'
- }
- if (form.cliffAmount && Number(form.cliffAmount) > Number(form.amount)) {
- newErrors.cliffAmount = 'Cliff amount cannot exceed total amount'
- }
- }
-
- setErrors(newErrors)
- return Object.keys(newErrors).length === 0
- }
-
- const buildInput = useCallback(() => {
- const startTime = toUnixSeconds(form.startDate)
- const endTime = toUnixSeconds(form.endDate)
- const cliffTime = form.hasCliff ? toUnixSeconds(form.cliffDate) : startTime
- const cliffAmount = form.hasCliff && form.cliffAmount
- ? parseTokenAmount(form.cliffAmount, selectedToken.decimals)
- : 0n
- const metadata = metaName || metaCategory || metaMemo
- ? { name: metaName, category: metaCategory, memo: metaMemo }
- : undefined
- return {
- recipient: form.recipient.trim(),
- token: selectedToken,
- totalAmount: parseTokenAmount(form.amount, selectedToken.decimals),
- startTime,
- endTime,
- cliffTime,
- cliffAmount,
- metadata,
- }
- }, [form, selectedToken, metaName, metaCategory, metaMemo])
-
- async function handleEstimateFee() {
- if (!validate()) return
- setEstimatingFee(true)
- setFeeEstimate(null)
- try {
- const estimate = await estimateFee(buildInput())
- if (estimate) {
- setFeeEstimate(estimate.estimatedFeeXlm)
- }
- } catch {
- setFeeEstimate(null)
- } finally {
- setEstimatingFee(false)
- }
- }
-
- function handleSubmit(e: React.FormEvent) {
- e.preventDefault()
- if (!validate()) return
- // Show tx simulation preview first; user proceeds to Freighter from there.
- setShowTxPreview(true)
- }
-
- async function handleConfirmedCreate() {
- try {
- const input = buildInput()
- const id = await createStream(input)
- touchAddressBookEntry(input.recipient, input.recipient)
- setAddressBookEntries(getAddressBookEntries())
-
- if (recurrenceCadence !== 'none') {
- saveRecurringRule({
- cadence: recurrenceCadence,
- nextRunAt: buildNextRunAt(Date.now(), recurrenceCadence),
- lastCreatedAt: Date.now(),
- streamId: id,
- recipient: input.recipient,
- tokenSymbol: input.token.symbol,
- amount: input.totalAmount.toString(),
- })
- }
-
- discard()
- setShowConfirmation(false)
- toast.success('Stream created', { description: `Stream #${id} is live.` })
- router.push(`/app/stream/${id}`)
- } catch {
- // error is exposed via useContract
- }
- }
-
- function handleTemplateSelect(template: StreamTemplate) {
- setSelectedTemplateId(template.id)
- const newStart = localDatetimeMin(60)
- const newEnd = addDuration(newStart, template.durationSeconds)
- const hasCliff = template.cliffSeconds > 0
- const newCliff = hasCliff ? addDuration(newStart, template.cliffSeconds) : newStart
-
- setForm((prev) => {
- const amount = prev.amount
- const cliffAmount = hasCliff && template.cliffPercent > 0 && amount
- ? String(Math.floor(Number(amount) * template.cliffPercent / 100))
- : ''
- return {
- ...prev,
- startDate: newStart,
- endDate: newEnd,
- hasCliff,
- cliffDate: newCliff,
- cliffAmount,
- }
- })
- setErrors({})
- }
-
- const input = (showTxPreview || showConfirmation) ? buildInput() : null
- const durationSeconds = (new Date(form.endDate).getTime() - new Date(form.startDate).getTime()) / 1000
- const amountPerSecond = input && durationSeconds > 0
- ? input.totalAmount / BigInt(Math.floor(durationSeconds))
- : 0n
-
- return (
-
- {/* Back */}
-
-
- Back to dashboard
-
-
-
-
Create a stream
-
- Tokens unlock continuously to the recipient from start to end.
-
-
-
-
-
-
- {cloneId && (
-
-
-
- Duplicating Stream #{cloneId} — form pre-filled with its parameters.
-
-
- )}
-
- {/* Issue #168: Draft restore banner */}
- {showDraftBanner && (
-
-
-
-
- You have an unsaved draft from{' '}
- {draftSavedAt
- ? new Date(draftSavedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
- : 'earlier'}
- .
-
-
-
- {
- restore()
- setShowDraftBanner(false)
- toast.success('Draft restored')
- }}
- >
- Restore
-
- {
- discard()
- setShowDraftBanner(false)
- }}
- >
- Discard
-
-
-
- )}
-
-
-
-
- {/* Live preview sidebar */}
-
-
-
- {/* Step 1: dry-run simulation preview */}
- {input && (
-
{ setShowTxPreview(false); setShowConfirmation(true) }}
- onCancel={() => setShowTxPreview(false)}
- pending={false}
- />
- )}
-
- {/* Step 2: confirmation + fee details → Freighter signing */}
- {input && (
- setShowConfirmation(false)}
- pending={pending}
- feeEstimate={feeEstimate}
- recipient={input.recipient}
- token={input.token}
- totalAmount={input.totalAmount}
- startTime={input.startTime}
- endTime={input.endTime}
- cliffTime={input.cliffTime}
- cliffAmount={input.cliffAmount}
- amountPerSecond={amountPerSecond}
- />
- )}
-
- )
-}
+export const metadata: Metadata = {
+ title: "Create a Token Stream",
+ description:
+ "Create a new token stream on FlowStar with customizable schedules, cliffs, and cancellation options.",
+};
export default function CreatePage() {
return (
- )
-}
\ No newline at end of file
+ );
+}
diff --git a/app/app/page.tsx b/app/app/page.tsx
index 0eec55a..47773ef 100644
--- a/app/app/page.tsx
+++ b/app/app/page.tsx
@@ -1,486 +1,17 @@
-import { Metadata } from 'next'
-import { RequireWallet } from '@/components/layout/require-wallet'
-import { Dashboard } from './dashboard'
+import { Metadata } from "next";
+import { RequireWallet } from "@/components/layout/require-wallet";
+import { Dashboard } from "./dashboard";
export const metadata: Metadata = {
- title: 'Your Streams',
- description: 'View and manage your active and historical token streams on FlowStar.',
-'use client'
-
-import Link from 'next/link'
-import { useState, useMemo, Suspense } from 'react'
-import { useRouter, useSearchParams } from 'next/navigation'
-import { Plus, ArrowDownToLine, Search, X } from 'lucide-react'
-import { RequireWallet } from '@/components/layout/require-wallet'
-import { DashboardStats, DashboardStatsSkeleton } from '@/components/streams/dashboard-stats'
-import { StreamCard, StreamCardSkeleton } from '@/components/streams/stream-card'
-import { EmptyStreams } from '@/components/streams/empty-state'
-import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
-import { Button } from '@/components/ui/button'
-import { Input } from '@/components/ui/input'
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@/components/ui/select'
-import { Badge } from '@/components/ui/badge'
-import { useStreams } from '@/hooks/use-streams'
-import { useContract } from '@/hooks/use-contract'
-import { useNow } from '@/hooks/use-now'
-import { getWithdrawableAmount, getStreamStatus, getStreamProgress } from '@/lib/stream-utils'
-import type { StreamData, StreamStatus } from '@/types/stream'
-
-// ─── Filter / sort types ──────────────────────────────────────────────────────
-
-type SortKey = 'newest' | 'oldest' | 'amount_desc' | 'amount_asc' | 'end_asc' | 'progress_desc'
-
-const STATUS_OPTIONS: { value: StreamStatus | 'all'; label: string }[] = [
- { value: 'all', label: 'All statuses' },
- { value: 'scheduled', label: 'Scheduled' },
- { value: 'streaming', label: 'Streaming' },
- { value: 'completed', label: 'Completed' },
- { value: 'cancelled', label: 'Cancelled' },
-]
-
-const SORT_OPTIONS: { value: SortKey; label: string }[] = [
- { value: 'newest', label: 'Newest first' },
- { value: 'oldest', label: 'Oldest first' },
- { value: 'amount_desc', label: 'Highest amount' },
- { value: 'amount_asc', label: 'Lowest amount' },
- { value: 'end_asc', label: 'Ending soonest' },
- { value: 'progress_desc', label: 'Most progress' },
-]
-
-// ─── Hook: URL-synced filters ─────────────────────────────────────────────────
-
-function useFilters() {
- const router = useRouter()
- const params = useSearchParams()
-
- const search = params.get('q') ?? ''
- const status = (params.get('status') ?? 'all') as StreamStatus | 'all'
- const token = params.get('token') ?? 'all'
- const sort = (params.get('sort') ?? 'newest') as SortKey
-
- function update(key: string, value: string) {
- const next = new URLSearchParams(params.toString())
- if (value === '' || value === 'all' || value === 'newest') {
- next.delete(key)
- } else {
- next.set(key, value)
- }
- router.replace(`?${next.toString()}`, { scroll: false })
- }
-
- function clear() {
- router.replace('?', { scroll: false })
- }
-
- const isActive = search !== '' || status !== 'all' || token !== 'all' || sort !== 'newest'
-
- return { search, status, token, sort, update, clear, isActive }
-}
-
-// ─── Filter + sort logic ──────────────────────────────────────────────────────
-
-function applyFilters(
- streams: StreamData[],
- search: string,
- status: StreamStatus | 'all',
- token: string,
- sort: SortKey,
- nowSeconds: number,
-): StreamData[] {
- let result = streams
-
- if (search) {
- const q = search.toLowerCase()
- result = result.filter(
- (s) =>
- s.id.toLowerCase().includes(q) ||
- s.sender.toLowerCase().includes(q) ||
- s.recipient.toLowerCase().includes(q) ||
- s.token.symbol.toLowerCase().includes(q),
- )
- }
-
- if (status !== 'all') {
- result = result.filter((s) => getStreamStatus(s, nowSeconds) === status)
- }
-
- if (token !== 'all') {
- result = result.filter((s) => s.token.symbol === token)
- }
-
- result = [...result].sort((a, b) => {
- switch (sort) {
- case 'oldest':
- return Number(a.startTime - b.startTime)
- case 'amount_desc':
- return Number(b.depositedAmount - a.depositedAmount)
- case 'amount_asc':
- return Number(a.depositedAmount - b.depositedAmount)
- case 'end_asc':
- return Number(a.endTime - b.endTime)
- case 'progress_desc':
- return getStreamProgress(b, nowSeconds) - getStreamProgress(a, nowSeconds)
- case 'newest':
- default:
- return Number(b.startTime - a.startTime)
- }
- })
-
- return result
-}
-
-// ─── StreamGrid ───────────────────────────────────────────────────────────────
-
-function StreamGrid({ streams }: { streams: StreamData[] }) {
- if (streams.length === 0) return
- return (
-
- {streams.map((s) => (
-
- ))}
-
- )
-}
-
-// ─── FilterBar ────────────────────────────────────────────────────────────────
-
-function FilterBar({
- tokens,
- filters,
-}: {
- tokens: string[]
- filters: ReturnType
-}) {
- const { search, status, token, sort, update, clear, isActive } = filters
-
- return (
-
- {/* Search + sort row */}
-
-
-
- update('q', e.target.value)}
- className="pl-9"
- />
- {search && (
- update('q', '')}
- className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
- >
-
-
- )}
-
-
update('sort', v)}>
-
-
-
-
- {SORT_OPTIONS.map((o) => (
-
- {o.label}
-
- ))}
-
-
-
-
- {/* Filter chips row */}
-
- update('status', v)}>
-
-
-
-
- {STATUS_OPTIONS.map((o) => (
-
- {o.label}
-
- ))}
-
-
-
- {tokens.length > 0 && (
- update('token', v)}>
-
-
-
-
- All tokens
- {tokens.map((sym) => (
-
- {sym}
-
- ))}
-
-
- )}
-
- {isActive && (
-
-
- Clear filters
-
- )}
-
-
- )
-}
-
-// ─── Dashboard ────────────────────────────────────────────────────────────────
-import { SectionErrorBoundary } from '@/components/error-boundary/section-error-boundary'
-import { ComponentErrorBoundary } from '@/components/error-boundary/component-error-boundary'
-import { useStreams } from '@/hooks/use-streams'
-import { useContract } from '@/hooks/use-contract'
-import { useNow } from '@/hooks/use-now'
-import { useWallet } from '@/hooks/use-wallet'
-import { getWithdrawableAmount } from '@/lib/stream-utils'
-import { ActivityFeed } from '@/components/streams/activity-feed'
-
-function Dashboard() {
- const { sent, received, all, loading } = useStreams()
- const { withdrawAll, pending } = useContract()
- const now = useNow(1000)
- const filters = useFilters()
- const { address: walletAddress } = useWallet()
- const [withdrawProgress, setWithdrawProgress] = useState<{ current: number; total: number } | null>(null)
-
- const withdrawableStreams = received.filter((s) => getWithdrawableAmount(s, now) > 0n)
- const isWithdrawingAll = withdrawProgress !== null
-
- const handleWithdrawAll = async () => {
- setWithdrawProgress({ current: 0, total: withdrawableStreams.length })
- try {
- await withdrawAll(received, (current, total) => {
- setWithdrawProgress({ current, total })
- })
- } finally {
- setWithdrawProgress(null)
- }
- }
-
- // Derive unique token symbols for the filter dropdown
- const tokenSymbols = useMemo(
- () => [...new Set(all.map((s) => s.token.symbol))].sort(),
- [all],
- )
-
- const filteredAll = useMemo(
- () => applyFilters(all, filters.search, filters.status, filters.token, filters.sort, now),
- [all, filters.search, filters.status, filters.token, filters.sort, now],
- )
- const filteredReceived = useMemo(
- () => applyFilters(received, filters.search, filters.status, filters.token, filters.sort, now),
- [received, filters.search, filters.status, filters.token, filters.sort, now],
- )
- const filteredSent = useMemo(
- () => applyFilters(sent, filters.search, filters.status, filters.token, filters.sort, now),
- [sent, filters.search, filters.status, filters.token, filters.sort, now],
- )
-
- return (
-
- {/* Header */}
-
-
-
Dashboard
-
- Your active and historical token streams.
-
-
-
- {withdrawableStreams.length > 0 && (
-
-
-
- {isWithdrawingAll
- ? `Withdrawing ${withdrawProgress.current}/${withdrawProgress.total}…`
- : `Withdraw all (${withdrawableStreams.length})`}
-
-
- {isWithdrawingAll
- ? `${withdrawProgress.current}/${withdrawProgress.total}`
- : withdrawableStreams.length}
-
-
- )}
-
-
-
- New stream
-
-
-
-
-
- {/* Stats */}
- {loading ?
:
}
-
-
-
-
- {/* Search / filter */}
-
-
- {/* Stream list */}
-
-
-
- All ({filteredAll.length}{filteredAll.length !== all.length ? `/${all.length}` : ''})
-
-
- Receiving ({filteredReceived.length}{filteredReceived.length !== received.length ? `/${received.length}` : ''})
-
-
- Sending ({filteredSent.length}{filteredSent.length !== sent.length ? `/${sent.length}` : ''})
-
- All ({all.length})
- Receiving ({received.length})
- Sending ({sent.length})
- Activity
-
-
-
-
- All ({all.length})
- Receiving ({received.length})
- Sending ({sent.length})
-
-
-
- {loading ? (
-
- {[0, 1, 2, 4].map((i) => )}
-
- ) : all.length === 0 ? (
-
- ) : (
-
- {all.map((s) => (
-
- ))}
-
- )}
-
-
-
- {loading ? (
-
- {[0, 1].map((i) => )}
-
- ) : received.length === 0 ? (
-
-
-
-
- {filteredReceived.length === 0 && received.length === 0 ? (
-
- ) : (
-
- )}
-
-
- {all.length === 0 ? (
-
- ) : (
-
- {all.map((s) => (
-
-
-
- ))}
-
- )}
-
-
-
- {received.length === 0 ? (
-
- ) : (
-
- {received.map((s) => (
-
-
-
- ))}
-
- )}
-
-
-
- {loading ? (
-
- {[0, 1].map((i) => )}
-
- ) : sent.length === 0 ? (
- {filteredSent.length === 0 && sent.length === 0 ? (
-
- ) : (
-
- )}
-
-
-
-
-
-
-
- {sent.length === 0 ? (
-
- ) : (
-
- {sent.map((s) => (
-
-
-
- ))}
-
- )}
-
-
-
-
- )
-}
+ title: "Your Streams",
+ description:
+ "View and manage your active and historical token streams on FlowStar.",
+};
export default function DashboardPage() {
return (
-
-
-
+
- )
+ );
}
diff --git a/app/app/stream/[id]/layout.tsx b/app/app/stream/[id]/layout.tsx
new file mode 100644
index 0000000..cff978e
--- /dev/null
+++ b/app/app/stream/[id]/layout.tsx
@@ -0,0 +1,11 @@
+import type { ReactNode } from "react";
+
+export { generateMetadata } from "./metadata";
+
+export default function StreamDetailLayout({
+ children,
+}: {
+ children: ReactNode;
+}) {
+ return children;
+}
diff --git a/app/app/stream/[id]/metadata.ts b/app/app/stream/[id]/metadata.ts
index 26c1b10..9f2cb38 100644
--- a/app/app/stream/[id]/metadata.ts
+++ b/app/app/stream/[id]/metadata.ts
@@ -1,40 +1,54 @@
-import { Metadata } from 'next'
-import { getStream } from '@/lib/contract'
+import { Metadata } from "next";
+import { fetchStream } from "@/lib/contract";
+import { NETWORK } from "@/lib/stellar";
export async function generateMetadata({
params,
}: {
- params: Promise<{ id: string }>
+ params: Promise<{ id: string }>;
}): Promise {
try {
- const { id } = await params
- const stream = await getStream(id)
+ const { id } = await params;
+ const stream = await fetchStream(NETWORK, id);
if (!stream) {
return {
- title: 'Stream not found | FlowStar',
- description: 'This stream may not exist or may have expired.',
- }
+ title: "Stream not found | FlowStar",
+ description: "This stream may not exist or may have expired.",
+ };
}
- const ogImageUrl = new URL('/api/og', 'https://flowstar.app')
- ogImageUrl.searchParams.append('amount', (stream.depositedAmount / 10n ** BigInt(stream.token.decimals)).toString())
- ogImageUrl.searchParams.append('symbol', stream.token.symbol)
- ogImageUrl.searchParams.append('recipient', stream.recipient)
- ogImageUrl.searchParams.append('sender', stream.sender)
- ogImageUrl.searchParams.append('status', stream.cancelled ? 'cancelled' : 'active')
+ const ogImageUrl = new URL("/api/og", "https://flowstar.app");
+ ogImageUrl.searchParams.append(
+ "amount",
+ (
+ stream.depositedAmount /
+ 10n ** BigInt(stream.token.decimals)
+ ).toString(),
+ );
+ ogImageUrl.searchParams.append("symbol", stream.token.symbol);
+ ogImageUrl.searchParams.append("recipient", stream.recipient);
+ ogImageUrl.searchParams.append("sender", stream.sender);
+ ogImageUrl.searchParams.append(
+ "status",
+ stream.cancelled ? "cancelled" : "active",
+ );
- const shortenAddress = (addr: string) => addr.slice(0, 6) + '...' + addr.slice(-4)
- const amount = (stream.depositedAmount / 10n ** BigInt(stream.token.decimals)).toString()
- const description = `${amount} ${stream.token.symbol} streaming to ${shortenAddress(stream.recipient)}`
+ const shortenAddress = (addr: string) =>
+ addr.slice(0, 6) + "..." + addr.slice(-4);
+ const amount = (
+ stream.depositedAmount /
+ 10n ** BigInt(stream.token.decimals)
+ ).toString();
+ const description = `${amount} ${stream.token.symbol} streaming to ${shortenAddress(stream.recipient)}`;
return {
title: `Stream #${stream.id} | FlowStar`,
description,
- metadataBase: new URL('https://flowstar.app'),
+ metadataBase: new URL("https://flowstar.app"),
openGraph: {
- type: 'website',
- siteName: 'FlowStar',
+ type: "website",
+ siteName: "FlowStar",
title: `Stream #${stream.id} | FlowStar`,
description,
images: [
@@ -47,17 +61,17 @@ export async function generateMetadata({
],
},
twitter: {
- card: 'summary_large_image',
+ card: "summary_large_image",
title: `Stream #${stream.id} | FlowStar`,
description,
images: [ogImageUrl.toString()],
},
- }
+ };
} catch (error) {
- console.error('Failed to generate metadata:', error)
+ console.error("Failed to generate metadata:", error);
return {
- title: 'Stream | FlowStar',
- description: 'Stream tokens by the second on Stellar',
- }
+ title: "Stream | FlowStar",
+ description: "Stream tokens by the second on Stellar",
+ };
}
}
diff --git a/app/app/stream/[id]/page.tsx b/app/app/stream/[id]/page.tsx
index 608c678..306110c 100644
--- a/app/app/stream/[id]/page.tsx
+++ b/app/app/stream/[id]/page.tsx
@@ -1,8 +1,8 @@
-'use client'
+"use client";
-import { use, useState } from 'react'
-import Link from 'next/link'
-import { useRouter } from 'next/navigation'
+import { use, useState } from "react";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
import {
ArrowLeft,
ArrowDownLeft,
@@ -17,31 +17,35 @@ import {
Share2,
MessageCircle,
Send,
-} from 'lucide-react'
-import { toast } from 'sonner'
-import { ConnectWalletButton } from '@/components/layout/connect-wallet-button'
-import { StreamStatusBadge } from '@/components/streams/stream-status-badge'
-import { ProgressBar } from '@/components/ui/progress-bar'
-import { TokenAmount } from '@/components/ui/token-amount'
-import { CountdownTimer } from '@/components/ui/countdown-timer'
-import { AccessibleCountdownTimer } from '@/components/ui/accessible-countdown-timer'
-import { AccessibleUnlockAmount } from '@/components/ui/accessible-unlock-amount'
-import { Button } from '@/components/ui/button'
-import { Input } from '@/components/ui/input'
-import { Label } from '@/components/ui/label'
-import { FeeEstimateDialog } from '@/components/ui/fee-estimate-dialog'
+} from "lucide-react";
+import { toast } from "sonner";
+import { ConnectWalletButton } from "@/components/layout/connect-wallet-button";
+import { StreamStatusBadge } from "@/components/streams/stream-status-badge";
+import { ProgressBar } from "@/components/ui/progress-bar";
+import { TokenAmount } from "@/components/ui/token-amount";
+import { CountdownTimer } from "@/components/ui/countdown-timer";
+import { AccessibleCountdownTimer } from "@/components/ui/accessible-countdown-timer";
+import { AccessibleUnlockAmount } from "@/components/ui/accessible-unlock-amount";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { FeeEstimateDialog } from "@/components/ui/fee-estimate-dialog";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
-} from '@/components/ui/dialog'
-import { calculateFeeBreakdown, TYPICAL_FEES, isHighFee } from '@/lib/fee-utils'
-import { useStream } from '@/hooks/use-streams'
-import { useContract } from '@/hooks/use-contract'
-import { useWallet } from '@/hooks/use-wallet'
-import { useNow } from '@/hooks/use-now'
+} from "@/components/ui/dialog";
+import {
+ calculateFeeBreakdown,
+ TYPICAL_FEES,
+ isHighFee,
+} from "@/lib/fee-utils";
+import { useStream } from "@/hooks/use-streams";
+import { useContract } from "@/hooks/use-contract";
+import { useWallet } from "@/hooks/use-wallet";
+import { useNow } from "@/hooks/use-now";
import {
getStreamStatus,
getStreamProgress,
@@ -52,23 +56,29 @@ import {
parseTokenAmount,
shortenAddress,
formatRate,
-} from '@/lib/stream-utils'
-import { explorerUrl } from '@/lib/stellar'
-import { useNetwork } from '@/components/providers/network-provider'
-import { useAutoWithdraw } from '@/hooks/use-auto-withdraw'
-import { UnlockChart } from '@/components/streams/unlock-chart'
-import { StreamTimeline } from '@/components/streams/stream-timeline'
-import { DownloadReceiptButton } from '@/components/streams/download-receipt-button'
-import { bumpStreamTtl } from '@/lib/contract'
+} from "@/lib/stream-utils";
+import { explorerUrl } from "@/lib/stellar";
+import { useNetwork } from "@/components/providers/network-provider";
+import { useAutoWithdraw } from "@/hooks/use-auto-withdraw";
+import { UnlockChart } from "@/components/streams/unlock-chart";
+import { StreamTimeline } from "@/components/streams/stream-timeline";
+import { DownloadReceiptButton } from "@/components/streams/download-receipt-button";
+import { bumpStreamTtl } from "@/lib/contract";
// ─── Address copy button ────────────────────────────────────────────────────
-function CopyableAddress({ address, href }: { address: string; href?: string }) {
- const [copied, setCopied] = useState(false)
+function CopyableAddress({
+ address,
+ href,
+}: {
+ address: string;
+ href?: string;
+}) {
+ const [copied, setCopied] = useState(false);
function copy() {
- navigator.clipboard.writeText(address)
- setCopied(true)
- setTimeout(() => setCopied(false), 1500)
+ navigator.clipboard.writeText(address);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1500);
}
return (
@@ -76,7 +86,9 @@ function CopyableAddress({ address, href }: { address: string; href?: string })
onClick={copy}
className="group inline-flex items-center gap-1.5 font-mono text-sm hover:text-primary transition-colors"
>
- {shortenAddress(address, 6)}
+
+ {shortenAddress(address, 6)}
+
{copied ? (
) : (
@@ -95,18 +107,24 @@ function CopyableAddress({ address, href }: { address: string; href?: string })
)}
- )
+ );
}
// ─── Detail row ─────────────────────────────────────────────────────────────
-function DetailRow({ label, children }: { label: string; children: React.ReactNode }) {
+function DetailRow({
+ label,
+ children,
+}: {
+ label: string;
+ children: React.ReactNode;
+}) {
return (
{label}
{children}
- )
+ );
}
// ─── Withdraw dialog ────────────────────────────────────────────────────────
@@ -118,40 +136,46 @@ function WithdrawDialog({
withdrawable,
token,
}: {
- open: boolean
- onClose: () => void
- streamId: string
- withdrawable: bigint
- token: { symbol: string; decimals: number; address: string }
+ open: boolean;
+ onClose: () => void;
+ streamId: string;
+ withdrawable: bigint;
+ token: { symbol: string; decimals: number; address: string };
}) {
- const { withdraw, pending, error } = useContract()
- const { network } = useNetwork()
- const [inputAmount, setInputAmount] = useState('')
- const [showFeeEstimate, setShowFeeEstimate] = useState(false)
+ const { withdraw, pending, error } = useContract();
+ const { network } = useNetwork();
+ const [inputAmount, setInputAmount] = useState("");
+ const [showFeeEstimate, setShowFeeEstimate] = useState(false);
- const max = formatTokenAmount(withdrawable, token.decimals, token.decimals)
- const parsed = inputAmount ? parseTokenAmount(inputAmount, token.decimals) : 0n
- const invalid = parsed <= 0n || parsed > withdrawable
+ const max = formatTokenAmount(withdrawable, token.decimals, token.decimals);
+ const parsed = inputAmount
+ ? parseTokenAmount(inputAmount, token.decimals)
+ : 0n;
+ const invalid = parsed <= 0n || parsed > withdrawable;
// Calculate estimated fees
- const estimatedFee = TYPICAL_FEES.withdraw.typical
- const feeBreakdown = calculateFeeBreakdown(estimatedFee)
- const withdrawFeeHigh = isHighFee(estimatedFee, TYPICAL_FEES.withdraw.typical)
+ const estimatedFee = TYPICAL_FEES.withdraw.typical;
+ const feeBreakdown = calculateFeeBreakdown(estimatedFee);
+ const withdrawFeeHigh = isHighFee(
+ estimatedFee,
+ TYPICAL_FEES.withdraw.typical,
+ );
async function handleWithdraw() {
try {
- const hash = await withdraw(streamId, parsed)
- toast.success('Withdrawal successful', {
+ const hash = await withdraw(streamId, parsed);
+ toast.success("Withdrawal successful", {
description: `${formatTokenAmount(parsed, token.decimals, 4)} ${token.symbol} sent to your wallet.`,
...(hash && {
action: {
- label: 'View transaction',
- onClick: () => window.open(explorerUrl(network, 'tx', hash), '_blank'),
+ label: "View transaction",
+ onClick: () =>
+ window.open(explorerUrl(network, "tx", hash), "_blank"),
},
}),
- })
- onClose()
- setInputAmount('')
+ });
+ onClose();
+ setInputAmount("");
} catch {
// error shown inline
}
@@ -159,12 +183,15 @@ function WithdrawDialog({
return (
<>
- !o && onClose()}>
+ !o && onClose()}
+ >
Withdraw funds
- Enter how much to withdraw. Max:{' '}
+ Enter how much to withdraw. Max:{" "}
{max} {token.symbol}
@@ -194,14 +221,19 @@ function WithdrawDialog({
{inputAmount && invalid && (
-
Amount exceeds withdrawable balance
+
+ Amount exceeds withdrawable balance
+
)}
{/* Fee info */}
- Estimated network fee: {(estimatedFee / 1e7).toFixed(7)} XLM
+ Estimated network fee:{" "}
+
+ {(estimatedFee / 1e7).toFixed(7)} XLM
+
Fee will be shown again before wallet confirmation.
@@ -210,12 +242,14 @@ function WithdrawDialog({
{error &&
{error}
}
- Cancel
+
+ Cancel
+
setShowFeeEstimate(true)}
disabled={pending || invalid || !inputAmount}
>
- {pending ? 'Withdrawing…' : 'Review fees'}
+ {pending ? "Withdrawing…" : "Review fees"}
@@ -233,7 +267,7 @@ function WithdrawDialog({
loading={pending}
/>
>
- )
+ );
}
// ─── Cancel dialog ───────────────────────────────────────────────────────────
@@ -243,33 +277,35 @@ function CancelDialog({
onClose,
streamId,
}: {
- open: boolean
- onClose: () => void
- streamId: string
+ open: boolean;
+ onClose: () => void;
+ streamId: string;
}) {
- const { cancel, pending, error } = useContract()
- const { network } = useNetwork()
- const router = useRouter()
- const [showFeeEstimate, setShowFeeEstimate] = useState(false)
+ const { cancel, pending, error } = useContract();
+ const { network } = useNetwork();
+ const router = useRouter();
+ const [showFeeEstimate, setShowFeeEstimate] = useState(false);
- const estimatedFee = TYPICAL_FEES.cancel.typical
- const feeBreakdown = calculateFeeBreakdown(estimatedFee)
- const cancelFeeHigh = isHighFee(estimatedFee, TYPICAL_FEES.cancel.typical)
+ const estimatedFee = TYPICAL_FEES.cancel.typical;
+ const feeBreakdown = calculateFeeBreakdown(estimatedFee);
+ const cancelFeeHigh = isHighFee(estimatedFee, TYPICAL_FEES.cancel.typical);
async function handleCancel() {
try {
- const hash = await cancel(streamId)
- toast.success('Stream cancelled', {
- description: 'Unlocked funds sent to recipient. Remainder returned to you.',
+ const hash = await cancel(streamId);
+ toast.success("Stream cancelled", {
+ description:
+ "Unlocked funds sent to recipient. Remainder returned to you.",
...(hash && {
action: {
- label: 'View transaction',
- onClick: () => window.open(explorerUrl(network, 'tx', hash), '_blank'),
+ label: "View transaction",
+ onClick: () =>
+ window.open(explorerUrl(network, "tx", hash), "_blank"),
},
}),
- })
- onClose()
- router.push('/app')
+ });
+ onClose();
+ router.push("/app");
} catch {
// error shown inline
}
@@ -277,7 +313,10 @@ function CancelDialog({
return (
<>
- !o && onClose()}>
+ !o && onClose()}
+ >
Cancel stream
@@ -290,19 +329,24 @@ function CancelDialog({
{/* Fee info */}
- Estimated network fee: {(estimatedFee / 1e7).toFixed(7)} XLM
+ Estimated network fee:{" "}
+
+ {(estimatedFee / 1e7).toFixed(7)} XLM
+
{error && {error}
}
- Keep stream
+
+ Keep stream
+
setShowFeeEstimate(true)}
disabled={pending}
>
- {pending ? 'Cancelling…' : 'Review & cancel'}
+ {pending ? "Cancelling…" : "Review & cancel"}
@@ -319,34 +363,60 @@ function CancelDialog({
loading={pending}
/>
>
- )
+ );
}
// ─── Auto-withdraw settings ─────────────────────────────────────────────────
const INTERVAL_OPTIONS = [
- { label: 'Every 6 hours', hours: 6 },
- { label: 'Every 12 hours', hours: 12 },
- { label: 'Every 24 hours', hours: 24 },
- { label: 'Every 48 hours', hours: 48 },
-] as const
+ { label: "Every 6 hours", hours: 6 },
+ { label: "Every 12 hours", hours: 12 },
+ { label: "Every 24 hours", hours: 24 },
+ { label: "Every 48 hours", hours: 48 },
+] as const;
function AutoWithdrawSection({
stream,
}: {
- stream: import('@/types/stream').StreamData
+ stream: import("@/types/stream").StreamData;
}) {
- const { settings, updateSettings, lastAutoWithdraw, autoWithdrawPending, withdrawalHistory } = useAutoWithdraw(stream)
- const [minDisplay, setMinDisplay] = useState('')
- const [maxDisplay, setMaxDisplay] = useState('')
- const [showHistory, setShowHistory] = useState(false)
-
- const STRATEGY_OPTIONS: Array<{ value: import('@/hooks/use-auto-withdraw').WithdrawStrategy; label: string; description: string }> = [
- { value: 'time-based', label: 'Time-based', description: 'Withdraw on fixed intervals' },
- { value: 'threshold-based', label: 'Threshold-based', description: 'Withdraw when amount reaches threshold' },
- { value: 'gas-optimized', label: 'Gas-optimized', description: 'Limit frequency to reduce gas costs' },
- { value: 'max', label: 'Max amount', description: 'Always withdraw maximum available' },
- ]
+ const {
+ settings,
+ updateSettings,
+ lastAutoWithdraw,
+ autoWithdrawPending,
+ withdrawalHistory,
+ } = useAutoWithdraw(stream);
+ const [minDisplay, setMinDisplay] = useState("");
+ const [maxDisplay, setMaxDisplay] = useState("");
+ const [showHistory, setShowHistory] = useState(false);
+
+ const STRATEGY_OPTIONS: Array<{
+ value: import("@/hooks/use-auto-withdraw").WithdrawStrategy;
+ label: string;
+ description: string;
+ }> = [
+ {
+ value: "time-based",
+ label: "Time-based",
+ description: "Withdraw on fixed intervals",
+ },
+ {
+ value: "threshold-based",
+ label: "Threshold-based",
+ description: "Withdraw when amount reaches threshold",
+ },
+ {
+ value: "gas-optimized",
+ label: "Gas-optimized",
+ description: "Limit frequency to reduce gas costs",
+ },
+ {
+ value: "max",
+ label: "Max amount",
+ description: "Always withdraw maximum available",
+ },
+ ];
return (
@@ -372,7 +442,8 @@ function AutoWithdrawSection({
{settings.enabled && (
- Automatically withdraw funds using your chosen strategy. The app must be open and your wallet connected.
+ Automatically withdraw funds using your chosen strategy. The app
+ must be open and your wallet connected.
@@ -384,20 +455,22 @@ function AutoWithdrawSection({
type="button"
onClick={() => updateSettings({ strategy: opt.value })}
className={
- 'w-full text-left p-3 rounded-lg border transition-colors ' +
+ "w-full text-left p-3 rounded-lg border transition-colors " +
(settings.strategy === opt.value
- ? 'border-primary bg-primary/10'
- : 'border-border hover:border-primary/50')
+ ? "border-primary bg-primary/10"
+ : "border-border hover:border-primary/50")
}
>
{opt.label}
-
{opt.description}
+
+ {opt.description}
+
))}
- {settings.strategy === 'time-based' && (
+ {settings.strategy === "time-based" && (
Frequency
@@ -407,10 +480,10 @@ function AutoWithdrawSection({
type="button"
onClick={() => updateSettings({ intervalHours: opt.hours })}
className={
- 'rounded-full border px-3 py-1 text-xs font-medium transition-colors ' +
+ "rounded-full border px-3 py-1 text-xs font-medium transition-colors " +
(settings.intervalHours === opt.hours
- ? 'border-primary bg-primary/10 text-primary'
- : 'border-border text-muted-foreground hover:border-primary hover:text-primary')
+ ? "border-primary bg-primary/10 text-primary"
+ : "border-border text-muted-foreground hover:border-primary hover:text-primary")
}
>
{opt.label}
@@ -420,7 +493,7 @@ function AutoWithdrawSection({
)}
- {settings.strategy === 'threshold-based' && (
+ {settings.strategy === "threshold-based" && (
Threshold (% of total deposited)
@@ -431,7 +504,11 @@ function AutoWithdrawSection({
min="1"
max="100"
value={settings.thresholdPercentage}
- onChange={(e) => updateSettings({ thresholdPercentage: parseInt(e.target.value) || 50 })}
+ onChange={(e) =>
+ updateSettings({
+ thresholdPercentage: parseInt(e.target.value) || 50,
+ })
+ }
className="max-w-48"
/>
@@ -449,16 +526,20 @@ function AutoWithdrawSection({
placeholder="0 (no minimum)"
value={minDisplay}
onChange={(e) => {
- setMinDisplay(e.target.value)
+ setMinDisplay(e.target.value);
const raw = e.target.value
- ? parseTokenAmount(e.target.value, stream.token.decimals).toString()
- : '0'
- updateSettings({ minAmountRaw: raw })
+ ? parseTokenAmount(
+ e.target.value,
+ stream.token.decimals,
+ ).toString()
+ : "0";
+ updateSettings({ minAmountRaw: raw });
}}
className="max-w-48"
/>
- Skip auto-withdraw if the available amount is below this threshold.
+ Skip auto-withdraw if the available amount is below this
+ threshold.
@@ -474,11 +555,14 @@ function AutoWithdrawSection({
placeholder="0 (no limit)"
value={maxDisplay}
onChange={(e) => {
- setMaxDisplay(e.target.value)
+ setMaxDisplay(e.target.value);
const raw = e.target.value
- ? parseTokenAmount(e.target.value, stream.token.decimals).toString()
- : '0'
- updateSettings({ maxSafetyLimitRaw: raw })
+ ? parseTokenAmount(
+ e.target.value,
+ stream.token.decimals,
+ ).toString()
+ : "0";
+ updateSettings({ maxSafetyLimitRaw: raw });
}}
className="max-w-48"
/>
@@ -492,7 +576,8 @@ function AutoWithdrawSection({
)}
{lastAutoWithdraw && (
- Last auto-withdrawal: {new Date(lastAutoWithdraw).toLocaleTimeString()}
+ Last auto-withdrawal:{" "}
+ {new Date(lastAutoWithdraw).toLocaleTimeString()}
)}
@@ -502,15 +587,25 @@ function AutoWithdrawSection({
onClick={() => setShowHistory(!showHistory)}
className="text-xs text-primary hover:underline"
>
- {showHistory ? 'Hide' : 'Show'} withdrawal history ({withdrawalHistory.length})
+ {showHistory ? "Hide" : "Show"} withdrawal history (
+ {withdrawalHistory.length})
{showHistory && (
{withdrawalHistory.map((entry, idx) => (
-
+
{new Date(entry.timestamp).toLocaleTimeString()}
-
- {entry.error ? `Error: ${entry.error}` : `Withdrew: ${entry.amount}`}
+
+ {entry.error
+ ? `Error: ${entry.error}`
+ : `Withdrew: ${entry.amount}`}
))}
@@ -521,46 +616,57 @@ function AutoWithdrawSection({
)}
- )
+ );
}
// ─── TTL warning ───────────────────────────────────────────────────────────
-const TTL_DAYS = 30
-const WARN_DAYS = 7
+const TTL_DAYS = 30;
+const WARN_DAYS = 7;
-function estimateDaysSinceLastWrite(stream: import('@/types/stream').StreamData, nowSeconds: number): number {
- const now = BigInt(nowSeconds)
- const lastWrite = stream.withdrawnAmount > 0n
- ? now
- : stream.cancelled
+function estimateDaysSinceLastWrite(
+ stream: import("@/types/stream").StreamData,
+ nowSeconds: number,
+): number {
+ const now = BigInt(nowSeconds);
+ const lastWrite =
+ stream.withdrawnAmount > 0n
? now
- : stream.startTime
- return Number(now - lastWrite) / 86400
+ : stream.cancelled
+ ? now
+ : stream.startTime;
+ return Number(now - lastWrite) / 86400;
}
-function TtlWarning({ stream, nowSeconds }: { stream: import('@/types/stream').StreamData; nowSeconds: number }) {
- const { address } = useWallet()
- const { network } = useNetwork()
- const [bumping, setBumping] = useState(false)
- const [bumped, setBumped] = useState(false)
+function TtlWarning({
+ stream,
+ nowSeconds,
+}: {
+ stream: import("@/types/stream").StreamData;
+ nowSeconds: number;
+}) {
+ const { address } = useWallet();
+ const { network } = useNetwork();
+ const [bumping, setBumping] = useState(false);
+ const [bumped, setBumped] = useState(false);
- const daysSinceWrite = estimateDaysSinceLastWrite(stream, nowSeconds)
- const estimatedDaysLeft = TTL_DAYS - daysSinceWrite
+ const daysSinceWrite = estimateDaysSinceLastWrite(stream, nowSeconds);
+ const estimatedDaysLeft = TTL_DAYS - daysSinceWrite;
- if (bumped || estimatedDaysLeft > WARN_DAYS || estimatedDaysLeft < 0) return null
+ if (bumped || estimatedDaysLeft > WARN_DAYS || estimatedDaysLeft < 0)
+ return null;
async function handleBump() {
- if (!address) return
- setBumping(true)
+ if (!address) return;
+ setBumping(true);
try {
- await bumpStreamTtl(network, stream.id, address)
- setBumped(true)
- toast.success('Storage TTL extended by 30 days')
+ await bumpStreamTtl(network, stream.id, address);
+ setBumped(true);
+ toast.success("Storage TTL extended by 30 days");
} catch {
- toast.error('Failed to extend TTL')
+ toast.error("Failed to extend TTL");
} finally {
- setBumping(false)
+ setBumping(false);
}
}
@@ -572,8 +678,10 @@ function TtlWarning({ stream, nowSeconds }: { stream: import('@/types/stream').S
Storage may be expiring soon
- This stream's on-chain data may expire in ~{Math.max(0, Math.floor(estimatedDaysLeft))} day{Math.floor(estimatedDaysLeft) !== 1 ? 's' : ''}.
- Extend the TTL to prevent data loss and keep the stream active.
+ This stream's on-chain data may expire in ~
+ {Math.max(0, Math.floor(estimatedDaysLeft))} day
+ {Math.floor(estimatedDaysLeft) !== 1 ? "s" : ""}. Extend the TTL to
+ prevent data loss and keep the stream active.
{address && (
- {bumping ? 'Extending…' : 'Extend TTL'}
+ {bumping ? "Extending…" : "Extend TTL"}
)}
- )
+ );
}
// ─── Rate display ──────────────────────────────────────────────────────────
-function RateDisplay({ stream }: { stream: import('@/types/stream').StreamData }) {
- const [expanded, setExpanded] = useState(false)
- const rate = formatRate(stream.amountPerSecond, stream.token.decimals, stream.token.symbol)
+function RateDisplay({
+ stream,
+}: {
+ stream: import("@/types/stream").StreamData;
+}) {
+ const [expanded, setExpanded] = useState(false);
+ const rate = formatRate(
+ stream.amountPerSecond,
+ stream.token.decimals,
+ stream.token.symbol,
+ );
return (
@@ -615,7 +731,7 @@ function RateDisplay({ stream }: { stream: import('@/types/stream').StreamData }
)}
- )
+ );
}
// ─── Stream detail skeleton ──────────────────────────────────────────────────
@@ -656,40 +772,46 @@ function StreamDetailSkeleton() {
{[0, 1, 2, 3, 4].map((i) => (
-
- )
+ );
}
// ─── Main page ───────────────────────────────────────────────────────────────
function ShareButtons({ streamId }: { streamId: string }) {
- const [copied, setCopied] = useState(false)
- const [showShare, setShowShare] = useState(false)
+ const [copied, setCopied] = useState(false);
+ const [showShare, setShowShare] = useState(false);
- const streamUrl = typeof window !== 'undefined' ? window.location.href : `https://flowstar.app/app/stream/${streamId}`
- const shareText = `Check out this token stream on FlowStar - Stream #${streamId}`
+ const streamUrl =
+ typeof window !== "undefined"
+ ? window.location.href
+ : `https://flowstar.app/app/stream/${streamId}`;
+ const shareText = `Check out this token stream on FlowStar - Stream #${streamId}`;
function copyLink() {
- navigator.clipboard.writeText(streamUrl)
- setCopied(true)
- setTimeout(() => setCopied(false), 1500)
- toast.success('Link copied to clipboard')
+ navigator.clipboard.writeText(streamUrl);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1500);
+ toast.success("Link copied to clipboard");
}
function shareToTwitter() {
- const url = `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}&url=${encodeURIComponent(streamUrl)}`
- window.open(url, '_blank', 'width=550,height=420')
+ const url = `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}&url=${encodeURIComponent(streamUrl)}`;
+ window.open(url, "_blank", "width=550,height=420");
}
function shareToTelegram() {
- const url = `https://t.me/share/url?url=${encodeURIComponent(streamUrl)}&text=${encodeURIComponent(shareText)}`
- window.open(url, '_blank', 'width=550,height=420')
+ const url = `https://t.me/share/url?url=${encodeURIComponent(streamUrl)}&text=${encodeURIComponent(shareText)}`;
+ window.open(url, "_blank", "width=550,height=420");
}
return (
@@ -711,8 +833,12 @@ function ShareButtons({ streamId }: { streamId: string }) {
className="inline-flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-secondary rounded transition-colors"
aria-label="Copy link"
>
- {copied ? : }
- {copied ? 'Copied!' : 'Copy link'}
+ {copied ? (
+
+ ) : (
+
+ )}
+ {copied ? "Copied!" : "Copy link"}
)}
- )
+ );
}
function ConnectPrompt() {
@@ -748,20 +874,20 @@ function ConnectPrompt() {
- )
+ );
}
function StreamDetail({ id }: { id: string }) {
- const { stream, loading } = useStream(id)
- const { address, isConnected } = useWallet()
- const { network, config } = useNetwork()
- const router = useRouter()
- const now = useNow(1000)
- const [withdrawOpen, setWithdrawOpen] = useState(false)
- const [cancelOpen, setCancelOpen] = useState(false)
+ const { stream, loading } = useStream(id);
+ const { address, isConnected } = useWallet();
+ const { network, config } = useNetwork();
+ const router = useRouter();
+ const now = useNow(1000);
+ const [withdrawOpen, setWithdrawOpen] = useState(false);
+ const [cancelOpen, setCancelOpen] = useState(false);
if (loading) {
- return
+ return ;
}
if (!stream) {
@@ -775,41 +901,50 @@ function StreamDetail({ id }: { id: string }) {
Back to dashboard
- )
+ );
}
- const status = getStreamStatus(stream, now)
- const progress = getStreamProgress(stream, now)
- const unlocked = getUnlockedAmount(stream, now)
- const withdrawable = getWithdrawableAmount(stream, now)
+ const status = getStreamStatus(stream, now);
+ const progress = getStreamProgress(stream, now);
+ const unlocked = getUnlockedAmount(stream, now);
+ const withdrawable = getWithdrawableAmount(stream, now);
const withdrawnFrac =
stream.depositedAmount > 0n
- ? Number((stream.withdrawnAmount * 10000n) / stream.depositedAmount) / 10000
- : 0
+ ? Number((stream.withdrawnAmount * 10000n) / stream.depositedAmount) /
+ 10000
+ : 0;
- const isRecipient = address === stream.recipient
- const isSender = address === stream.sender
- const canWithdraw = isRecipient && !stream.cancelled && withdrawable > 0n
- const canCancel = isSender && !stream.cancelled && status !== 'completed'
+ const isRecipient = address === stream.recipient;
+ const isSender = address === stream.sender;
+ const canWithdraw = isRecipient && !stream.cancelled && withdrawable > 0n;
+ const canCancel = isSender && !stream.cancelled && status !== "completed";
function handleDuplicate() {
- const durationSecs = Number(stream.endTime - stream.startTime)
- const cliffSecs = Number(stream.cliffTime - stream.startTime)
- const hasCliff = stream.cliffTime > stream.startTime
+ if (!stream) return;
+ const durationSecs = Number(stream.endTime - stream.startTime);
+ const cliffSecs = Number(stream.cliffTime - stream.startTime);
+ const hasCliff = stream.cliffTime > stream.startTime;
const params = new URLSearchParams({
clone: stream.id,
recipient: stream.recipient,
token: stream.token.address,
- amount: (Number(stream.depositedAmount) / Math.pow(10, stream.token.decimals)).toString(),
+ amount: (
+ Number(stream.depositedAmount) / Math.pow(10, stream.token.decimals)
+ ).toString(),
duration: durationSecs.toString(),
- })
+ });
if (hasCliff) {
- params.set('cliff', cliffSecs.toString())
+ params.set("cliff", cliffSecs.toString());
if (stream.cliffAmount > 0n) {
- params.set('cliffAmount', (Number(stream.cliffAmount) / Math.pow(10, stream.token.decimals)).toString())
+ params.set(
+ "cliffAmount",
+ (
+ Number(stream.cliffAmount) / Math.pow(10, stream.token.decimals)
+ ).toString(),
+ );
}
}
- router.push(`/app/create?${params.toString()}`)
+ router.push(`/app/create?${params.toString()}`);
}
return (
@@ -835,8 +970,10 @@ function StreamDetail({ id }: { id: string }) {
{isSender ? (
@@ -847,14 +984,16 @@ function StreamDetail({ id }: { id: string }) {
- {isSender ? 'Sending' : 'Receiving'}{' '}
+ {isSender ? "Sending" : "Receiving"}{" "}
-
Stream #{stream.id}
+
+ Stream #{stream.id}
+
@@ -871,8 +1010,10 @@ function StreamDetail({ id }: { id: string }) {
decimals={stream.token.decimals}
symbol={stream.token.symbol}
className="font-mono text-3xl font-semibold tabular-nums"
- isCompleted={status === 'completed'}
- isCliffReached={Number(stream.cliffTime) <= now && stream.cliffAmount > 0n}
+ isCompleted={status === "completed"}
+ isCliffReached={
+ Number(stream.cliffTime) <= now && stream.cliffAmount > 0n
+ }
/>
@@ -882,7 +1023,7 @@ function StreamDetail({ id }: { id: string }) {
{(progress * 100).toFixed(2)}% unlocked
@@ -892,31 +1033,39 @@ function StreamDetail({ id }: { id: string }) {
token={stream.token}
showSymbol={false}
maxFractionDigits={2}
- />{' '}
- / {" "}
+ /{" "}
+ {' '}
+ />{" "}
withdrawn
{/* Countdown */}
- {(status === 'streaming' || status === 'scheduled') && (
+ {(status === "streaming" || status === "scheduled") && (
- {status === 'scheduled' && (
+ {status === "scheduled" && (
)}
- {status === 'scheduled' ? 'Duration' : 'Ends in'}
+ {status === "scheduled" ? "Duration" : "Ends in"}
-
+
)}
@@ -927,18 +1076,15 @@ function StreamDetail({ id }: { id: string }) {
{canWithdraw && (
setWithdrawOpen(true)} className="gap-1.5">
- Withdraw{' '}
+ Withdraw{" "}
- {formatTokenAmount(withdrawable, stream.token.decimals, 2)}{' '}
+ {formatTokenAmount(withdrawable, stream.token.decimals, 2)}{" "}
{stream.token.symbol}
)}
{canCancel && (
- setCancelOpen(true)}
- >
+ setCancelOpen(true)}>
Cancel stream
)}
@@ -958,7 +1104,7 @@ function StreamDetail({ id }: { id: string }) {
{/* TTL warning */}
- {!stream.cancelled && status !== 'completed' && (
+ {!stream.cancelled && status !== "completed" && (
)}
@@ -973,17 +1119,23 @@ function StreamDetail({ id }: { id: string }) {
Details
-
+
-
+
{stream.token.symbol}
@@ -991,19 +1143,31 @@ function StreamDetail({ id }: { id: string }) {
)}
-
+
-
+
- 0n ? 'text-primary font-medium' : ''}>
-
+ 0n ? "text-primary font-medium" : ""}>
+
@@ -1013,7 +1177,11 @@ function StreamDetail({ id }: { id: string }) {
{formatDateTime(stream.startTime)}
- ({new Date(Number(stream.startTime) * 1000).toUTCString().replace(' GMT', ' UTC')})
+ (
+ {new Date(Number(stream.startTime) * 1000)
+ .toUTCString()
+ .replace(" GMT", " UTC")}
+ )
@@ -1022,7 +1190,13 @@ function StreamDetail({ id }: { id: string }) {
{formatDateTime(stream.cliffTime)}
{stream.cliffAmount > 0n && (
- (+ )
+ (+
+
+ )
)}
@@ -1031,7 +1205,11 @@ function StreamDetail({ id }: { id: string }) {
{formatDateTime(stream.endTime)}
- ({new Date(Number(stream.endTime) * 1000).toUTCString().replace(' GMT', ' UTC')})
+ (
+ {new Date(Number(stream.endTime) * 1000)
+ .toUTCString()
+ .replace(" GMT", " UTC")}
+ )
@@ -1044,7 +1222,7 @@ function StreamDetail({ id }: { id: string }) {
{/* Auto-withdraw (recipients only, active streams) */}
- {isRecipient && !stream.cancelled && status !== 'completed' && (
+ {isRecipient && !stream.cancelled && status !== "completed" && (
)}
@@ -1062,14 +1240,14 @@ function StreamDetail({ id }: { id: string }) {
streamId={stream.id}
/>
- )
+ );
}
export default function StreamPage({
params,
}: {
- params: Promise<{ id: string }>
+ params: Promise<{ id: string }>;
}) {
- const { id } = use(params)
- return
+ const { id } = use(params);
+ return ;
}
diff --git a/components/CommandPalette.tsx b/components/CommandPalette.tsx
deleted file mode 100644
index 26b3131..0000000
--- a/components/CommandPalette.tsx
+++ /dev/null
@@ -1,234 +0,0 @@
-'use client'
-
-import { Command } from 'cmdk'
-import { useEffect, useRef, useState } from 'react'
-import { useRouter } from 'next/navigation'
-
-const ACTIONS = [
- { id: 'dashboard', label: 'Go to Dashboard', shortcut: 'G D', href: '/app' },
- { id: 'create', label: 'Create New Stream', shortcut: 'G C', href: '/app/create' },
- { id: 'streams', label: 'View All Streams', shortcut: '', href: '/app/streams' },
- { id: 'analytics', label: 'Analytics', shortcut: '', href: '/app/analytics' },
- { id: 'help', label: 'Keyboard Shortcut Help', shortcut: 'Ctrl+/', href: '#help' },
-]
-
-function FocusTrap({
- children,
- onEscape,
-}: {
- children: React.ReactNode
- onEscape: () => void
-}) {
- const ref = useRef(null)
-
- useEffect(() => {
- const el = ref.current
- if (!el) return
-
- const focusable = () =>
- Array.from(
- el.querySelectorAll(
- 'a[href], button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])',
- ),
- )
-
- function handleKeyDown(e: KeyboardEvent) {
- if (e.key === 'Escape') {
- onEscape()
- return
- }
- if (e.key !== 'Tab') return
- const nodes = focusable()
- if (nodes.length === 0) return
- const first = nodes[0]
- const last = nodes[nodes.length - 1]
- if (e.shiftKey) {
- if (document.activeElement === first) {
- e.preventDefault()
- last.focus()
- }
- } else {
- if (document.activeElement === last) {
- e.preventDefault()
- first.focus()
- }
- }
- }
-
- el.addEventListener('keydown', handleKeyDown)
- return () => el.removeEventListener('keydown', handleKeyDown)
- }, [onEscape])
-
- return {children}
-}
-
-export function CommandPalette() {
- const [open, setOpen] = useState(false)
- const [showHelp, setShowHelp] = useState(false)
- const router = useRouter()
- const triggerRef = useRef(null)
-
- function closeAll() {
- setOpen(false)
- setShowHelp(false)
- ;(triggerRef.current as HTMLElement | null)?.focus()
- }
-
- useEffect(() => {
- const down = (e: KeyboardEvent) => {
- const tag = (e.target as HTMLElement).tagName
- if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tag)) return
-
- if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
- e.preventDefault()
- triggerRef.current = document.activeElement as HTMLElement
- setOpen((o) => !o)
- }
- if ((e.metaKey || e.ctrlKey) && e.key === '/') {
- e.preventDefault()
- triggerRef.current = document.activeElement as HTMLElement
- setShowHelp((o) => !o)
- }
- if ((e.metaKey || e.ctrlKey) && e.key === 'n') {
- e.preventDefault()
- router.push('/app/create')
- }
- }
-
- let gPressed = false
- const chord = (e: KeyboardEvent) => {
- const tag = (e.target as HTMLElement).tagName
- if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tag)) return
- if (e.key === 'g') {
- gPressed = true
- setTimeout(() => {
- gPressed = false
- }, 1000)
- return
- }
- if (gPressed) {
- if (e.key === 'd') router.push('/app')
- if (e.key === 'c') router.push('/app/create')
- gPressed = false
- }
- }
-
- window.addEventListener('keydown', down)
- window.addEventListener('keydown', chord)
- return () => {
- window.removeEventListener('keydown', down)
- window.removeEventListener('keydown', chord)
- }
- }, [router])
-
- return (
- <>
- {open && (
-
-
- e.stopPropagation()}
- className="bg-white dark:bg-gray-900 rounded-xl shadow-2xl w-full max-w-lg overflow-hidden"
- >
-
-
-
-
- No results found.
-
- {ACTIONS.map((action) => (
- {
- if (action.href !== '#help') {
- router.push(action.href)
- } else {
- setOpen(false)
- setShowHelp(true)
- return
- }
- closeAll()
- }}
- className="flex justify-between items-center px-3 py-2 rounded cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 text-sm"
- >
- {action.label}
- {action.shortcut && (
-
- {action.shortcut}
-
- )}
-
- ))}
-
-
-
-
-
- )}
-
- {showHelp && (
-
-
- e.stopPropagation()}
- className="bg-white dark:bg-gray-900 rounded-xl shadow-2xl p-6 max-w-sm w-full"
- >
-
-
- Keyboard Shortcuts
-
-
- ✕
-
-
-
-
- {[
- ['Cmd/Ctrl + K', 'Open command palette'],
- ['Cmd/Ctrl + N', 'Create new stream'],
- ['Cmd/Ctrl + /', 'Toggle this help'],
- ['G then D', 'Go to dashboard'],
- ['G then C', 'Go to create stream'],
- ['Esc', 'Close dialogs'],
- ].map(([key, desc]) => (
-
-
-
- {key}
-
-
- {desc}
-
- ))}
-
-
-
-
-
- )}
- >
- )
-}
diff --git a/components/NetworkMismatchModal.tsx b/components/NetworkMismatchModal.tsx
deleted file mode 100644
index d19425b..0000000
--- a/components/NetworkMismatchModal.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-'use client'
-
-import { useEffect, useRef } from 'react'
-import { getNetworkName, APP_NETWORK } from '@/lib/network'
-
-interface Props {
- walletNetwork: string
- onDismiss: () => void
-}
-
-export function NetworkMismatchModal({ walletNetwork, onDismiss }: Props) {
- const dismissRef = useRef(null)
- const linkRef = useRef(null)
- const containerRef = useRef(null)
-
- // Auto-focus dismiss button on mount, restore on close
- useEffect(() => {
- const previouslyFocused = document.activeElement as HTMLElement | null
- dismissRef.current?.focus()
- return () => {
- previouslyFocused?.focus()
- }
- }, [])
-
- // Escape key closes modal
- useEffect(() => {
- function handleKeyDown(e: KeyboardEvent) {
- if (e.key === 'Escape') {
- onDismiss()
- return
- }
- // Focus trap: cycle Tab within modal
- if (e.key === 'Tab' && containerRef.current) {
- const focusable = Array.from(
- containerRef.current.querySelectorAll(
- 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])',
- ),
- )
- if (focusable.length === 0) return
- const first = focusable[0]
- const last = focusable[focusable.length - 1]
- if (e.shiftKey) {
- if (document.activeElement === first) {
- e.preventDefault()
- last.focus()
- }
- } else {
- if (document.activeElement === last) {
- e.preventDefault()
- first.focus()
- }
- }
- }
- }
- document.addEventListener('keydown', handleKeyDown)
- return () => document.removeEventListener('keydown', handleKeyDown)
- }, [onDismiss])
-
- return (
-
-
-
- ⚠️ Wrong Network
-
-
- Your wallet is connected to {walletNetwork} . FlowStar
- requires {getNetworkName(APP_NETWORK)} .
-
-
- Please open Freighter, go to Settings → Network, and switch to{' '}
- {getNetworkName(APP_NETWORK)}.
-
-
-
-
- )
-}
diff --git a/components/layout/testnet-faucet-banner.tsx b/components/layout/testnet-faucet-banner.tsx
index b018cb4..5cf5971 100644
--- a/components/layout/testnet-faucet-banner.tsx
+++ b/components/layout/testnet-faucet-banner.tsx
@@ -1,15 +1,15 @@
-'use client'
+"use client";
-import { useState, useEffect } from 'react'
-import { AlertCircle, Loader2, Check, X } from 'lucide-react'
-import { Button } from '@/components/ui/button'
-import { useWallet } from '@/hooks/use-wallet'
-import { useNetwork } from '@/components/providers/network-provider'
-import { getXlmBalance } from '@/lib/stellar'
-import { toast } from 'sonner'
+import { useState, useEffect } from "react";
+import { AlertCircle, Loader2, Check, X } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { useWallet } from "@/hooks/use-wallet";
+import { useNetwork } from "@/components/providers/network-provider";
+import { getXlmBalance } from "@/lib/stellar";
+import { toast } from "sonner";
interface TestnetFaucetBannerProps {
- onClose?: () => void
+ onClose?: () => void;
}
/**
@@ -18,92 +18,98 @@ interface TestnetFaucetBannerProps {
* account via Friendbot.
*/
export function TestnetFaucetBanner({ onClose }: TestnetFaucetBannerProps) {
- const { address, isConnected } = useWallet()
- const { network } = useNetwork()
-
- const [xlmBalance, setXlmBalance] = useState(null)
- const [loading, setLoading] = useState(true)
- const [funding, setFunding] = useState(false)
- const [fundingStatus, setFundingStatus] = useState<'idle' | 'success' | 'error'>('idle')
- const [errorMessage, setErrorMessage] = useState('')
- const [dismissed, setDismissed] = useState(false)
+ const { address, isConnected } = useWallet();
+ const { network } = useNetwork();
+
+ const [xlmBalance, setXlmBalance] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [funding, setFunding] = useState(false);
+ const [fundingStatus, setFundingStatus] = useState<
+ "idle" | "success" | "error"
+ >("idle");
+ const [errorMessage, setErrorMessage] = useState("");
+ const [dismissed, setDismissed] = useState(false);
// Only show on testnet
- const isTestnet = network === 'testnet'
+ const isTestnet = network === "testnet";
// Fetch balance when address changes
useEffect(() => {
if (!address || !isConnected || !isTestnet) {
- setLoading(false)
- return
+ setLoading(false);
+ return;
}
- setLoading(true)
- getXlmBalance(address, 'testnet')
+ setLoading(true);
+ getXlmBalance(address, "testnet")
.then((balance) => {
- setXlmBalance(balance)
- setLoading(false)
+ setXlmBalance(balance);
+ setLoading(false);
})
.catch(() => {
- setLoading(false)
- })
- }, [address, isConnected, isTestnet])
+ setLoading(false);
+ });
+ }, [address, isConnected, isTestnet]);
// Hide if not applicable
if (dismissed || !isTestnet || !isConnected || !address || loading) {
- return null
+ return null;
}
// Hide if balance is not zero
- const isZeroBalance = xlmBalance === null || xlmBalance === 0n
+ const isZeroBalance = xlmBalance === null || xlmBalance === 0n;
if (!isZeroBalance) {
- return null
+ return null;
}
async function fundWithFriendbot() {
- setFunding(true)
- setFundingStatus('idle')
- setErrorMessage('')
+ if (!address) return;
+ setFunding(true);
+ setFundingStatus("idle");
+ setErrorMessage("");
try {
// Call Friendbot
- const friendbotUrl = `https://friendbot.stellar.org?addr=${encodeURIComponent(address)}`
- const response = await fetch(friendbotUrl)
+ const friendbotUrl = `https://friendbot.stellar.org?addr=${encodeURIComponent(address)}`;
+ const response = await fetch(friendbotUrl);
if (!response.ok) {
- const errorData = await response.text()
- throw new Error(errorData || 'Friendbot funding failed')
+ const errorData = await response.text();
+ throw new Error(errorData || "Friendbot funding failed");
}
// Wait a moment for the transaction to be confirmed
- await new Promise((resolve) => setTimeout(resolve, 3000))
+ await new Promise((resolve) => setTimeout(resolve, 3000));
// Refresh balance
- const newBalance = await getXlmBalance(address, 'testnet')
- setXlmBalance(newBalance)
- setFundingStatus('success')
+ const newBalance = await getXlmBalance(address, "testnet");
+ setXlmBalance(newBalance);
+ setFundingStatus("success");
- toast.success('Account funded!', {
- description: 'Your testnet account has been funded with test XLM from Friendbot.',
- })
+ toast.success("Account funded!", {
+ description:
+ "Your testnet account has been funded with test XLM from Friendbot.",
+ });
// Auto-dismiss after success
setTimeout(() => {
- setDismissed(true)
- onClose?.()
- }, 5000)
+ setDismissed(true);
+ onClose?.();
+ }, 5000);
} catch (error) {
- console.error('Friendbot funding error:', error)
- setFundingStatus('error')
+ console.error("Friendbot funding error:", error);
+ setFundingStatus("error");
const message =
- error instanceof Error ? error.message : 'Failed to fund account. Please try again.'
- setErrorMessage(message)
+ error instanceof Error
+ ? error.message
+ : "Failed to fund account. Please try again.";
+ setErrorMessage(message);
- toast.error('Funding failed', {
+ toast.error("Funding failed", {
description: message,
- })
+ });
} finally {
- setFunding(false)
+ setFunding(false);
}
}
@@ -116,17 +122,18 @@ export function TestnetFaucetBanner({ onClose }: TestnetFaucetBannerProps) {
Your testnet account needs funding
- Fund your account with test XLM using Friendbot to start creating streams.
+ Fund your account with test XLM using Friendbot to start creating
+ streams.
- {fundingStatus === 'success' && (
+ {fundingStatus === "success" && (
Account funded successfully! Reloading...
)}
- {fundingStatus === 'error' && (
+ {fundingStatus === "error" && (
{errorMessage}
@@ -143,7 +150,7 @@ export function TestnetFaucetBanner({ onClose }: TestnetFaucetBannerProps) {
className="gap-1.5 bg-amber-600 hover:bg-amber-700 text-white"
>
{funding && }
- {funding ? 'Funding...' : 'Fund with Friendbot'}
+ {funding ? "Funding..." : "Fund with Friendbot"}
- {' '}
- Friendbot may be rate-limited if you fund multiple accounts. If funding fails, wait a few
- minutes and try again.
+ {" "}
+ Friendbot may be rate-limited if you fund multiple accounts. If funding
+ fails, wait a few minutes and try again.
- )
+ );
}
diff --git a/components/providers/wallet-provider.tsx b/components/providers/wallet-provider.tsx
index 2e413f5..0eb3330 100644
--- a/components/providers/wallet-provider.tsx
+++ b/components/providers/wallet-provider.tsx
@@ -36,7 +36,6 @@ export const WALLET_OPTIONS: WalletOption[] = [
// ─── Wallet adapter interface ─────────────────────────────────────────────────
export interface WalletAdapter {
-interface WalletAdapter {
connect(): Promise;
signTransaction(xdr: string, networkPassphrase: string): Promise;
isAvailable(): boolean;
@@ -213,27 +212,6 @@ export const lobstrAdapter: WalletAdapter = {
return signedXdr;
}
return signWithLobstrWalletConnect(xdr, networkPassphrase);
-const lobstrAdapter: WalletAdapter = {
- isAvailable: () =>
- typeof window !== "undefined" && !!(window as any).lobstrSDK,
-
- async connect() {
- const sdk = (window as any).lobstrSDK;
- if (!sdk)
- throw new Error(
- "LOBSTR extension is not installed. Install it and refresh.",
- );
- const { publicKey } = await sdk.getPublicKey();
- if (!publicKey) throw new Error("LOBSTR did not return a public key.");
- return publicKey;
- },
-
- async signTransaction(xdr, networkPassphrase) {
- const sdk = (window as any).lobstrSDK;
- if (!sdk) throw new Error("LOBSTR extension is not installed.");
- const { signedXdr } = await sdk.signTransaction(xdr, { networkPassphrase });
- if (!signedXdr) throw new Error("LOBSTR signing failed.");
- return signedXdr;
},
};
@@ -263,19 +241,6 @@ const albedoAdapter: WalletAdapter = {
const albedo = (await import("@albedo-link/intent")).default;
const network = networkPassphrase.includes("Test") ? "testnet" : "public";
const result = await albedo.tx({ xdr, network, submit: false });
- const albedo = (await import("albedo-link")).default;
- const result = await albedo.publicKey({});
- if (!result?.pubkey) throw new Error("Albedo did not return a public key.");
- return result.pubkey;
- },
-
- async signTransaction(xdr, networkPassphrase) {
- const albedo = (await import("albedo-link")).default;
- const result = await albedo.tx({
- xdr,
- network: networkPassphrase,
- submit: false,
- });
if (!result?.signed_envelope_xdr) throw new Error("Albedo signing failed.");
return result.signed_envelope_xdr;
},
@@ -314,22 +279,6 @@ interface WalletContextValue {
const WalletContext = createContext(null);
// ─── Freighter network helpers ────────────────────────────────────────────────
-// ─── Wallet adapters ─────────────────────────────────────────────────────────
-
-async function connectFreighter(): Promise {
- const { isConnected, getAddress, requestAccess } =
- await import("@stellar/freighter-api");
- const connected = await isConnected();
- if (!connected.isConnected) {
- throw new Error(
- "Freighter is not installed. Please install the Freighter extension.",
- );
- }
- await requestAccess();
- const result = await getAddress();
- if (result.error) throw new Error(result.error);
- return result.address;
-}
async function getFreighterNetwork(): Promise {
try {
@@ -342,97 +291,6 @@ async function getFreighterNetwork(): Promise {
}
}
-async function signWithFreighter(
- xdr: string,
- networkPassphrase: string,
-): Promise {
- const { signTransaction } = await import("@stellar/freighter-api");
- const result = await signTransaction(xdr, { networkPassphrase });
- if (result.error) throw new Error(result.error);
- return result.signedTxXdr;
-}
-
-function passphraseToAlbedoNetwork(passphrase: string): "testnet" | "public" {
- if (passphrase.includes("Test")) return "testnet";
- return "public";
-}
-
-async function connectAlbedo(): Promise {
- const albedo = await import("@albedo-link/intent");
- try {
- const result = await albedo.default.publicKey({});
- return result.pubkey;
- } catch (err: unknown) {
- if (err instanceof Error && /popup/i.test(err.message)) {
- throw new Error(
- "Albedo popup was blocked. Please allow popups for this site and try again.",
- );
- }
- throw err;
- }
-}
-
-async function signWithAlbedo(
- xdr: string,
- networkPassphrase: string,
-): Promise {
- const albedo = await import("@albedo-link/intent");
- const result = await albedo.default.tx({
- xdr,
- network: passphraseToAlbedoNetwork(networkPassphrase),
- submit: false,
- });
- return result.signed_envelope_xdr;
-}
-
-async function connectXBull(): Promise {
- if (typeof window === "undefined" || !(window as any).xBullSDK) {
- throw new Error(
- "xBull is not installed. Install the xBull extension from https://xbull.app and refresh.",
- );
- }
- const sdk = (window as any).xBullSDK;
- const result = await sdk.connect();
- if (!result?.publicKey) throw new Error("xBull did not return a public key.");
- return result.publicKey;
-}
-
-async function signWithXBull(
- xdr: string,
- networkPassphrase: string,
-): Promise {
- if (typeof window === "undefined" || !(window as any).xBullSDK) {
- throw new Error("xBull is not installed.");
- }
- const sdk = (window as any).xBullSDK;
- const result = await sdk.signXDR(xdr, { networkPassphrase });
- if (!result?.signedXDR)
- throw new Error("xBull signing failed or was rejected.");
- return result.signedXDR;
-}
-
-// Stubs for wallets that need a dedicated SDK — shows a helpful message
-async function connectStub(name: string): Promise {
- throw new Error(
- `${name} connection requires the ${name} browser extension. Install it and refresh.`,
- );
-}
-
-async function connectWallet(id: string): Promise {
- switch (id) {
- case "freighter":
- return connectFreighter();
- case "xbull":
- return connectXBull();
- case "lobstr":
- return connectStub("LOBSTR");
- case "albedo":
- return connectAlbedo();
- default:
- throw new Error(`Unknown wallet: ${id}`);
- }
-}
-
// Maps Freighter network names → our NetworkName
function normalizeFreighterNetwork(raw: string): NetworkName | null {
const lower = raw.toLowerCase();
@@ -524,16 +382,6 @@ export function WalletProvider({ children }: { children: ReactNode }) {
if (!walletId) throw new Error("No wallet connected");
const config = getNetworkConfig(customNetwork ?? network);
return getAdapter(walletId).signTransaction(xdr, config.passphrase);
- switch (walletId) {
- case "freighter":
- return signWithFreighter(xdr, config.passphrase);
- case "xbull":
- return signWithXBull(xdr, config.passphrase);
- case "albedo":
- return signWithAlbedo(xdr, config.passphrase);
- default:
- throw new Error(`Signing not implemented for ${walletId}`);
- }
},
[walletId, network],
);
diff --git a/components/streams/dashboard-stats.tsx b/components/streams/dashboard-stats.tsx
index 9a29914..d3fbb83 100644
--- a/components/streams/dashboard-stats.tsx
+++ b/components/streams/dashboard-stats.tsx
@@ -1,15 +1,15 @@
-'use client'
+"use client";
-import { useNow } from '@/hooks/use-now'
-import { usePortfolioValue, formatUsd } from '@/hooks/use-token-price'
-import { useShowUsd } from '@/hooks/use-show-usd'
-import { getStreamStatus, getWithdrawableAmount } from '@/lib/stream-utils'
-import { TokenAmount } from '@/components/ui/token-amount'
-import type { StreamData } from '@/types/stream'
+import { useNow } from "@/hooks/use-now";
+import { usePortfolioValue, formatUsd } from "@/hooks/use-token-price";
+import { useShowUsd } from "@/hooks/use-show-usd";
+import { getStreamStatus, getWithdrawableAmount } from "@/lib/stream-utils";
+import { TokenAmount } from "@/components/ui/token-amount";
+import type { StreamData } from "@/types/stream";
interface DashboardStatsProps {
- sent: StreamData[]
- received: StreamData[]
+ sent: StreamData[];
+ received: StreamData[];
}
/**
@@ -19,59 +19,79 @@ interface DashboardStatsProps {
*/
export function DashboardStats({ sent, received }: DashboardStatsProps) {
const hasActiveStreams =
- sent.some((s) => !s.cancelled && Math.floor(Date.now() / 1000) < Number(s.endTime) && Math.floor(Date.now() / 1000) >= Number(s.startTime)) ||
- received.some((s) => !s.cancelled && Math.floor(Date.now() / 1000) < Number(s.endTime) && Math.floor(Date.now() / 1000) >= Number(s.startTime))
+ sent.some(
+ (s) =>
+ !s.cancelled &&
+ Math.floor(Date.now() / 1000) < Number(s.endTime) &&
+ Math.floor(Date.now() / 1000) >= Number(s.startTime),
+ ) ||
+ received.some(
+ (s) =>
+ !s.cancelled &&
+ Math.floor(Date.now() / 1000) < Number(s.endTime) &&
+ Math.floor(Date.now() / 1000) >= Number(s.startTime),
+ );
- const now = useNow(hasActiveStreams ? 1000 : 60000)
- const [showUsd] = useShowUsd()
- const { totalUsd, loading: priceLoading, stale } = usePortfolioValue([...sent, ...received])
+ const now = useNow(hasActiveStreams ? 1000 : 60000);
+ const [showUsd] = useShowUsd();
+ const {
+ totalUsd,
+ loading: priceLoading,
+ stale,
+ } = usePortfolioValue([...sent, ...received]);
const activeReceiving = received.filter(
- (s) => getStreamStatus(s, now) === 'streaming',
- ).length
+ (s) => getStreamStatus(s, now) === "streaming",
+ ).length;
const activeSending = sent.filter(
- (s) => getStreamStatus(s, now) === 'streaming',
- ).length
+ (s) => getStreamStatus(s, now) === "streaming",
+ ).length;
const withdrawableByToken = new Map<
string,
- { amount: bigint; token: StreamData['token'] }
- >()
+ { amount: bigint; token: StreamData["token"] }
+ >();
for (const s of received) {
- const amt = getWithdrawableAmount(s, now)
- const existing = withdrawableByToken.get(s.token.symbol)
- if (existing) existing.amount += amt
- else withdrawableByToken.set(s.token.symbol, { amount: amt, token: s.token })
+ const amt = getWithdrawableAmount(s, now);
+ const existing = withdrawableByToken.get(s.token.symbol);
+ if (existing) existing.amount += amt;
+ else
+ withdrawableByToken.set(s.token.symbol, { amount: amt, token: s.token });
}
const topWithdrawable = [...withdrawableByToken.values()].sort((a, b) =>
a.amount > b.amount ? -1 : 1,
- )[0]
+ )[0];
- const usdValue = showUsd ? (
const usdDisplay = showUsd ? (
priceLoading ? (
) : totalUsd !== null ? (
{formatUsd(totalUsd)}
- {stale && ⚠️ }
+ {stale && (
+
+ ⚠️
+
+ )}
) : (
—
)
) : (
—
- )
+ );
const stats = [
{
- label: 'Total streaming value',
- value: usdValue,
+ label: "Total streaming value",
value: usdDisplay,
- hint: showUsd && totalUsd !== null ? 'locked across all streams' : 'enable USD in settings',
+ hint:
+ showUsd && totalUsd !== null
+ ? "locked across all streams"
+ : "enable USD in settings",
},
{
- label: 'Available to withdraw',
+ label: "Available to withdraw",
value: topWithdrawable ? (
1
- ? `+${withdrawableByToken.size - 1} more token${withdrawableByToken.size > 2 ? 's' : ''}`
- : 'across received streams',
+ ? `+${withdrawableByToken.size - 1} more token${withdrawableByToken.size > 2 ? "s" : ""}`
+ : "across received streams",
},
{
- label: 'Receiving',
+ label: "Receiving",
value: {received.length} ,
hint: `${activeReceiving} streaming now`,
},
{
- label: 'Sending',
+ label: "Sending",
value: {sent.length} ,
hint: `${activeSending} streaming now`,
},
- ]
+ ];
return (
{stats.map((stat) => (
-
+
{stat.label}
{stat.value}
@@ -110,7 +133,7 @@ export function DashboardStats({ sent, received }: DashboardStatsProps) {
))}
- )
+ );
}
// ─── Skeleton ────────────────────────────────────────────────────────────────
@@ -119,12 +142,15 @@ export function DashboardStatsSkeleton() {
return (
{[0, 1, 2, 3].map((i) => (
-
- )
+ );
}
diff --git a/components/streams/stream-card.tsx b/components/streams/stream-card.tsx
index 07fdef2..193e5fa 100644
--- a/components/streams/stream-card.tsx
+++ b/components/streams/stream-card.tsx
@@ -1,62 +1,72 @@
-'use client'
+"use client";
-import { memo } from 'react'
-import Link from 'next/link'
-import { ArrowDownLeft, ArrowUpRight } from 'lucide-react'
-import { useNow } from '@/hooks/use-now'
-import { useWallet } from '@/hooks/use-wallet'
-import { useTokenPrice, formatUsd } from '@/hooks/use-token-price'
-import { useShowUsd } from '@/hooks/use-show-usd'
+import { memo } from "react";
+import Link from "next/link";
+import { ArrowDownLeft, ArrowUpRight } from "lucide-react";
+import { useNow } from "@/hooks/use-now";
+import { useWallet } from "@/hooks/use-wallet";
+import { useTokenPrice, formatUsd } from "@/hooks/use-token-price";
+import { useShowUsd } from "@/hooks/use-show-usd";
import {
getStreamProgress,
getStreamStatus,
formatTokenAmount,
shortenAddress,
formatRate,
-} from '@/lib/stream-utils'
-import { ProgressBar } from '@/components/ui/progress-bar'
-import { TokenAmount } from '@/components/ui/token-amount'
-import { CountdownTimer } from '@/components/ui/countdown-timer'
-import { AccessibleCountdownTimer } from '@/components/ui/accessible-countdown-timer'
-import { StreamStatusBadge } from '@/components/streams/stream-status-badge'
-import type { StreamData } from '@/types/stream'
+} from "@/lib/stream-utils";
+import { ProgressBar } from "@/components/ui/progress-bar";
+import { TokenAmount } from "@/components/ui/token-amount";
+import { CountdownTimer } from "@/components/ui/countdown-timer";
+import { AccessibleCountdownTimer } from "@/components/ui/accessible-countdown-timer";
+import { StreamStatusBadge } from "@/components/streams/stream-status-badge";
+import type { StreamData } from "@/types/stream";
// Pick update interval based on a quick pre-check of stream state.
// completed/cancelled streams never change — no interval needed.
// scheduled streams only need minute-level updates for the countdown.
// streaming streams need per-second updates for the live counter.
function getInterval(stream: StreamData): number | null {
- const nowSec = Math.floor(Date.now() / 1000)
- if (stream.cancelled) return null
- if (nowSec >= Number(stream.endTime)) return null // completed
- if (nowSec < Number(stream.startTime)) return 60000 // scheduled: 1 min
- return 1000 // streaming: 1 sec
+ const nowSec = Math.floor(Date.now() / 1000);
+ if (stream.cancelled) return null;
+ if (nowSec >= Number(stream.endTime)) return null; // completed
+ if (nowSec < Number(stream.startTime)) return 60000; // scheduled: 1 min
+ return 1000; // streaming: 1 sec
}
function StreamCardInner({ stream }: { stream: StreamData }) {
- const interval = getInterval(stream)
- const now = useNow(interval)
- const { address } = useWallet()
- const { usdPrice } = useTokenPrice(stream.token.symbol)
- const [showUsd] = useShowUsd()
- const status = getStreamStatus(stream, now)
- const progress = getStreamProgress(stream, now)
+ const interval = getInterval(stream);
+ const now = useNow(interval);
+ const { address } = useWallet();
+ const { usdPrice } = useTokenPrice(stream.token.symbol);
+ const [showUsd] = useShowUsd();
+ const status = getStreamStatus(stream, now);
+ const progress = getStreamProgress(stream, now);
const withdrawnFrac =
stream.depositedAmount > 0n
- ? Number((stream.withdrawnAmount * 10000n) / stream.depositedAmount) / 10000
- : 0
+ ? Number((stream.withdrawnAmount * 10000n) / stream.depositedAmount) /
+ 10000
+ : 0;
- const rate = formatRate(stream.amountPerSecond, stream.token.decimals, stream.token.symbol)
- const isOutgoing = address === stream.sender
- const counterparty = isOutgoing ? stream.recipient : stream.sender
- const direction = isOutgoing ? 'Sending' : 'Receiving'
- const displayAmount = formatTokenAmount(stream.depositedAmount, stream.token.decimals, 2)
- const ariaLabel = `${direction} ${displayAmount} ${stream.token.symbol}, ${status}, ${(progress * 100).toFixed(0)}% unlocked`
+ const rate = formatRate(
+ stream.amountPerSecond,
+ stream.token.decimals,
+ stream.token.symbol,
+ );
+ const isOutgoing = address === stream.sender;
+ const counterparty = isOutgoing ? stream.recipient : stream.sender;
+ const direction = isOutgoing ? "Sending" : "Receiving";
+ const displayAmount = formatTokenAmount(
+ stream.depositedAmount,
+ stream.token.decimals,
+ 2,
+ );
+ const ariaLabel = `${direction} ${displayAmount} ${stream.token.symbol}, ${status}, ${(progress * 100).toFixed(0)}% unlocked`;
const usdValue =
showUsd && usdPrice !== null
- ? (Number(stream.depositedAmount) / Math.pow(10, stream.token.decimals)) * usdPrice
- : null
+ ? (Number(stream.depositedAmount) / Math.pow(10, stream.token.decimals)) *
+ usdPrice
+ : null;
return (
{isOutgoing ? (
@@ -82,7 +92,8 @@ function StreamCardInner({ stream }: { stream: StreamData }) {
- {stream.metadata?.name ?? (isOutgoing ? 'Sending to' : 'Receiving from')}
+ {stream.metadata?.name ??
+ (isOutgoing ? "Sending to" : "Receiving from")}
{shortenAddress(counterparty, 5)}
@@ -102,38 +113,42 @@ function StreamCardInner({ stream }: { stream: StreamData }) {
maxFractionDigits={2}
/>
{usdValue !== null && (
-
{formatUsd(usdValue)}
+
+ {formatUsd(usdValue)}
+
)}
- {status === 'scheduled'
- ? 'Starts in'
- : status === 'completed' || status === 'cancelled'
- ? 'Ended'
- : 'Ends in'}
+ {status === "scheduled"
+ ? "Starts in"
+ : status === "completed" || status === "cancelled"
+ ? "Ended"
+ : "Ends in"}
-
- {status === 'scheduled' ? (
+
+ {status === "scheduled" ? (
- ) : status === 'completed' || status === 'cancelled' ? (
+ ) : status === "completed" || status === "cancelled" ? (
—
) : (
)}
-
+
- {(status === 'streaming' || status === 'scheduled') && (
-
{rate.best}
+ {(status === "streaming" || status === "scheduled") && (
+
+ {rate.best}
+
)}
{(progress * 100).toFixed(1)}% unlocked
@@ -143,16 +158,16 @@ function StreamCardInner({ stream }: { stream: StreamData }) {
token={stream.token}
showSymbol={false}
maxFractionDigits={2}
- />{' '}
+ />{" "}
withdrawn
- )
+ );
}
-export const StreamCard = memo(StreamCardInner)
+export const StreamCard = memo(StreamCardInner);
// ─── Skeleton ────────────────────────────────────────────────────────────────
@@ -187,5 +202,5 @@ export function StreamCardSkeleton() {
- )
+ );
}
diff --git a/contracts/streaming/src/bench.rs b/contracts/streaming/src/bench.rs
index 34f71de..fe5fa67 100644
--- a/contracts/streaming/src/bench.rs
+++ b/contracts/streaming/src/bench.rs
@@ -20,14 +20,11 @@ use soroban_sdk::{
/// Resets budget, runs `f`, then prints CPU + memory costs.
fn measure(env: &Env, label: &str, f: impl FnOnce()) {
- env.budget().reset_default();
+ env.cost_estimate().budget().reset_default();
f();
- let cpu = env.budget().cpu_instruction_cost();
- let mem = env.budget().memory_bytes_cost();
- std::println!(
- "[BENCH] {:<50} cpu={:>12} mem={:>10}",
- label, cpu, mem
- );
+ let cpu = env.cost_estimate().budget().cpu_instruction_cost();
+ let mem = env.cost_estimate().budget().memory_bytes_cost();
+ std::println!("[BENCH] {:<50} cpu={:>12} mem={:>10}", label, cpu, mem);
// Flag operations that approach the Soroban limit (100M cpu instructions)
if cpu > 50_000_000 {
@@ -59,17 +56,25 @@ impl BenchEnv {
let recipient = Address::generate(&env);
let token_admin = Address::generate(&env);
- let token_id = env.register_stellar_asset_contract_v2(token_admin.clone()).address();
+ let token_id = env
+ .register_stellar_asset_contract_v2(token_admin.clone())
+ .address();
StellarAssetClient::new(&env, &token_id).mint(&sender, &1_000_000_000_0000000);
- BenchEnv { env, contract_id, token_id, sender, recipient }
+ BenchEnv {
+ env,
+ contract_id,
+ token_id,
+ sender,
+ recipient,
+ }
}
- fn client(&self) -> StreamingContractClient {
+ fn client(&self) -> StreamingContractClient<'_> {
StreamingContractClient::new(&self.env, &self.contract_id)
}
- fn token(&self) -> TokenClient {
+ fn token(&self) -> TokenClient<'_> {
TokenClient::new(&self.env, &self.token_id)
}
@@ -250,9 +255,13 @@ fn bench_get_sent_streams_pagination() {
measure(&b.env, "get_sent_streams (100 streams, full page)", || {
b.client().get_sent_streams(&b.sender, &0, &100);
});
- measure(&b.env, "get_sent_streams (100 streams, page offset 50)", || {
- b.client().get_sent_streams(&b.sender, &50, &50);
- });
+ measure(
+ &b.env,
+ "get_sent_streams (100 streams, page offset 50)",
+ || {
+ b.client().get_sent_streams(&b.sender, &50, &50);
+ },
+ );
}
#[test]
@@ -265,20 +274,32 @@ fn bench_get_received_streams_pagination() {
for _ in 0..10 {
b.create_default(now);
}
- measure(&b.env, "get_received_streams (10 streams, full page)", || {
- b.client().get_received_streams(&b.recipient, &0, &100);
- });
+ measure(
+ &b.env,
+ "get_received_streams (10 streams, full page)",
+ || {
+ b.client().get_received_streams(&b.recipient, &0, &100);
+ },
+ );
// Create 90 more for 100 total
for _ in 0..90 {
b.create_default(now);
}
- measure(&b.env, "get_received_streams (100 streams, full page)", || {
- b.client().get_received_streams(&b.recipient, &0, &100);
- });
- measure(&b.env, "get_received_streams (100 streams, page offset 50)", || {
- b.client().get_received_streams(&b.recipient, &50, &50);
- });
+ measure(
+ &b.env,
+ "get_received_streams (100 streams, full page)",
+ || {
+ b.client().get_received_streams(&b.recipient, &0, &100);
+ },
+ );
+ measure(
+ &b.env,
+ "get_received_streams (100 streams, page offset 50)",
+ || {
+ b.client().get_received_streams(&b.recipient, &50, &50);
+ },
+ );
}
#[test]
diff --git a/contracts/streaming/src/lib.rs b/contracts/streaming/src/lib.rs
index 0c39c7b..5fede6f 100644
--- a/contracts/streaming/src/lib.rs
+++ b/contracts/streaming/src/lib.rs
@@ -41,10 +41,13 @@
//! called to restore its TTL.
#![no_std]
+// Amounts are grouped to separate whole units from the 7-decimal-place
+// fractional part (e.g. `1_000_0000000` = 1000 units), not by strict
+// thousands — clearer for this domain than clippy's default suggestion.
+#![allow(clippy::inconsistent_digit_grouping)]
use soroban_sdk::{
- contract, contracterror, contractimpl, contracttype, token, Address, Env, Vec,
- contract, contractimpl, contracttype, token, Address, BytesN, Env, Vec,
+ contract, contracterror, contractimpl, contracttype, token, Address, BytesN, Env, Vec,
};
// ─── Constants ───────────────────────────────────────────────────────────────
@@ -101,12 +104,15 @@ pub enum DataKey {
ArchiveSentBy(Address),
/// Archived (completed/cancelled) stream IDs where address is the recipient.
ArchiveReceivedBy(Address),
+
+ /// Optional withdrawal delegate for a stream. Stored in Persistent.
+ Delegate(u64),
}
// ─── Types ───────────────────────────────────────────────────────────────────
#[contracttype]
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, PartialEq)]
pub struct Stream {
pub id: u64,
pub sender: Address,
@@ -131,12 +137,10 @@ pub struct Stream {
pub cancelled: bool,
pub linear_amount: i128,
pub duration: i128,
- /// Optional metadata attached to this stream.
- pub metadata: Option,
}
#[contracttype]
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, PartialEq)]
pub struct StreamMetadata {
pub name: soroban_sdk::String,
pub category: soroban_sdk::String,
@@ -153,7 +157,6 @@ pub struct CreateStreamParams {
pub end_time: u64,
pub cliff_time: u64,
pub cliff_amount: i128,
- pub metadata: Option,
}
/// Input parameters for a single stream in a batch creation call.
@@ -287,7 +290,9 @@ impl StreamingContract {
env.storage().instance().set(&DataKey::Admin, &admin);
env.storage().instance().set(&DataKey::Paused, &false);
- env.storage().instance().extend_ttl(INSTANCE_TTL_LEDGERS, INSTANCE_TTL_LEDGERS);
+ env.storage()
+ .instance()
+ .extend_ttl(INSTANCE_TTL_LEDGERS, INSTANCE_TTL_LEDGERS);
}
// ── Admin: Pause/Unpause ─────────────────────────────────────────────────
@@ -302,27 +307,38 @@ impl StreamingContract {
admin.require_auth();
env.storage().instance().set(&DataKey::Paused, &true);
- env.storage().instance().extend_ttl(INSTANCE_TTL_LEDGERS, INSTANCE_TTL_LEDGERS);
+ env.storage()
+ .instance()
+ .extend_ttl(INSTANCE_TTL_LEDGERS, INSTANCE_TTL_LEDGERS);
- PauseEvent { timestamp: env.ledger().timestamp() }.publish(&env);
+ PauseEvent {
+ timestamp: env.ledger().timestamp(),
+ }
+ .publish(&env);
}
/// Unpause all write operations (admin only).
pub fn unpause(env: Env) {
let admin: Address = env
- // ── Write: Admin / Upgrade ───────────────────────────────────────────────
+ .storage()
+ .instance()
+ .get(&DataKey::Admin)
+ .unwrap_or_else(|| panic!("not initialized"));
+ admin.require_auth();
- /// Initialize the contract with an admin address.
- /// Can only be called once.
- pub fn initialize(env: Env, admin: Address) {
- if env.storage().instance().has(&DataKey::Admin) {
- panic!("already initialized");
- }
- env.storage().instance().set(&DataKey::Admin, &admin);
env.storage().instance().set(&DataKey::Paused, &false);
- env.storage().instance().extend_ttl(INSTANCE_TTL_LEDGERS, INSTANCE_TTL_LEDGERS);
+ env.storage()
+ .instance()
+ .extend_ttl(INSTANCE_TTL_LEDGERS, INSTANCE_TTL_LEDGERS);
+
+ UnpauseEvent {
+ timestamp: env.ledger().timestamp(),
+ }
+ .publish(&env);
}
+ // ── Write: Admin / Upgrade ───────────────────────────────────────────────
+
/// Upgrade the contract wasm. Only callable by the admin.
pub fn upgrade(env: Env, admin: Address, new_wasm_hash: BytesN<32>) {
admin.require_auth();
@@ -336,42 +352,15 @@ impl StreamingContract {
/// Post-upgrade data migration hook. Call this after an upgrade to
/// migrate storage layouts.
pub fn migrate(env: Env) {
- let _admin: Address = env
+ let admin: Address = env
.storage()
.instance()
.get(&DataKey::Admin)
.unwrap_or_else(|| panic!("not initialized"));
-
- // By default, unfreeze after wasm upgrade.
- env.storage().instance().set(&DataKey::Paused, &false);
- }
-
- // ── Admin: Freeze / Unfreeze ─────────────────────────────────────────
-
- /// Admin freezes the contract to prevent new stream creation.
- pub fn freeze(env: Env, admin: Address) {
- admin.require_auth();
- Self::require_admin(&env, &admin);
- env.storage().instance().set(&DataKey::Paused, &true);
- }
-
- /// Admin freezes the contract to prevent new stream creation.
- /// Alias kept for compatibility.
- pub fn pause(env: Env, admin: Address) {
- Self::freeze(env, admin);
- }
-
- /// Admin unfreezes the contract to allow new stream creation.
- pub fn unfreeze(env: Env, admin: Address) {
- admin.require_auth();
- Self::require_admin(&env, &admin);
- env.storage().instance().set(&DataKey::Paused, &false);
admin.require_auth();
+ // By default, unfreeze after wasm upgrade.
env.storage().instance().set(&DataKey::Paused, &false);
- env.storage().instance().extend_ttl(INSTANCE_TTL_LEDGERS, INSTANCE_TTL_LEDGERS);
-
- UnpauseEvent { timestamp: env.ledger().timestamp() }.publish(&env);
}
// ─── Helpers ──────────────────────────────────────────────────────────────
@@ -386,13 +375,6 @@ impl StreamingContract {
panic!("contract is paused");
}
}
- }
-
- /// Return the current contract version.
- pub fn version(env: Env) -> u32 {
- let _ = env;
- 1
- }
// ── Write: Create ────────────────────────────────────────────────────────
@@ -436,8 +418,8 @@ impl StreamingContract {
let duration_i128 = duration as i128;
let linear_amount = params.total_amount - params.cliff_amount;
- let amount_per_second = if duration > 0 {
- linear_amount / duration
+ let amount_per_second = if duration_i128 > 0 {
+ linear_amount / duration_i128
} else {
0
};
@@ -446,7 +428,6 @@ impl StreamingContract {
if amount_per_second == 0 && linear_amount > 0 {
panic!("stream amount too small for duration — rate would be 0");
}
- let amount_per_second = if duration_i128 > 0 { linear_amount / duration_i128 } else { 0 };
// ── Pull funds from sender into contract ─────────────────────────────
let token_client = token::Client::new(&env, ¶ms.token);
@@ -464,7 +445,7 @@ impl StreamingContract {
id,
sender: sender.clone(),
recipient: params.recipient.clone(),
- token: params.token,
+ token: params.token.clone(),
deposited_amount: params.total_amount,
withdrawn_amount: 0,
start_time: params.start_time,
@@ -474,8 +455,7 @@ impl StreamingContract {
amount_per_second,
cancelled: false,
linear_amount,
- duration,
- metadata: params.metadata.clone(),
+ duration: duration_i128,
};
// ── Persist stream ───────────────────────────────────────────────────
@@ -483,29 +463,16 @@ impl StreamingContract {
.persistent()
.set(&DataKey::Stream(id), &stream);
- // ── Persist metadata separately if provided ──────────────────────────
- if let Some(ref meta) = params.metadata {
- env.storage()
- .persistent()
- .set(&DataKey::StreamMetadata(id), meta);
- env.storage().persistent().extend_ttl(
- &DataKey::StreamMetadata(id),
- 518_400,
- 518_400,
- );
- }
-
Self::extend_stream_ttl(&env, id);
// ── Update sender index ──────────────────────────────────────────────
- Self::push_to_index(&env, DataKey::SentBy(sender), id);
+ Self::push_to_index(&env, DataKey::SentBy(sender.clone()), id);
// ── Update recipient index ───────────────────────────────────────────
- Self::push_to_index(&env, DataKey::ReceivedBy(params.recipient), id);
+ Self::push_to_index(&env, DataKey::ReceivedBy(params.recipient.clone()), id);
StreamCreatedEvent {
stream_id: id,
- deposited_amount: stream.deposited_amount,
sender: sender.clone(),
recipient: params.recipient.clone(),
token: params.token.clone(),
@@ -586,7 +553,11 @@ impl StreamingContract {
// Validate rate would be non-zero when linear amount > 0
let linear_amount = input.total_amount - input.cliff_amount;
let duration_i128 = duration as i128;
- let amount_per_second = if duration_i128 > 0 { linear_amount / duration_i128 } else { 0 };
+ let amount_per_second = if duration_i128 > 0 {
+ linear_amount / duration_i128
+ } else {
+ 0
+ };
if amount_per_second == 0 && linear_amount > 0 {
panic!("stream amount too small for duration — rate would be 0");
}
@@ -599,7 +570,11 @@ impl StreamingContract {
let duration = input.end_time - input.start_time;
let duration_i128 = duration as i128;
let linear_amount = input.total_amount - input.cliff_amount;
- let amount_per_second = if duration_i128 > 0 { linear_amount / duration_i128 } else { 0 };
+ let amount_per_second = if duration_i128 > 0 {
+ linear_amount / duration_i128
+ } else {
+ 0
+ };
// Pull funds from sender into contract
let token_client = token::Client::new(&env, &input.token);
@@ -627,10 +602,11 @@ impl StreamingContract {
cancelled: false,
linear_amount,
duration: duration_i128,
- metadata: None,
};
- env.storage().persistent().set(&DataKey::Stream(id), &stream);
+ env.storage()
+ .persistent()
+ .set(&DataKey::Stream(id), &stream);
Self::extend_stream_ttl(&env, id);
Self::push_to_index(&env, DataKey::SentBy(sender.clone()), id);
@@ -656,7 +632,6 @@ impl StreamingContract {
}
// ── Write: Transfer ──────────────────────────────────────────────────────
- // ── Write: Transfer ────────────────────────────────────────────────────────
/// Transfer a token stream right to a new address.
pub fn transfer_stream(
@@ -669,7 +644,6 @@ impl StreamingContract {
let old_recipient = stream.recipient.clone();
Self::require_not_paused(&env);
- let old_recipient = stream.recipient;
if stream.cancelled {
return Err(StreamError::StreamCancelled);
@@ -688,18 +662,17 @@ impl StreamingContract {
Self::remove_from_index(&env, DataKey::ReceivedBy(old_recipient.clone()), stream_id);
Self::push_to_index(&env, DataKey::ReceivedBy(new_recipient.clone()), stream_id);
+ // Clear delegate on transfer
+ env.storage()
+ .persistent()
+ .remove(&DataKey::Delegate(stream_id));
+
StreamTransferEvent {
stream_id,
old_recipient,
new_recipient,
}
.publish(&env);
- // Clear delegate on transfer
- env.storage()
- .persistent()
- .remove(&DataKey::Delegate(stream_id));
-
- StreamTransferEvent { stream_id, old_recipient, new_recipient }.publish(&env);
Self::extend_stream_ttl(&env, stream_id);
Ok(())
@@ -714,11 +687,7 @@ impl StreamingContract {
///
/// The caller must have approved this contract to spend `additional_amount`
/// of the stream's token before calling.
- pub fn top_up(
- env: Env,
- stream_id: u64,
- additional_amount: i128,
- ) -> Result<(), StreamError> {
+ pub fn top_up(env: Env, stream_id: u64, additional_amount: i128) -> Result<(), StreamError> {
let mut stream = Self::load_stream(&env, stream_id)?;
stream.sender.require_auth();
Self::require_not_paused(&env);
@@ -745,36 +714,65 @@ impl StreamingContract {
&additional_amount,
);
- // ── Recalculate rate over remaining duration ──────────────────────────
- let remaining_seconds = (stream.end_time - now) as i128;
-
- let already_vested = Self::vested_amount(&stream, now);
- let remaining_deposited = stream
- .deposited_amount
- .checked_sub(already_vested)
- .expect("deposited < vested — invariant broken");
-
- let new_remaining = remaining_deposited
- .checked_add(additional_amount)
- .expect("remaining + additional overflow");
-
- let new_amount_per_second = if remaining_seconds > 0 {
- new_remaining / remaining_seconds
- } else {
- 0
- };
-
- // Security: Reject dust streams with zero rate after top-up
- if new_amount_per_second == 0 && new_remaining > 0 {
- panic!("stream amount too small for remaining duration — rate would be 0");
- }
-
stream.deposited_amount = stream
.deposited_amount
.checked_add(additional_amount)
.expect("deposited_amount overflow");
- stream.amount_per_second = new_amount_per_second;
+ // ── Re-anchor the vesting schedule ──────────────────────────────────
+ //
+ // `unlocked_amount` is the single source of truth for how much has
+ // vested (it's what withdraw/cancel use too), so top-up must feed it
+ // back in rather than keeping its own separate vesting math — that
+ // divergence used to let `top_up`'s bookkeeping drift out of sync
+ // with what a recipient could actually withdraw.
+ let remaining_seconds = (stream.end_time - now) as i128;
+
+ if now >= stream.cliff_time {
+ // Past the cliff: freeze what's unlocked so far as a new
+ // "cliff" at `now`, and spread everything still owed (the old
+ // remainder plus the top-up) linearly across the remaining
+ // time. This keeps already-unlocked funds untouched while
+ // making the top-up actually stream out instead of sitting
+ // inert until end_time.
+ let already_unlocked = Self::unlocked_amount(&stream, now);
+ let remaining = stream
+ .deposited_amount
+ .checked_sub(already_unlocked)
+ .expect("deposited < unlocked — invariant broken");
+
+ let new_rate = if remaining_seconds > 0 {
+ remaining / remaining_seconds
+ } else {
+ 0
+ };
+ if new_rate == 0 && remaining > 0 {
+ panic!("stream amount too small for remaining duration");
+ }
+
+ stream.cliff_time = now;
+ stream.cliff_amount = already_unlocked;
+ stream.start_time = now;
+ stream.linear_amount = remaining;
+ stream.duration = remaining_seconds;
+ stream.amount_per_second = new_rate;
+ } else {
+ // Still before the cliff: the cliff bonus hasn't unlocked yet,
+ // so leave it untouched and just grow the linear portion.
+ stream.linear_amount = stream
+ .linear_amount
+ .checked_add(additional_amount)
+ .expect("linear_amount overflow");
+ let new_rate = if stream.duration > 0 {
+ stream.linear_amount / stream.duration
+ } else {
+ 0
+ };
+ if new_rate == 0 && stream.linear_amount > 0 {
+ panic!("stream amount too small for remaining duration");
+ }
+ stream.amount_per_second = new_rate;
+ }
env.storage()
.persistent()
@@ -786,7 +784,7 @@ impl StreamingContract {
stream_id,
additional_amount,
new_deposited_amount: stream.deposited_amount,
- new_amount_per_second,
+ new_amount_per_second: stream.amount_per_second,
}
.publish(&env);
@@ -797,22 +795,11 @@ impl StreamingContract {
/// Withdraw unlocked tokens from a stream.
///
- /// Only the recipient or their delegate can call this. Pass the exact amount to withdraw
- /// (must be ≤ withdrawable amount). Use `get_withdrawable` to query first.
- pub fn withdraw(env: Env, stream_id: u64, amount: i128) {
- let mut stream = Self::load_stream(&env, stream_id);
- let caller = env.invoker();
-
- // Check if caller is recipient or authorized delegate
- let is_recipient = caller == stream.recipient;
- let is_delegate = match Self::get_delegate(&env, stream_id) {
- Some(delegate) => caller == delegate,
- None => false,
- };
-
- if !is_recipient && !is_delegate {
- panic!("only recipient or delegate can withdraw");
- }
+ /// Only the recipient can authorize a withdrawal — a delegate (if set via
+ /// `set_delegate`) may submit the transaction, but `require_auth` is always
+ /// checked against the recipient, not the delegate. Pass the exact amount
+ /// to withdraw (must be ≤ withdrawable amount). Use `get_withdrawable` to
+ /// query first.
pub fn withdraw(env: Env, stream_id: u64, amount: i128) -> Result<(), StreamError> {
let mut stream = Self::load_stream(&env, stream_id)?;
@@ -831,9 +818,12 @@ impl StreamingContract {
return Err(StreamError::InsufficientFunds);
}
- stream.withdrawn_amount += amount;
- let fully_drained = stream.withdrawn_amount >= stream.deposited_amount
- && env.ledger().timestamp() >= stream.end_time;
+ stream.withdrawn_amount = stream
+ .withdrawn_amount
+ .checked_add(amount)
+ .expect("withdrawn_amount overflow");
+ let fully_drained =
+ stream.withdrawn_amount >= stream.deposited_amount && now >= stream.end_time;
env.storage()
.persistent()
@@ -864,9 +854,6 @@ impl StreamingContract {
let token_client = token::Client::new(&env, &stream.token);
token_client.transfer(&env.current_contract_address(), &stream.recipient, &amount);
- WithdrawEvent { stream_id, amount }.publish(&env);
-
- Ok(())
let remaining_withdrawable = Self::withdrawable_amount(&stream, now);
WithdrawEvent {
stream_id,
@@ -876,6 +863,8 @@ impl StreamingContract {
timestamp: now,
}
.publish(&env);
+
+ Ok(())
}
// ── Write: Cancel ────────────────────────────────────────────────────────
@@ -1079,7 +1068,8 @@ impl StreamingContract {
pub fn cleanup_stream(env: Env, caller: Address, stream_id: u64) {
caller.require_auth();
- let stream = Self::load_stream(&env, stream_id);
+ let stream =
+ Self::load_stream(&env, stream_id).unwrap_or_else(|_| panic!("stream not found"));
// Only sender or recipient may clean up.
if caller != stream.sender && caller != stream.recipient {
@@ -1124,13 +1114,14 @@ impl StreamingContract {
pub fn bump_stream(env: Env, stream_id: u64) -> Result<(), StreamError> {
Self::load_stream(&env, stream_id)?;
Self::extend_stream_ttl(&env, stream_id);
- Ok(())
StreamBumpedEvent {
stream_id,
timestamp: env.ledger().timestamp(),
}
.publish(&env);
+
+ Ok(())
}
// ── Metadata ──────────────────────────────────────────────────────────────
@@ -1168,15 +1159,15 @@ impl StreamingContract {
CONTRACT_VERSION
}
/// Get the contract name.
- pub fn name(env: Env) -> String {
- String::from_small_copy(&String::from_slice(&env, CONTRACT_NAME))
+ pub fn name(env: Env) -> soroban_sdk::String {
+ soroban_sdk::String::from_str(&env, CONTRACT_NAME)
}
// ── Delegation ────────────────────────────────────────────────────────────
/// Set a delegate who can withdraw on behalf of the recipient.
- pub fn set_delegate(env: Env, stream_id: u64, delegate: Address) {
- let stream = Self::load_stream(&env, stream_id);
+ pub fn set_delegate(env: Env, stream_id: u64, delegate: Address) -> Result<(), StreamError> {
+ let stream = Self::load_stream(&env, stream_id)?;
stream.recipient.require_auth();
env.storage()
@@ -1187,16 +1178,20 @@ impl StreamingContract {
PERSISTENT_TTL_LEDGERS,
PERSISTENT_TTL_LEDGERS,
);
+
+ Ok(())
}
/// Remove the delegate for a stream.
- pub fn remove_delegate(env: Env, stream_id: u64) {
- let stream = Self::load_stream(&env, stream_id);
+ pub fn remove_delegate(env: Env, stream_id: u64) -> Result<(), StreamError> {
+ let stream = Self::load_stream(&env, stream_id)?;
stream.recipient.require_auth();
env.storage()
.persistent()
.remove(&DataKey::Delegate(stream_id));
+
+ Ok(())
}
/// Get the delegate for a stream, if set.
@@ -1207,29 +1202,6 @@ impl StreamingContract {
}
// ── Internal helpers ─────────────────────────────────────────────────────
- // ──── Internal helpers ─────────────────────────────────────────────────────
-
- fn require_admin(env: &Env, admin: &Address) {
- let stored_admin: Address = env
- .storage()
- .instance()
- .get(&DataKey::Admin)
- .unwrap_or_else(|| panic!("not initialized"));
- if admin != &stored_admin {
- panic!("unauthorized");
- }
- }
-
- fn require_not_paused(env: &Env) {
- let paused: bool = env
- .storage()
- .instance()
- .get(&DataKey::Paused)
- .unwrap_or(false);
- if paused {
- panic!("contract is paused");
- }
- }
fn load_stream(env: &Env, id: u64) -> Result {
env.storage()
@@ -1280,10 +1252,9 @@ impl StreamingContract {
.unwrap_or(0u64);
let next = id + 1;
env.storage().instance().set(&DataKey::NextId, &next);
- env.storage().instance().extend_ttl(
- INSTANCE_TTL_LEDGERS,
- INSTANCE_TTL_LEDGERS,
- );
+ env.storage()
+ .instance()
+ .extend_ttl(INSTANCE_TTL_LEDGERS, INSTANCE_TTL_LEDGERS);
next
}
@@ -1296,7 +1267,9 @@ impl StreamingContract {
.unwrap_or(Vec::new(env));
list.push_back(id);
env.storage().persistent().set(&key, &list);
- env.storage().persistent().extend_ttl(&key, PERSISTENT_TTL_LEDGERS, PERSISTENT_TTL_LEDGERS);
+ env.storage()
+ .persistent()
+ .extend_ttl(&key, PERSISTENT_TTL_LEDGERS, PERSISTENT_TTL_LEDGERS);
}
/// Remove a stream ID from an address index list.
@@ -1309,12 +1282,14 @@ impl StreamingContract {
// Fix: use `existing_id` to avoid shadowing the outer `id` parameter.
let position = indexes.iter().position(|existing_id| existing_id == id);
- let position = indexes.iter().position(|x| x == id);
if let Some(i) = position {
indexes.remove(i as u32);
}
env.storage().persistent().set(&key, &indexes);
+ env.storage()
+ .persistent()
+ .extend_ttl(&key, PERSISTENT_TTL_LEDGERS, PERSISTENT_TTL_LEDGERS);
}
/// Extend the TTL of all storage entries for a stream to [`PERSISTENT_TTL_LEDGERS`] (~30 days).
@@ -1329,28 +1304,10 @@ impl StreamingContract {
PERSISTENT_TTL_LEDGERS,
);
}
-
- fn vested_amount(stream: &Stream, now: u64) -> i128 {
- if now < stream.cliff_time {
- return 0;
- }
-
- let elapsed = (now.min(stream.end_time) - stream.cliff_time) as i128;
- let linear = stream
- .amount_per_second
- .checked_mul(elapsed)
- .expect("amount_per_second * elapsed overflow");
-
- stream
- .cliff_amount
- .checked_add(linear)
- .expect("cliff_amount + linear overflow")
- .min(stream.deposited_amount)
- }
}
-mod test;
-mod test_security;
mod bench;
+mod test;
mod test_batch;
mod test_integration;
+mod test_security;
diff --git a/contracts/streaming/src/test.rs b/contracts/streaming/src/test.rs
index 4de4bd7..aa0e955 100644
--- a/contracts/streaming/src/test.rs
+++ b/contracts/streaming/src/test.rs
@@ -6,8 +6,7 @@ use super::*;
use soroban_sdk::{
testutils::{Address as _, Ledger},
token::{Client as TokenClient, StellarAssetClient},
- Address, Env,
- Address, Env, testutils::{Address as _, Ledger}, token::{Client as TokenClient, StellarAssetClient}, vec
+ vec, Address, Env,
};
// ─── Test helpers ─────────────────────────────────────────────────────────────
@@ -33,7 +32,9 @@ impl TestEnv {
// Deploy a native Stellar Asset Contract as the test token.
let token_admin = Address::generate(&env);
- let token_id = env.register_stellar_asset_contract_v2(token_admin.clone()).address();
+ let token_id = env
+ .register_stellar_asset_contract_v2(token_admin.clone())
+ .address();
// Mint tokens to sender.
let asset_client = StellarAssetClient::new(&env, &token_id);
@@ -42,14 +43,21 @@ impl TestEnv {
let client = StreamingContractClient::new(&env, &contract_id);
client.initialize(&admin);
- TestEnv { env, contract_id, token_id, sender, recipient, admin }
+ TestEnv {
+ env,
+ contract_id,
+ token_id,
+ sender,
+ recipient,
+ admin,
+ }
}
- fn client(&self) -> StreamingContractClient {
+ fn client(&self) -> StreamingContractClient<'_> {
StreamingContractClient::new(&self.env, &self.contract_id)
}
- fn token(&self) -> TokenClient {
+ fn token(&self) -> TokenClient<'_> {
TokenClient::new(&self.env, &self.token_id)
}
@@ -83,13 +91,18 @@ fn test_create_stream_basic() {
let total = params.total_amount;
// Approve contract to pull funds.
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
- let stream_id = client.create_stream(&t.sender, ¶ms).unwrap();
+ let stream_id = client.create_stream(&t.sender, ¶ms);
assert_eq!(stream_id, 1);
- let stream = client.get_stream(&stream_id).unwrap();
+ let stream = client.get_stream(&stream_id);
assert_eq!(stream.sender, t.sender);
assert_eq!(stream.recipient, t.recipient);
assert_eq!(stream.deposited_amount, total);
@@ -127,12 +140,12 @@ fn test_create_stream_with_cliff() {
&(t.env.ledger().sequence() + 500),
);
- let stream_id = client.create_stream(&t.sender, ¶ms).unwrap();
- let stream = client.get_stream(&stream_id).unwrap();
+ let stream_id = client.create_stream(&t.sender, ¶ms);
+ let stream = client.get_stream(&stream_id);
assert_eq!(stream.cliff_amount, cliff_amount);
// Before cliff — nothing withdrawable.
- assert_eq!(client.get_withdrawable(&stream_id).unwrap(), 0);
+ assert_eq!(client.get_withdrawable(&stream_id), 0);
}
#[test]
@@ -143,7 +156,12 @@ fn test_create_stream_invalid_times() {
let client = t.client();
let mut params = t.default_params(now);
params.end_time = params.start_time; // same = invalid
- t.token().approve(&t.sender, &t.contract_id, ¶ms.total_amount, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ ¶ms.total_amount,
+ &(t.env.ledger().sequence() + 500),
+ );
let result = client.try_create_stream(&t.sender, ¶ms);
assert_eq!(result, Err(Ok(StreamError::InvalidTimeRange)));
}
@@ -156,7 +174,12 @@ fn test_create_stream_zero_amount() {
let client = t.client();
let mut params = t.default_params(now);
params.total_amount = 0;
- t.token().approve(&t.sender, &t.contract_id, ¶ms.total_amount, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ ¶ms.total_amount,
+ &(t.env.ledger().sequence() + 500),
+ );
let result = client.try_create_stream(&t.sender, ¶ms);
assert_eq!(result, Err(Ok(StreamError::InvalidAmount)));
}
@@ -172,20 +195,25 @@ fn test_withdraw_partial() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
- let stream_id = client.create_stream(&t.sender, ¶ms).unwrap();
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
+ let stream_id = client.create_stream(&t.sender, ¶ms);
// Advance time by 500s (halfway through the 1000s stream).
t.set_time(now + 500);
- let withdrawable = client.get_withdrawable(&stream_id).unwrap();
+ let withdrawable = client.get_withdrawable(&stream_id);
// Should be ~500/1000 * total.
assert!(withdrawable > 0);
assert!(withdrawable <= total / 2 + 1); // allow for rounding
- client.withdraw(&stream_id, &withdrawable).unwrap();
+ client.withdraw(&stream_id, &withdrawable);
- let stream = client.get_stream(&stream_id).unwrap();
+ let stream = client.get_stream(&stream_id);
assert_eq!(stream.withdrawn_amount, withdrawable);
assert_eq!(t.token().balance(&t.recipient), withdrawable);
}
@@ -199,15 +227,20 @@ fn test_withdraw_full_after_end() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
- let stream_id = client.create_stream(&t.sender, ¶ms).unwrap();
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
+ let stream_id = client.create_stream(&t.sender, ¶ms);
// After end time — all unlocked.
t.set_time(now + 2000);
- let withdrawable = client.get_withdrawable(&stream_id).unwrap();
+ let withdrawable = client.get_withdrawable(&stream_id);
assert_eq!(withdrawable, total);
- client.withdraw(&stream_id, &total).unwrap();
+ client.withdraw(&stream_id, &total);
assert_eq!(t.token().balance(&t.recipient), total);
assert_eq!(t.token().balance(&t.contract_id), 0);
@@ -222,8 +255,13 @@ fn test_withdraw_too_much() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
- let stream_id = client.create_stream(&t.sender, ¶ms).unwrap();
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
+ let stream_id = client.create_stream(&t.sender, ¶ms);
t.set_time(now + 500);
@@ -249,16 +287,21 @@ fn test_withdraw_cliff_before_time() {
cliff_amount: 0,
};
- t.token().approve(&t.sender, &t.contract_id, ¶ms.total_amount, &(t.env.ledger().sequence() + 500));
- let stream_id = client.create_stream(&t.sender, ¶ms).unwrap();
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ ¶ms.total_amount,
+ &(t.env.ledger().sequence() + 500),
+ );
+ let stream_id = client.create_stream(&t.sender, ¶ms);
// Before cliff — nothing available.
t.set_time(now + 100);
- assert_eq!(client.get_withdrawable(&stream_id).unwrap(), 0);
+ assert_eq!(client.get_withdrawable(&stream_id), 0);
// After cliff — linear unlock starts.
t.set_time(now + 600);
- assert!(client.get_withdrawable(&stream_id).unwrap() > 0);
+ assert!(client.get_withdrawable(&stream_id) > 0);
}
#[test]
@@ -269,7 +312,12 @@ fn test_create_stream_self_rejected() {
let client = t.client();
let mut params = t.default_params(now);
params.recipient = t.sender.clone(); // same as sender
- t.token().approve(&t.sender, &t.contract_id, ¶ms.total_amount, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ ¶ms.total_amount,
+ &(t.env.ledger().sequence() + 500),
+ );
let result = client.try_create_stream(&t.sender, ¶ms);
assert_eq!(result, Err(Ok(StreamError::SelfStream)));
}
@@ -285,17 +333,22 @@ fn test_cancel_midway() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
- let stream_id = client.create_stream(&t.sender, ¶ms).unwrap();
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
+ let stream_id = client.create_stream(&t.sender, ¶ms);
// Advance halfway.
t.set_time(now + 500);
let sender_balance_before = t.token().balance(&t.sender);
- client.cancel(&stream_id).unwrap();
+ client.cancel(&stream_id);
- let stream = client.get_stream(&stream_id).unwrap();
+ let stream = client.get_stream(&stream_id);
assert!(stream.cancelled);
let recipient_balance = t.token().balance(&t.recipient);
@@ -319,10 +372,15 @@ fn test_cancel_twice() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
- let stream_id = client.create_stream(&t.sender, ¶ms).unwrap();
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
+ let stream_id = client.create_stream(&t.sender, ¶ms);
- client.cancel(&stream_id).unwrap();
+ client.cancel(&stream_id);
let result = client.try_cancel(&stream_id);
assert_eq!(result, Err(Ok(StreamError::StreamCancelled)));
}
@@ -336,11 +394,16 @@ fn test_withdraw_from_cancelled_stream() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
- let stream_id = client.create_stream(&t.sender, ¶ms).unwrap();
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
+ let stream_id = client.create_stream(&t.sender, ¶ms);
t.set_time(now + 500);
- client.cancel(&stream_id).unwrap();
+ client.cancel(&stream_id);
// Recipient tries to withdraw after cancel — should return StreamCancelled.
let result = client.try_withdraw(&stream_id, &1_0000000);
@@ -363,7 +426,7 @@ fn test_stream_indexes() {
¶ms.total_amount,
&(t.env.ledger().sequence() + 500),
);
- client.create_stream(&t.sender, ¶ms).unwrap()
+ client.create_stream(&t.sender, ¶ms)
};
let id1 = approve_and_create(t.default_params(now));
@@ -371,13 +434,13 @@ fn test_stream_indexes() {
let sent = client.get_sent_streams(&t.sender, &0, &100);
assert_eq!(sent.len(), 2);
- assert!(sent.contains(&id1));
- assert!(sent.contains(&id2));
+ assert!(sent.contains(id1));
+ assert!(sent.contains(id2));
let received = client.get_received_streams(&t.recipient, &0, &100);
assert_eq!(received.len(), 2);
- assert!(received.contains(&id1));
- assert!(received.contains(&id2));
+ assert!(received.contains(id1));
+ assert!(received.contains(id2));
// Test pagination and count
let sent_page1 = client.get_sent_streams(&t.sender, &0, &1);
@@ -403,7 +466,7 @@ fn test_incrementing_ids() {
¶ms.total_amount,
&(t.env.ledger().sequence() + 500),
);
- let id = client.create_stream(&t.sender, ¶ms).unwrap();
+ let id = client.create_stream(&t.sender, ¶ms);
assert_eq!(id, expected_id);
}
}
@@ -419,7 +482,12 @@ fn test_transfer_stream_basic() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
let new_recipient = Address::generate(&t.env);
@@ -437,7 +505,7 @@ fn test_transfer_stream_basic() {
let new_received = client.get_received_streams(&new_recipient, &0, &100);
assert_eq!(new_received.len(), 1);
- assert!(new_received.contains(&stream_id));
+ assert!(new_received.contains(stream_id));
}
#[test]
@@ -450,8 +518,12 @@ fn test_transfer_stream_with_partial_withdrawals() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
- let stream_id = client.create_stream(&t.sender, ¶ms).unwrap();
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
// Advance halfway; old recipient withdraws half
@@ -484,7 +556,12 @@ fn test_transfer_stream_old_recipient_cannot_withdraw() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
let new_recipient = Address::generate(&t.env);
@@ -505,7 +582,12 @@ fn test_transfer_stream_roundtrip() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
let new_recipient = Address::generate(&t.env);
@@ -516,7 +598,7 @@ fn test_transfer_stream_roundtrip() {
assert_eq!(stream.recipient, t.recipient);
let received = client.get_received_streams(&t.recipient, &0, &100);
- assert!(received.contains(&stream_id));
+ assert!(received.contains(stream_id));
}
#[test]
@@ -537,21 +619,23 @@ fn test_transfer_stream_at_cliff_time() {
};
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
// Transfer at exact cliff time
t.set_time(now + 200);
- let stream_id = client.create_stream(&t.sender, ¶ms);
let new_recipient = Address::generate(&t.env);
- client.transfer_stream(&stream_id, &new_recipient).unwrap();
+ client.transfer_stream(&stream_id, &new_recipient);
- let stream = client.get_stream(&stream_id).unwrap();
- assert_eq!(stream.sender, t.sender);
let stream = client.get_stream(&stream_id);
+ assert_eq!(stream.sender, t.sender);
assert_eq!(stream.recipient, new_recipient);
-}
let old_received = client.get_received_streams(&t.recipient, &0, &100);
assert_eq!(old_received.len(), 0);
@@ -563,6 +647,28 @@ fn test_transfer_stream_at_cliff_time() {
#[test]
fn test_transfer_stream_cancelled_fails() {
+ let t = TestEnv::setup();
+ let now = 1_000_000u64;
+ t.set_time(now);
+ let client = t.client();
+ let params = t.default_params(now);
+ let total = params.total_amount;
+
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
+ let stream_id = client.create_stream(&t.sender, ¶ms);
+
+ client.cancel(&stream_id);
+
+ let new_recipient = Address::generate(&t.env);
+ let result = client.try_transfer_stream(&stream_id, &new_recipient);
+ assert_eq!(result, Err(Ok(StreamError::StreamCancelled)));
+}
+
#[test]
fn test_transfer_stream_near_end() {
// Transfer when stream is 99% complete
@@ -573,7 +679,12 @@ fn test_transfer_stream_near_end() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
t.set_time(now + 990); // 99% through
@@ -589,8 +700,13 @@ fn test_transfer_stream_near_end() {
let old_id = client.get_received_streams(&t.recipient, &0, &100);
assert_eq!(old_id, Vec::new(&t.env));
+ // Fully withdrawing at end_time drains and archives the stream, so it
+ // moves out of the active received index into the archive index.
let new_id = client.get_received_streams(&new_recipient, &0, &100);
- assert_eq!(new_id, vec![&t.env, 1]);
+ assert_eq!(new_id, Vec::new(&t.env));
+
+ let archived_id = client.get_archived_received_streams(&new_recipient, &0, &100);
+ assert_eq!(archived_id, vec![&t.env, 1]);
}
#[test]
@@ -602,49 +718,29 @@ fn test_transfer_stream_sender_index_unaffected() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
- let stream_id = client.create_stream(&t.sender, ¶ms).unwrap();
-
- client.cancel(&stream_id).unwrap();
-
- let new_recipient = Address::generate(&t.env);
- let result = client.try_transfer_stream(&stream_id, &new_recipient);
- assert_eq!(result, Err(Ok(StreamError::StreamCancelled)));
-
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
- client.cancel(&stream_id);
+ let sent_before = client.get_sent_streams(&t.sender, &0, &100);
+ assert_eq!(sent_before.len(), 1);
+ assert!(sent_before.contains(stream_id));
let new_recipient = Address::generate(&t.env);
client.transfer_stream(&stream_id, &new_recipient);
+
+ // Transferring the recipient must not change the sender's own index.
+ let sent_after = client.get_sent_streams(&t.sender, &0, &100);
+ assert_eq!(sent_after.len(), 1);
+ assert!(sent_after.contains(stream_id));
}
// ─── top up ───────────────────────────────────────────────────────────────────
-#[test]
-fn test_top_up_increases_deposited_amount() {
- let t = TestEnv::setup();
- let now = 1_000_000u64;
- t.set_time(now);
- let client = t.client();
- let params = t.default_params(now);
- let total = params.total_amount;
-
- // Approve contract to pull funds.
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
- let stream_id = client.create_stream(&t.sender, ¶ms).unwrap();
-
- let stream_id = client.create_stream(&t.sender, ¶ms);
-
- let new_recipient = Address::generate(&t.env);
- client.transfer_stream(&stream_id, &new_recipient);
-
- // Sender index must still contain the stream
- let sent = client.get_sent_streams(&t.sender, &0, &100);
- assert!(sent.contains(&stream_id));
-}
-
#[test]
fn test_transfer_stream_multiple_times() {
let t = TestEnv::setup();
@@ -654,7 +750,12 @@ fn test_transfer_stream_multiple_times() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
let r1 = Address::generate(&t.env);
@@ -667,10 +768,15 @@ fn test_transfer_stream_multiple_times() {
assert_eq!(stream_id, 1);
let additional = 500_0000000i128;
- t.token().approve(&t.sender, &t.contract_id, &additional, &(t.env.ledger().sequence() + 500));
- client.top_up(&stream_id, &additional).unwrap();
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &additional,
+ &(t.env.ledger().sequence() + 500),
+ );
+ client.top_up(&stream_id, &additional);
- let stream = client.get_stream(&stream_id).unwrap();
+ let stream = client.get_stream(&stream_id);
assert_eq!(stream.deposited_amount, total + additional);
assert_eq!(stream.end_time, now + 1000);
let stream = client.get_stream(&stream_id);
@@ -692,11 +798,16 @@ fn test_transfer_then_cancel() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
- let stream_id = client.create_stream(&t.sender, ¶ms).unwrap();
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
+ let stream_id = client.create_stream(&t.sender, ¶ms);
- let stream_before = client.get_stream(&stream_id).unwrap();
- let rate_before = stream_before.amount_per_second;
+ let stream_before = client.get_stream(&stream_id);
+ let _rate_before = stream_before.amount_per_second;
let new_recipient = Address::generate(&t.env);
t.set_time(now + 500);
client.transfer_stream(&stream_id, &new_recipient);
@@ -715,7 +826,6 @@ fn test_transfer_then_cancel() {
}
#[test]
-#[should_panic(expected = "cannot transfer a cancelled stream")]
fn test_transfer_stream_cancelled_panics() {
let t = TestEnv::setup();
let now = 1_000_000u64;
@@ -724,19 +834,19 @@ fn test_transfer_stream_cancelled_panics() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
- client.top_up(&stream_id, &total).unwrap();
-
- let stream_after = client.get_stream(&stream_id).unwrap();
- assert_eq!(stream_after.amount_per_second, rate_before * 2);
-}
-
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
client.cancel(&stream_id);
let new_recipient = Address::generate(&t.env);
- client.transfer_stream(&stream_id, &new_recipient);
+ let result = client.try_transfer_stream(&stream_id, &new_recipient);
+ assert_eq!(result, Err(Ok(StreamError::StreamCancelled)));
}
// ─── top_up ───────────────────────────────────────────────────────────────────
@@ -750,11 +860,21 @@ fn test_top_up_increases_deposited_amount() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
let additional = 500_0000000i128;
- t.token().approve(&t.sender, &t.contract_id, &additional, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &additional,
+ &(t.env.ledger().sequence() + 500),
+ );
client.top_up(&stream_id, &additional);
let stream = client.get_stream(&stream_id);
@@ -770,12 +890,22 @@ fn test_top_up_at_start_doubles_rate() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
let rate_before = client.get_stream(&stream_id).amount_per_second;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
client.top_up(&stream_id, &total);
let rate_after = client.get_stream(&stream_id).amount_per_second;
@@ -791,18 +921,28 @@ fn test_top_up_mid_stream_recalculates_rate() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
- let stream_id = client.create_stream(&t.sender, ¶ms).unwrap();
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
+ let stream_id = client.create_stream(&t.sender, ¶ms);
t.set_time(now + 500);
let additional = 500_0000000i128;
- t.token().approve(&t.sender, &t.contract_id, &additional, &(t.env.ledger().sequence() + 500));
- client.top_up(&stream_id, &additional).unwrap();
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &additional,
+ &(t.env.ledger().sequence() + 500),
+ );
+ client.top_up(&stream_id, &additional);
// remaining_deposited = 500, additional = 500 → new_remaining = 1000
// new_rate = 1000 / 500s remaining = 2 tokens/sec
- let stream = client.get_stream(&stream_id).unwrap();
+ let _stream = client.get_stream(&stream_id);
// remaining_deposited = 500 tokens, additional = 500 tokens
// new_rate = 1000 / 500s remaining = 2 tokens/sec (in smallest units)
let stream = client.get_stream(&stream_id);
@@ -819,13 +959,23 @@ fn test_top_up_cancelled_stream_returns_error() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
- let stream_id = client.create_stream(&t.sender, ¶ms).unwrap();
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
+ let stream_id = client.create_stream(&t.sender, ¶ms);
- client.cancel(&stream_id).unwrap();
+ client.cancel(&stream_id);
let additional = 100_0000000i128;
- t.token().approve(&t.sender, &t.contract_id, &additional, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &additional,
+ &(t.env.ledger().sequence() + 500),
+ );
let result = client.try_top_up(&stream_id, &additional);
assert_eq!(result, Err(Ok(StreamError::StreamCancelled)));
}
@@ -839,13 +989,23 @@ fn test_top_up_ended_stream_returns_error() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
- let stream_id = client.create_stream(&t.sender, ¶ms).unwrap();
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
+ let stream_id = client.create_stream(&t.sender, ¶ms);
t.set_time(now + 2000);
let additional = 100_0000000i128;
- t.token().approve(&t.sender, &t.contract_id, &additional, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &additional,
+ &(t.env.ledger().sequence() + 500),
+ );
let result = client.try_top_up(&stream_id, &additional);
assert_eq!(result, Err(Ok(StreamError::StreamEnded)));
}
@@ -859,8 +1019,13 @@ fn test_top_up_zero_amount_returns_error() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
- let stream_id = client.create_stream(&t.sender, ¶ms).unwrap();
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
+ let stream_id = client.create_stream(&t.sender, ¶ms);
let result = client.try_top_up(&stream_id, &0i128);
assert_eq!(result, Err(Ok(StreamError::InvalidAmount)));
@@ -875,12 +1040,22 @@ fn test_top_up_immediately_after_creation() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
// Top up at t=0 (no time elapsed)
let additional = 200_0000000i128;
- t.token().approve(&t.sender, &t.contract_id, &additional, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &additional,
+ &(t.env.ledger().sequence() + 500),
+ );
client.top_up(&stream_id, &additional);
let stream = client.get_stream(&stream_id);
@@ -908,13 +1083,23 @@ fn test_top_up_at_cliff_time() {
};
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
// Top up at exact cliff time
t.set_time(now + 200);
let additional = 200_0000000i128;
- t.token().approve(&t.sender, &t.contract_id, &additional, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &additional,
+ &(t.env.ledger().sequence() + 500),
+ );
client.top_up(&stream_id, &additional);
let stream = client.get_stream(&stream_id);
@@ -931,12 +1116,22 @@ fn test_top_up_near_end() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
t.set_time(now + 999); // 1 second remaining
let additional = 100_0000000i128;
- t.token().approve(&t.sender, &t.contract_id, &additional, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &additional,
+ &(t.env.ledger().sequence() + 500),
+ );
client.top_up(&stream_id, &additional);
let stream = client.get_stream(&stream_id);
@@ -954,12 +1149,22 @@ fn test_top_up_multiple_successive() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
let chunk = 100_0000000i128;
for _ in 0..3 {
- t.token().approve(&t.sender, &t.contract_id, &chunk, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &chunk,
+ &(t.env.ledger().sequence() + 500),
+ );
client.top_up(&stream_id, &chunk);
}
@@ -976,11 +1181,21 @@ fn test_top_up_then_withdraw() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
let additional = 500_0000000i128;
- t.token().approve(&t.sender, &t.contract_id, &additional, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &additional,
+ &(t.env.ledger().sequence() + 500),
+ );
client.top_up(&stream_id, &additional);
// Advance to end, withdraw everything
@@ -1001,12 +1216,22 @@ fn test_top_up_then_cancel() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
t.set_time(now + 250);
let additional = 500_0000000i128;
- t.token().approve(&t.sender, &t.contract_id, &additional, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &additional,
+ &(t.env.ledger().sequence() + 500),
+ );
client.top_up(&stream_id, &additional);
let sender_before = t.token().balance(&t.sender);
@@ -1017,6 +1242,8 @@ fn test_top_up_then_cancel() {
// All funds accounted for
assert_eq!(recipient_got + sender_refund, total + additional);
+}
+
// ─── admin / upgrade ──────────────────────────────────────────────────────────
#[test]
@@ -1059,7 +1286,6 @@ fn test_upgrade_succeeds_with_admin_auth() {
assert!(result.is_err());
}
-
// ─── Version and Metadata Tests ────────────────────────────────────────────────
#[test]
@@ -1074,7 +1300,10 @@ fn test_contract_name() {
let t = TestEnv::setup();
let client = t.client();
let name = client.name();
- assert_eq!(name, String::from_slice(&t.env, "FlowStar Streaming"));
+ assert_eq!(
+ name,
+ soroban_sdk::String::from_str(&t.env, "FlowStar Streaming")
+ );
}
// ─── Max Stream Duration Tests ─────────────────────────────────────────────────
@@ -1089,7 +1318,12 @@ fn test_create_stream_exceeds_max_duration() {
let mut params = t.default_params(now);
// Set duration to 11 years (exceeds 10-year max)
params.end_time = now + 315_360_000 + 1;
- t.token().approve(&t.sender, &t.contract_id, ¶ms.total_amount, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ ¶ms.total_amount,
+ &(t.env.ledger().sequence() + 500),
+ );
client.create_stream(&t.sender, ¶ms);
}
@@ -1102,7 +1336,12 @@ fn test_create_stream_at_max_duration() {
let mut params = t.default_params(now);
// Set duration to exactly 10 years (max allowed)
params.end_time = now + 315_360_000;
- t.token().approve(&t.sender, &t.contract_id, ¶ms.total_amount, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ ¶ms.total_amount,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
let stream = client.get_stream(&stream_id);
assert_eq!(stream.end_time - stream.start_time, 315_360_000);
@@ -1115,7 +1354,12 @@ fn test_create_stream_under_max_duration() {
t.set_time(now);
let client = t.client();
let params = t.default_params(now); // 1000 seconds < max
- t.token().approve(&t.sender, &t.contract_id, ¶ms.total_amount, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ ¶ms.total_amount,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
assert_eq!(stream_id, 1);
}
@@ -1131,7 +1375,12 @@ fn test_set_delegate_by_recipient() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
// Recipient sets a delegate
@@ -1152,7 +1401,12 @@ fn test_delegate_can_withdraw() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
// Recipient sets a delegate
@@ -1180,12 +1434,17 @@ fn test_remove_delegate() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
let delegate = Address::generate(&t.env);
client.set_delegate(&stream_id, &delegate);
-
+
// Verify delegate is set
assert_eq!(client.get_delegate(&stream_id), Some(delegate.clone()));
@@ -1205,7 +1464,12 @@ fn test_transfer_clears_delegate() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
let delegate = Address::generate(&t.env);
@@ -1223,7 +1487,7 @@ fn test_transfer_clears_delegate() {
// ─── Transfer Function Fix Tests ──────────────────────────────────────────────
#[test]
-fn test_transfer_stream_basic() {
+fn test_transfer_stream_basic_regression() {
let t = TestEnv::setup();
let now = 1_000_000u64;
t.set_time(now);
@@ -1231,7 +1495,12 @@ fn test_transfer_stream_basic() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
let old_recipient = t.recipient.clone();
@@ -1252,7 +1521,6 @@ fn test_transfer_stream_basic() {
}
#[test]
-#[should_panic(expected = "cannot transfer a cancelled stream")]
fn test_transfer_cancelled_stream_panics() {
let t = TestEnv::setup();
let now = 1_000_000u64;
@@ -1261,7 +1529,12 @@ fn test_transfer_cancelled_stream_panics() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
// Cancel stream
@@ -1269,7 +1542,8 @@ fn test_transfer_cancelled_stream_panics() {
// Try to transfer cancelled stream
let new_recipient = Address::generate(&t.env);
- client.transfer_stream(&stream_id, &new_recipient);
+ let result = client.try_transfer_stream(&stream_id, &new_recipient);
+ assert_eq!(result, Err(Ok(StreamError::StreamCancelled)));
}
#[test]
@@ -1281,7 +1555,12 @@ fn test_transfer_stream_updates_indices() {
let params = t.default_params(now);
let total = params.total_amount;
- t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500));
+ t.token().approve(
+ &t.sender,
+ &t.contract_id,
+ &total,
+ &(t.env.ledger().sequence() + 500),
+ );
let stream_id = client.create_stream(&t.sender, ¶ms);
let new_recipient = Address::generate(&t.env);
diff --git a/contracts/streaming/src/test_batch.rs b/contracts/streaming/src/test_batch.rs
index 0b10c60..d12635b 100644
--- a/contracts/streaming/src/test_batch.rs
+++ b/contracts/streaming/src/test_batch.rs
@@ -27,7 +27,6 @@ struct TestEnv {
token_id: Address,
sender: Address,
recipient: Address,
- admin: Address,
}
impl TestEnv {
@@ -52,14 +51,20 @@ impl TestEnv {
let client = StreamingContractClient::new(&env, &contract_id);
client.initialize(&admin);
- TestEnv { env, contract_id, token_id, sender, recipient, admin }
+ TestEnv {
+ env,
+ contract_id,
+ token_id,
+ sender,
+ recipient,
+ }
}
- fn client(&self) -> StreamingContractClient {
+ fn client(&self) -> StreamingContractClient<'_> {
StreamingContractClient::new(&self.env, &self.contract_id)
}
- fn token(&self) -> TokenClient {
+ fn token(&self) -> TokenClient<'_> {
TokenClient::new(&self.env, &self.token_id)
}
@@ -113,7 +118,7 @@ fn test_batch_create_happy_path() {
inputs.push_back(t.make_input(&r3, now));
let client = t.client();
- let ids = client.create_streams_batch(&t.sender, &inputs).unwrap();
+ let ids = client.create_streams_batch(&t.sender, &inputs);
// Three IDs returned in order
assert_eq!(ids.len(), 3);
@@ -124,7 +129,7 @@ fn test_batch_create_happy_path() {
// Each stream has the correct state
for (i, recipient) in [&r1, &r2, &r3].iter().enumerate() {
let id = ids.get(i as u32).unwrap();
- let stream = client.get_stream(&id).unwrap();
+ let stream = client.get_stream(&id);
assert_eq!(stream.sender, t.sender);
assert_eq!(&stream.recipient, *recipient);
assert_eq!(stream.deposited_amount, per_stream);
@@ -159,9 +164,8 @@ fn test_batch_create_returns_ids_in_order() {
end_time: now + 1000,
cliff_time: now,
cliff_amount: 0,
- metadata: None,
};
- client.create_stream(&t.sender, ¶ms).unwrap();
+ client.create_stream(&t.sender, ¶ms);
// Batch of 2 → IDs should be 2 and 3
let r1 = Address::generate(&t.env);
@@ -172,7 +176,7 @@ fn test_batch_create_returns_ids_in_order() {
inputs.push_back(t.make_input(&r1, now));
inputs.push_back(t.make_input(&r2, now));
- let ids = client.create_streams_batch(&t.sender, &inputs).unwrap();
+ let ids = client.create_streams_batch(&t.sender, &inputs);
assert_eq!(ids.len(), 2);
assert_eq!(ids.get(0).unwrap(), 2u64);
assert_eq!(ids.get(1).unwrap(), 3u64);
@@ -196,14 +200,14 @@ fn test_batch_sender_index_updated() {
}
let client = t.client();
- let ids = client.create_streams_batch(&t.sender, &inputs).unwrap();
+ let ids = client.create_streams_batch(&t.sender, &inputs);
let sent = client.get_sent_streams(&t.sender, &0, &100);
assert_eq!(sent.len(), 5);
for i in 0..5u32 {
let id = ids.get(i).unwrap();
- assert!(sent.contains(&id));
+ assert!(sent.contains(id));
}
}
@@ -226,18 +230,18 @@ fn test_batch_recipient_indexes_updated() {
inputs.push_back(t.make_input(&r_other, now));
let client = t.client();
- let ids = client.create_streams_batch(&t.sender, &inputs).unwrap();
+ let ids = client.create_streams_batch(&t.sender, &inputs);
// r_shared should have 2 streams
let shared_received = client.get_received_streams(&r_shared, &0, &100);
assert_eq!(shared_received.len(), 2);
- assert!(shared_received.contains(&ids.get(0).unwrap()));
- assert!(shared_received.contains(&ids.get(1).unwrap()));
+ assert!(shared_received.contains(ids.get(0).unwrap()));
+ assert!(shared_received.contains(ids.get(1).unwrap()));
// r_other should have 1 stream
let other_received = client.get_received_streams(&r_other, &0, &100);
assert_eq!(other_received.len(), 1);
- assert!(other_received.contains(&ids.get(2).unwrap()));
+ assert!(other_received.contains(ids.get(2).unwrap()));
}
// ─── Validation failures ──────────────────────────────────────────────────────
@@ -257,7 +261,7 @@ fn test_batch_partial_failure_rejected_atomically() {
let mut inputs: Vec = Vec::new(&t.env);
inputs.push_back(t.make_input(&r1, now)); // valid
- // Invalid: zero amount
+ // Invalid: zero amount
inputs.push_back(CreateStreamInput {
recipient: r2.clone(),
token: t.token_id.clone(),
@@ -372,7 +376,7 @@ fn test_batch_max_size_exactly_20_succeeds() {
}
let client = t.client();
- let ids = client.create_streams_batch(&t.sender, &inputs).unwrap();
+ let ids = client.create_streams_batch(&t.sender, &inputs);
assert_eq!(ids.len(), 20);
}
@@ -422,10 +426,10 @@ fn test_batch_single_stream_works() {
inputs.push_back(t.make_input(&r, now));
let client = t.client();
- let ids = client.create_streams_batch(&t.sender, &inputs).unwrap();
+ let ids = client.create_streams_batch(&t.sender, &inputs);
assert_eq!(ids.len(), 1);
- let stream = client.get_stream(&ids.get(0).unwrap()).unwrap();
+ let stream = client.get_stream(&ids.get(0).unwrap());
assert_eq!(stream.deposited_amount, amount);
}
@@ -454,20 +458,20 @@ fn test_batch_supports_cliff() {
});
let client = t.client();
- let ids = client.create_streams_batch(&t.sender, &inputs).unwrap();
+ let ids = client.create_streams_batch(&t.sender, &inputs);
let id = ids.get(0).unwrap();
- let stream = client.get_stream(&id).unwrap();
+ let stream = client.get_stream(&id);
assert_eq!(stream.cliff_amount, cliff_amount);
assert_eq!(stream.cliff_time, now + 200);
// Before cliff — nothing withdrawable
t.set_time(now + 100);
- assert_eq!(client.get_withdrawable(&id).unwrap(), 0);
+ assert_eq!(client.get_withdrawable(&id), 0);
// After cliff — cliff amount available
t.set_time(now + 200);
- assert!(client.get_withdrawable(&id).unwrap() >= cliff_amount);
+ assert!(client.get_withdrawable(&id) >= cliff_amount);
}
// ─── Batch streams are individually withdrawable ──────────────────────────────
@@ -488,19 +492,19 @@ fn test_batch_streams_are_independently_withdrawable() {
inputs.push_back(t.make_input(&r2, now));
let client = t.client();
- let ids = client.create_streams_batch(&t.sender, &inputs).unwrap();
+ let ids = client.create_streams_batch(&t.sender, &inputs);
// Advance halfway
t.set_time(now + 500);
- let w1 = client.get_withdrawable(&ids.get(0).unwrap()).unwrap();
- let w2 = client.get_withdrawable(&ids.get(1).unwrap()).unwrap();
+ let w1 = client.get_withdrawable(&ids.get(0).unwrap());
+ let w2 = client.get_withdrawable(&ids.get(1).unwrap());
assert!(w1 > 0);
assert!(w2 > 0);
// Withdraw from stream 1 only
- client.withdraw(&ids.get(0).unwrap(), &w1).unwrap();
+ client.withdraw(&ids.get(0).unwrap(), &w1);
assert_eq!(t.token().balance(&r1), w1);
assert_eq!(t.token().balance(&r2), 0); // stream 2 untouched
}
@@ -523,13 +527,13 @@ fn test_batch_streams_are_independently_cancellable() {
inputs.push_back(t.make_input(&r2, now));
let client = t.client();
- let ids = client.create_streams_batch(&t.sender, &inputs).unwrap();
+ let ids = client.create_streams_batch(&t.sender, &inputs);
// Cancel stream 1 only
- client.cancel(&ids.get(0).unwrap()).unwrap();
+ client.cancel(&ids.get(0).unwrap());
- let s1 = client.get_stream(&ids.get(0).unwrap()).unwrap();
- let s2 = client.get_stream(&ids.get(1).unwrap()).unwrap();
+ let s1 = client.get_stream(&ids.get(0).unwrap());
+ let s2 = client.get_stream(&ids.get(1).unwrap());
assert!(s1.cancelled);
assert!(!s2.cancelled);
}
diff --git a/contracts/streaming/src/test_integration.rs b/contracts/streaming/src/test_integration.rs
index 55f02dd..bf70934 100644
--- a/contracts/streaming/src/test_integration.rs
+++ b/contracts/streaming/src/test_integration.rs
@@ -34,7 +34,6 @@ struct TestEnv {
token_id: Address,
sender: Address,
recipient: Address,
- admin: Address,
}
impl TestEnv {
@@ -58,14 +57,20 @@ impl TestEnv {
let client = StreamingContractClient::new(&env, &contract_id);
client.initialize(&admin);
- TestEnv { env, contract_id, token_id, sender, recipient, admin }
+ TestEnv {
+ env,
+ contract_id,
+ token_id,
+ sender,
+ recipient,
+ }
}
- fn client(&self) -> StreamingContractClient {
+ fn client(&self) -> StreamingContractClient<'_> {
StreamingContractClient::new(&self.env, &self.contract_id)
}
- fn token(&self) -> TokenClient {
+ fn token(&self) -> TokenClient<'_> {
TokenClient::new(&self.env, &self.token_id)
}
@@ -93,21 +98,18 @@ impl TestEnv {
) -> u64 {
let now = self.env.ledger().timestamp();
self.approve(total_amount);
- self.client()
- .create_stream(
- &self.sender,
- &CreateStreamParams {
- recipient: self.recipient.clone(),
- token: self.token_id.clone(),
- total_amount,
- start_time: now,
- end_time: now + duration_secs,
- cliff_time: now + cliff_delay_secs,
- cliff_amount,
- metadata: None,
- },
- )
- .unwrap()
+ self.client().create_stream(
+ &self.sender,
+ &CreateStreamParams {
+ recipient: self.recipient.clone(),
+ token: self.token_id.clone(),
+ total_amount,
+ start_time: now,
+ end_time: now + duration_secs,
+ cliff_time: now + cliff_delay_secs,
+ cliff_amount,
+ },
+ )
}
}
@@ -134,7 +136,7 @@ fn test_scenario1_happy_path_full_lifecycle() {
let cliff_delay = 10u64;
let stream_id = t.create(total, duration, cliff_delay, 0);
- let stream = t.client().get_stream(&stream_id).unwrap();
+ let stream = t.client().get_stream(&stream_id);
assert_eq!(stream.deposited_amount, total);
assert_eq!(stream.withdrawn_amount, 0);
assert_eq!(stream.start_time, t0);
@@ -143,11 +145,11 @@ fn test_scenario1_happy_path_full_lifecycle() {
// ── Before cliff: nothing withdrawable ───────────────────────────────────
t.set_time(t0 + 5);
- assert_eq!(t.client().get_withdrawable(&stream_id).unwrap(), 0);
+ assert_eq!(t.client().get_withdrawable(&stream_id), 0);
// ── Past cliff: linear unlock has started ─────────────────────────────────
t.set_time(t0 + cliff_delay);
- let withdrawable_at_cliff = t.client().get_withdrawable(&stream_id).unwrap();
+ let withdrawable_at_cliff = t.client().get_withdrawable(&stream_id);
assert!(
withdrawable_at_cliff >= 0,
"should have some amount available at cliff"
@@ -155,7 +157,7 @@ fn test_scenario1_happy_path_full_lifecycle() {
// ── Advance to 20% through the stream; withdraw 200 tokens ───────────────
t.set_time(t0 + 20);
- let withdrawable_at_20 = t.client().get_withdrawable(&stream_id).unwrap();
+ let withdrawable_at_20 = t.client().get_withdrawable(&stream_id);
assert!(withdrawable_at_20 > 0);
// Withdraw exactly 200 tokens (within what's unlocked at t0+20)
@@ -166,21 +168,21 @@ fn test_scenario1_happy_path_full_lifecycle() {
);
let recipient_balance_before = t.token().balance(&t.recipient);
- t.client().withdraw(&stream_id, &withdraw_amount).unwrap();
+ t.client().withdraw(&stream_id, &withdraw_amount);
assert_eq!(
t.token().balance(&t.recipient),
recipient_balance_before + withdraw_amount
);
- let stream = t.client().get_stream(&stream_id).unwrap();
+ let stream = t.client().get_stream(&stream_id);
assert_eq!(stream.withdrawn_amount, withdraw_amount);
// ── Top up 500 more tokens ────────────────────────────────────────────────
let top_up_amount = 500_0000000i128;
t.approve(top_up_amount);
- t.client().top_up(&stream_id, &top_up_amount).unwrap();
+ t.client().top_up(&stream_id, &top_up_amount);
- let stream_after_topup = t.client().get_stream(&stream_id).unwrap();
+ let stream_after_topup = t.client().get_stream(&stream_id);
assert_eq!(stream_after_topup.deposited_amount, total + top_up_amount);
// Rate must be recalculated over remaining duration
assert!(
@@ -190,31 +192,32 @@ fn test_scenario1_happy_path_full_lifecycle() {
// ── Transfer stream to new_recipient ──────────────────────────────────────
let new_recipient = Address::generate(&t.env);
- t.client()
- .transfer_stream(&stream_id, &new_recipient)
- .unwrap();
+ t.client().transfer_stream(&stream_id, &new_recipient);
// old recipient no longer in active index for this stream
let old_received = t.client().get_received_streams(&t.recipient, &0, &100);
assert!(
- !old_received.contains(&stream_id),
+ !old_received.contains(stream_id),
"old recipient should no longer be in active index"
);
let new_received = t.client().get_received_streams(&new_recipient, &0, &100);
assert!(
- new_received.contains(&stream_id),
+ new_received.contains(stream_id),
"new recipient should appear in active index"
);
- let stream = t.client().get_stream(&stream_id).unwrap();
+ let stream = t.client().get_stream(&stream_id);
assert_eq!(stream.recipient, new_recipient);
// ── Advance to end; new_recipient drains the stream ───────────────────────
t.set_time(t0 + duration + 1); // past end_time
- let remaining = t.client().get_withdrawable(&stream_id).unwrap();
- assert!(remaining > 0, "new recipient should have tokens to withdraw");
+ let remaining = t.client().get_withdrawable(&stream_id);
+ assert!(
+ remaining > 0,
+ "new recipient should have tokens to withdraw"
+ );
- t.client().withdraw(&stream_id, &remaining).unwrap();
+ t.client().withdraw(&stream_id, &remaining);
// Contract balance should be zero (all funds distributed)
assert_eq!(
@@ -254,7 +257,7 @@ fn test_scenario2_cancel_after_topup_before_cliff() {
let stream_id = t.create(total, duration, cliff_delay, 0);
- let stream = t.client().get_stream(&stream_id).unwrap();
+ let stream = t.client().get_stream(&stream_id);
assert_eq!(stream.deposited_amount, total);
assert_eq!(stream.cliff_time, t0 + cliff_delay);
@@ -262,9 +265,9 @@ fn test_scenario2_cancel_after_topup_before_cliff() {
t.set_time(t0 + 20);
let top_up = 300_0000000i128;
t.approve(top_up);
- t.client().top_up(&stream_id, &top_up).unwrap();
+ t.client().top_up(&stream_id, &top_up);
- let stream = t.client().get_stream(&stream_id).unwrap();
+ let stream = t.client().get_stream(&stream_id);
assert_eq!(
stream.deposited_amount,
total + top_up,
@@ -273,7 +276,7 @@ fn test_scenario2_cancel_after_topup_before_cliff() {
// Nothing unlocked yet (before cliff)
assert_eq!(
- t.client().get_withdrawable(&stream_id).unwrap(),
+ t.client().get_withdrawable(&stream_id),
0,
"nothing should be withdrawable before cliff"
);
@@ -283,9 +286,9 @@ fn test_scenario2_cancel_after_topup_before_cliff() {
let sender_balance_before = t.token().balance(&t.sender);
let recipient_balance_before = t.token().balance(&t.recipient);
- t.client().cancel(&stream_id).unwrap();
+ t.client().cancel(&stream_id);
- let stream = t.client().get_stream(&stream_id).unwrap();
+ let stream = t.client().get_stream(&stream_id);
assert!(stream.cancelled);
// Sender gets back everything (no tokens vested before cliff)
@@ -305,14 +308,12 @@ fn test_scenario2_cancel_after_topup_before_cliff() {
// Stream moves to archived index, not active
let active_sent = t.client().get_sent_streams(&t.sender, &0, &100);
assert!(
- !active_sent.contains(&stream_id),
+ !active_sent.contains(stream_id),
"cancelled stream must leave active sent index"
);
- let archived_sent = t
- .client()
- .get_archived_sent_streams(&t.sender, &0, &100);
+ let archived_sent = t.client().get_archived_sent_streams(&t.sender, &0, &100);
assert!(
- archived_sent.contains(&stream_id),
+ archived_sent.contains(stream_id),
"cancelled stream must enter archived sent index"
);
}
@@ -339,31 +340,29 @@ fn test_scenario3_transfer_then_withdraw_race() {
// ── Advance to 40s; record unlocked ──────────────────────────────────────
t.set_time(t0 + 40);
- let unlocked_at_transfer = t.client().get_withdrawable(&stream_id).unwrap();
+ let unlocked_at_transfer = t.client().get_withdrawable(&stream_id);
assert!(unlocked_at_transfer > 0);
// ── Transfer to new_recipient ─────────────────────────────────────────────
let new_recipient = Address::generate(&t.env);
- t.client()
- .transfer_stream(&stream_id, &new_recipient)
- .unwrap();
+ t.client().transfer_stream(&stream_id, &new_recipient);
// Verify old recipient's index is cleared
let old_received = t.client().get_received_streams(&t.recipient, &0, &100);
assert!(
- !old_received.contains(&stream_id),
+ !old_received.contains(stream_id),
"old recipient's active index must not contain the transferred stream"
);
// Verify new recipient's index contains the stream
let new_received = t.client().get_received_streams(&new_recipient, &0, &100);
assert!(
- new_received.contains(&stream_id),
+ new_received.contains(stream_id),
"new recipient must appear in active index after transfer"
);
// ── new_recipient withdraws unlocked portion immediately after transfer ───
- let withdrawable_after_transfer = t.client().get_withdrawable(&stream_id).unwrap();
+ let withdrawable_after_transfer = t.client().get_withdrawable(&stream_id);
// Should be the same as before transfer (no time has passed)
assert_eq!(
withdrawable_after_transfer, unlocked_at_transfer,
@@ -371,8 +370,7 @@ fn test_scenario3_transfer_then_withdraw_race() {
);
t.client()
- .withdraw(&stream_id, &withdrawable_after_transfer)
- .unwrap();
+ .withdraw(&stream_id, &withdrawable_after_transfer);
assert_eq!(
t.token().balance(&new_recipient),
withdrawable_after_transfer
@@ -387,10 +385,10 @@ fn test_scenario3_transfer_then_withdraw_race() {
// ── Advance to end; new_recipient drains remainder ────────────────────────
t.set_time(t0 + duration + 1);
- let remainder = t.client().get_withdrawable(&stream_id).unwrap();
+ let remainder = t.client().get_withdrawable(&stream_id);
assert!(remainder > 0);
- t.client().withdraw(&stream_id, &remainder).unwrap();
+ t.client().withdraw(&stream_id, &remainder);
// Token conservation
assert_eq!(
@@ -434,25 +432,25 @@ fn test_scenario4_multiple_streams_same_parties_index_integrity() {
// Sender index has all 3
let sent = t.client().get_sent_streams(&t.sender, &0, &100);
assert_eq!(sent.len(), 3);
- assert!(sent.contains(&stream_a));
- assert!(sent.contains(&stream_b));
- assert!(sent.contains(&stream_c));
+ assert!(sent.contains(stream_a));
+ assert!(sent.contains(stream_b));
+ assert!(sent.contains(stream_c));
// Recipient index has all 3
let received = t.client().get_received_streams(&t.recipient, &0, &100);
assert_eq!(received.len(), 3);
- assert!(received.contains(&stream_a));
- assert!(received.contains(&stream_b));
- assert!(received.contains(&stream_c));
+ assert!(received.contains(stream_a));
+ assert!(received.contains(stream_b));
+ assert!(received.contains(stream_c));
// ── Operation A: Cancel stream_a at t0+40 ────────────────────────────────
t.set_time(t0 + 40);
let sender_before_cancel = t.token().balance(&t.sender);
let recipient_before_cancel = t.token().balance(&t.recipient);
- t.client().cancel(&stream_a).unwrap();
+ t.client().cancel(&stream_a);
- let stream_a_state = t.client().get_stream(&stream_a).unwrap();
+ let stream_a_state = t.client().get_stream(&stream_a);
assert!(stream_a_state.cancelled);
// Some tokens should go to recipient (40% unlocked), rest back to sender
@@ -475,7 +473,7 @@ fn test_scenario4_multiple_streams_same_parties_index_integrity() {
// stream_a must leave sender's active index
let sent_after_cancel = t.client().get_sent_streams(&t.sender, &0, &100);
- assert!(!sent_after_cancel.contains(&stream_a));
+ assert!(!sent_after_cancel.contains(stream_a));
assert_eq!(
sent_after_cancel.len(),
2,
@@ -484,69 +482,65 @@ fn test_scenario4_multiple_streams_same_parties_index_integrity() {
// stream_a must leave recipient's active index
let received_after_cancel = t.client().get_received_streams(&t.recipient, &0, &100);
- assert!(!received_after_cancel.contains(&stream_a));
+ assert!(!received_after_cancel.contains(stream_a));
assert_eq!(received_after_cancel.len(), 2);
// stream_a must enter archived indexes
assert!(t
.client()
.get_archived_sent_streams(&t.sender, &0, &100)
- .contains(&stream_a));
+ .contains(stream_a));
assert!(t
.client()
.get_archived_received_streams(&t.recipient, &0, &100)
- .contains(&stream_a));
+ .contains(stream_a));
// ── Operation B: Complete stream_b (recipient withdraws in full) ──────────
t.set_time(t0 + duration + 1); // past end_time
- let full_amount = t.client().get_withdrawable(&stream_b).unwrap();
+ let full_amount = t.client().get_withdrawable(&stream_b);
assert_eq!(
full_amount, total,
"after end_time the full deposited amount should be withdrawable"
);
- t.client().withdraw(&stream_b, &full_amount).unwrap();
+ t.client().withdraw(&stream_b, &full_amount);
- let stream_b_state = t.client().get_stream(&stream_b).unwrap();
+ let stream_b_state = t.client().get_stream(&stream_b);
assert_eq!(stream_b_state.withdrawn_amount, total);
// A fully drained stream moves to archive
let sent_after_complete = t.client().get_sent_streams(&t.sender, &0, &100);
- assert!(!sent_after_complete.contains(&stream_b));
+ assert!(!sent_after_complete.contains(stream_b));
assert_eq!(
sent_after_complete.len(),
1,
"sender active index should have only stream_c"
);
- assert!(sent_after_complete.contains(&stream_c));
+ assert!(sent_after_complete.contains(stream_c));
- let received_after_complete = t
- .client()
- .get_received_streams(&t.recipient, &0, &100);
- assert!(!received_after_complete.contains(&stream_b));
+ let received_after_complete = t.client().get_received_streams(&t.recipient, &0, &100);
+ assert!(!received_after_complete.contains(stream_b));
assert_eq!(received_after_complete.len(), 1);
- assert!(received_after_complete.contains(&stream_c));
+ assert!(received_after_complete.contains(stream_c));
// stream_b in archived indexes
assert!(t
.client()
.get_archived_sent_streams(&t.sender, &0, &100)
- .contains(&stream_b));
+ .contains(stream_b));
assert!(t
.client()
.get_archived_received_streams(&t.recipient, &0, &100)
- .contains(&stream_b));
+ .contains(stream_b));
// ── Operation C: Transfer stream_c to third_recipient ────────────────────
let third_recipient = Address::generate(&t.env);
- t.client()
- .transfer_stream(&stream_c, &third_recipient)
- .unwrap();
+ t.client().transfer_stream(&stream_c, &third_recipient);
// stream_c stays in sender's active index
let sent_final = t.client().get_sent_streams(&t.sender, &0, &100);
assert!(
- sent_final.contains(&stream_c),
+ sent_final.contains(stream_c),
"sender active index must still contain stream_c after transfer"
);
assert_eq!(sent_final.len(), 1);
@@ -554,7 +548,7 @@ fn test_scenario4_multiple_streams_same_parties_index_integrity() {
// stream_c must leave recipient's active index
let received_final = t.client().get_received_streams(&t.recipient, &0, &100);
assert!(
- !received_final.contains(&stream_c),
+ !received_final.contains(stream_c),
"stream_c must leave recipient active index after transfer"
);
assert_eq!(
@@ -564,11 +558,9 @@ fn test_scenario4_multiple_streams_same_parties_index_integrity() {
);
// stream_c must enter third_recipient's active index
- let third_received = t
- .client()
- .get_received_streams(&third_recipient, &0, &100);
+ let third_received = t.client().get_received_streams(&third_recipient, &0, &100);
assert!(
- third_received.contains(&stream_c),
+ third_received.contains(stream_c),
"third_recipient active index must contain stream_c"
);
assert_eq!(third_received.len(), 1);
@@ -577,7 +569,7 @@ fn test_scenario4_multiple_streams_same_parties_index_integrity() {
// stream_a: distributed to sender/recipient at cancel
// stream_b: fully withdrawn by recipient
// stream_c: still in contract (not withdrawn yet)
- let stream_c_state = t.client().get_stream(&stream_c).unwrap();
+ let stream_c_state = t.client().get_stream(&stream_c);
assert_eq!(stream_c_state.recipient, third_recipient);
assert_eq!(
t.token().balance(&t.contract_id),
@@ -586,7 +578,7 @@ fn test_scenario4_multiple_streams_same_parties_index_integrity() {
);
// ── third_recipient can withdraw from stream_c ────────────────────────────
- let withdrawable_c = t.client().get_withdrawable(&stream_c).unwrap();
+ let withdrawable_c = t.client().get_withdrawable(&stream_c);
assert!(
withdrawable_c > 0,
"third_recipient should be able to withdraw from transferred stream"
@@ -614,9 +606,7 @@ fn test_scenario4b_old_recipient_cannot_withdraw_after_transfer() {
// Transfer to new_recipient
let new_recipient = Address::generate(&t.env);
- t.client()
- .transfer_stream(&stream_id, &new_recipient)
- .unwrap();
+ t.client().transfer_stream(&stream_id, &new_recipient);
// Old recipient is no longer in index
let old_received = t.client().get_received_streams(&t.recipient, &0, &100);
@@ -627,7 +617,7 @@ fn test_scenario4b_old_recipient_cannot_withdraw_after_transfer() {
);
// Verify stream now points to new_recipient
- let stream = t.client().get_stream(&stream_id).unwrap();
+ let stream = t.client().get_stream(&stream_id);
assert_eq!(
stream.recipient, new_recipient,
"stream.recipient must be new_recipient after transfer"
@@ -639,5 +629,5 @@ fn test_scenario4b_old_recipient_cannot_withdraw_after_transfer() {
// New recipient's index updated
let new_received = t.client().get_received_streams(&new_recipient, &0, &100);
- assert!(new_received.contains(&stream_id));
+ assert!(new_received.contains(stream_id));
}
diff --git a/contracts/streaming/src/test_security.rs b/contracts/streaming/src/test_security.rs
index c462067..be8c229 100644
--- a/contracts/streaming/src/test_security.rs
+++ b/contracts/streaming/src/test_security.rs
@@ -18,7 +18,6 @@ struct Ctx {
sender: Address,
recipient: Address,
attacker: Address,
- admin: Address,
}
impl Ctx {
@@ -31,12 +30,21 @@ impl Ctx {
let attacker = Address::generate(&env);
let admin = Address::generate(&env);
let token_admin = Address::generate(&env);
- let token_id = env.register_stellar_asset_contract_v2(token_admin.clone()).address();
+ let token_id = env
+ .register_stellar_asset_contract_v2(token_admin.clone())
+ .address();
let asset = StellarAssetClient::new(&env, &token_id);
- asset.mint(&sender, &10_000_000_0000000);
+ asset.mint(&sender, &10_000_000_0000000);
asset.mint(&attacker, &10_000_000_0000000);
StreamingContractClient::new(&env, &contract_id).initialize(&admin);
- Ctx { env, contract_id, token_id, sender, recipient, attacker, admin }
+ Ctx {
+ env,
+ contract_id,
+ token_id,
+ sender,
+ recipient,
+ attacker,
+ }
}
fn client(&self) -> StreamingContractClient<'_> {
@@ -54,7 +62,9 @@ impl Ctx {
fn create_basic_stream(&self, now: u64) -> u64 {
let total = 1_000_0000000i128;
self.token().approve(
- &self.sender, &self.contract_id, &total,
+ &self.sender,
+ &self.contract_id,
+ &total,
&(self.env.ledger().sequence() + 500),
);
self.client().create_stream(
@@ -68,7 +78,7 @@ impl Ctx {
cliff_time: now,
cliff_amount: 0,
},
- ).unwrap()
+ )
}
}
@@ -94,7 +104,7 @@ fn test_auth_attacker_cannot_withdraw() {
sub_invokes: &[],
},
}]);
- ctx.client().withdraw(&id, &1_0000000).unwrap();
+ ctx.client().withdraw(&id, &1_0000000);
}
/// Attacker cannot cancel a stream they did not create.
@@ -114,7 +124,7 @@ fn test_auth_attacker_cannot_cancel() {
sub_invokes: &[],
},
}]);
- ctx.client().cancel(&id).unwrap();
+ ctx.client().cancel(&id);
}
/// Recipient cannot cancel their own incoming stream.
@@ -134,7 +144,7 @@ fn test_auth_recipient_cannot_cancel() {
sub_invokes: &[],
},
}]);
- ctx.client().cancel(&id).unwrap();
+ ctx.client().cancel(&id);
}
/// Non-admin cannot upgrade the contract.
@@ -173,7 +183,7 @@ fn test_auth_sender_cannot_withdraw() {
sub_invokes: &[],
},
}]);
- ctx.client().withdraw(&id, &1_0000000).unwrap();
+ ctx.client().withdraw(&id, &1_0000000);
}
// ═══════════════════════════════════════════════════════════════════
@@ -218,16 +228,16 @@ fn test_no_overdraw_multiple_partial_withdrawals() {
for i in 1..=10u64 {
ctx.set_time(now + i * 100);
- let w = ctx.client().get_withdrawable(&id).unwrap();
+ let w = ctx.client().get_withdrawable(&id);
if w > 0 {
- ctx.client().withdraw(&id, &w).unwrap();
+ ctx.client().withdraw(&id, &w);
total_withdrawn += w;
}
}
ctx.set_time(now + 2000);
- let final_w = ctx.client().get_withdrawable(&id).unwrap();
+ let final_w = ctx.client().get_withdrawable(&id);
if final_w > 0 {
- ctx.client().withdraw(&id, &final_w).unwrap();
+ ctx.client().withdraw(&id, &final_w);
total_withdrawn += final_w;
}
@@ -243,7 +253,7 @@ fn test_withdraw_more_than_withdrawable() {
ctx.set_time(now);
let id = ctx.create_basic_stream(now);
ctx.set_time(now + 500);
- let withdrawable = ctx.client().get_withdrawable(&id).unwrap();
+ let withdrawable = ctx.client().get_withdrawable(&id);
let result = ctx.client().try_withdraw(&id, &(withdrawable + 1));
assert_eq!(result, Err(Ok(StreamError::InsufficientFunds)));
}
@@ -283,14 +293,14 @@ fn test_cancel_conservation_with_prior_withdrawal() {
let total = 1_000_0000000i128;
ctx.set_time(now + 300);
- let w = ctx.client().get_withdrawable(&id).unwrap();
- ctx.client().withdraw(&id, &w).unwrap();
+ let w = ctx.client().get_withdrawable(&id);
+ ctx.client().withdraw(&id, &w);
let recipient_before = ctx.token().balance(&ctx.recipient);
let sender_before = ctx.token().balance(&ctx.sender);
ctx.set_time(now + 500);
- ctx.client().cancel(&id).unwrap();
+ ctx.client().cancel(&id);
let recipient_got = ctx.token().balance(&ctx.recipient) - recipient_before;
let sender_got = ctx.token().balance(&ctx.sender) - sender_before;
@@ -309,10 +319,10 @@ fn test_cancel_after_full_withdrawal() {
let total = 1_000_0000000i128;
ctx.set_time(now + 2000);
- ctx.client().withdraw(&id, &total).unwrap();
- ctx.client().cancel(&id).unwrap();
+ ctx.client().withdraw(&id, &total);
+ ctx.client().cancel(&id);
- assert!(ctx.client().get_stream(&id).unwrap().cancelled);
+ assert!(ctx.client().get_stream(&id).cancelled);
assert_eq!(ctx.token().balance(&ctx.contract_id), 0);
}
@@ -323,7 +333,12 @@ fn test_cancel_before_start_full_refund() {
ctx.set_time(now);
let total = 1_000_0000000i128;
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &total, &(ctx.env.ledger().sequence() + 500));
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
let id = ctx.client().create_stream(
&ctx.sender,
&CreateStreamParams {
@@ -335,10 +350,10 @@ fn test_cancel_before_start_full_refund() {
cliff_time: now + 500,
cliff_amount: 0,
},
- ).unwrap();
+ );
let sender_before = ctx.token().balance(&ctx.sender);
- ctx.client().cancel(&id).unwrap();
+ ctx.client().cancel(&id);
assert_eq!(ctx.token().balance(&ctx.sender) - sender_before, total);
assert_eq!(ctx.token().balance(&ctx.recipient), 0);
@@ -355,7 +370,7 @@ fn test_cancel_at_end_time() {
ctx.set_time(now + 1000);
let sender_before = ctx.token().balance(&ctx.sender);
- ctx.client().cancel(&id).unwrap();
+ ctx.client().cancel(&id);
assert_eq!(ctx.token().balance(&ctx.sender), sender_before);
assert_eq!(ctx.token().balance(&ctx.recipient), total);
@@ -373,7 +388,12 @@ fn test_cliff_amount_equals_total() {
ctx.set_time(now);
let total = 1_000_0000000i128;
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &total, &(ctx.env.ledger().sequence() + 500));
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
let id = ctx.client().create_stream(
&ctx.sender,
&CreateStreamParams {
@@ -385,16 +405,16 @@ fn test_cliff_amount_equals_total() {
cliff_time: now + 200,
cliff_amount: total,
},
- ).unwrap();
+ );
ctx.set_time(now + 199);
- assert_eq!(ctx.client().get_withdrawable(&id).unwrap(), 0);
+ assert_eq!(ctx.client().get_withdrawable(&id), 0);
ctx.set_time(now + 200);
- assert_eq!(ctx.client().get_withdrawable(&id).unwrap(), total);
+ assert_eq!(ctx.client().get_withdrawable(&id), total);
ctx.set_time(now + 800);
- assert_eq!(ctx.client().get_withdrawable(&id).unwrap(), total);
+ assert_eq!(ctx.client().get_withdrawable(&id), total);
}
#[test]
@@ -404,7 +424,12 @@ fn test_cliff_time_equals_end_time() {
ctx.set_time(now);
let total = 1_000_0000000i128;
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &total, &(ctx.env.ledger().sequence() + 500));
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
let id = ctx.client().create_stream(
&ctx.sender,
&CreateStreamParams {
@@ -416,13 +441,13 @@ fn test_cliff_time_equals_end_time() {
cliff_time: now + 1000,
cliff_amount: 500_0000000,
},
- ).unwrap();
+ );
ctx.set_time(now + 999);
- assert_eq!(ctx.client().get_withdrawable(&id).unwrap(), 0);
+ assert_eq!(ctx.client().get_withdrawable(&id), 0);
ctx.set_time(now + 1000);
- assert_eq!(ctx.client().get_withdrawable(&id).unwrap(), total);
+ assert_eq!(ctx.client().get_withdrawable(&id), total);
}
#[test]
@@ -433,7 +458,12 @@ fn test_withdraw_exactly_at_cliff() {
let total = 1_000_0000000i128;
let cliff_amt = 200_0000000i128;
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &total, &(ctx.env.ledger().sequence() + 500));
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
let id = ctx.client().create_stream(
&ctx.sender,
&CreateStreamParams {
@@ -445,12 +475,12 @@ fn test_withdraw_exactly_at_cliff() {
cliff_time: now + 200,
cliff_amount: cliff_amt,
},
- ).unwrap();
+ );
ctx.set_time(now + 200);
- let w = ctx.client().get_withdrawable(&id).unwrap();
+ let w = ctx.client().get_withdrawable(&id);
assert!(w > 0);
- ctx.client().withdraw(&id, &w).unwrap();
+ ctx.client().withdraw(&id, &w);
assert_eq!(ctx.token().balance(&ctx.recipient), w);
}
@@ -461,7 +491,12 @@ fn test_nothing_withdrawable_before_cliff() {
ctx.set_time(now);
let total = 1_000_0000000i128;
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &total, &(ctx.env.ledger().sequence() + 500));
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
let id = ctx.client().create_stream(
&ctx.sender,
&CreateStreamParams {
@@ -473,10 +508,10 @@ fn test_nothing_withdrawable_before_cliff() {
cliff_time: now + 500,
cliff_amount: 0,
},
- ).unwrap();
+ );
ctx.set_time(now + 499);
- assert_eq!(ctx.client().get_withdrawable(&id).unwrap(), 0);
+ assert_eq!(ctx.client().get_withdrawable(&id), 0);
}
#[test]
@@ -486,7 +521,12 @@ fn test_withdraw_before_cliff_returns_error() {
ctx.set_time(now);
let total = 1_000_0000000i128;
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &total, &(ctx.env.ledger().sequence() + 500));
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
let id = ctx.client().create_stream(
&ctx.sender,
&CreateStreamParams {
@@ -498,7 +538,7 @@ fn test_withdraw_before_cliff_returns_error() {
cliff_time: now + 500,
cliff_amount: 0,
},
- ).unwrap();
+ );
ctx.set_time(now + 100);
let result = ctx.client().try_withdraw(&id, &1_0000000);
@@ -516,7 +556,12 @@ fn test_rounding_dust_stays_in_contract() {
ctx.set_time(now);
let total = 1_000_000_0000001i128;
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &total, &(ctx.env.ledger().sequence() + 500));
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
let id = ctx.client().create_stream(
&ctx.sender,
&CreateStreamParams {
@@ -528,12 +573,12 @@ fn test_rounding_dust_stays_in_contract() {
cliff_time: now,
cliff_amount: 0,
},
- ).unwrap();
+ );
ctx.set_time(now + 1000);
- let w = ctx.client().get_withdrawable(&id).unwrap();
+ let w = ctx.client().get_withdrawable(&id);
assert_eq!(w, total);
- ctx.client().withdraw(&id, &w).unwrap();
+ ctx.client().withdraw(&id, &w);
assert_eq!(ctx.token().balance(&ctx.recipient), total);
assert_eq!(ctx.token().balance(&ctx.contract_id), 0);
}
@@ -548,7 +593,12 @@ fn test_large_amounts_no_overflow() {
let asset = StellarAssetClient::new(&ctx.env, &ctx.token_id);
asset.mint(&ctx.sender, &large_total);
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &large_total, &(ctx.env.ledger().sequence() + 500));
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &large_total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
let id = ctx.client().create_stream(
&ctx.sender,
&CreateStreamParams {
@@ -560,15 +610,15 @@ fn test_large_amounts_no_overflow() {
cliff_time: now,
cliff_amount: 0,
},
- ).unwrap();
+ );
ctx.set_time(now + 15_768_000);
- let w = ctx.client().get_withdrawable(&id).unwrap();
+ let w = ctx.client().get_withdrawable(&id);
assert!(w > 0);
assert!(w < large_total);
ctx.set_time(now + 31_536_001);
- assert_eq!(ctx.client().get_withdrawable(&id).unwrap(), large_total);
+ assert_eq!(ctx.client().get_withdrawable(&id), large_total);
}
#[test]
@@ -578,7 +628,12 @@ fn test_minimum_duration_stream() {
ctx.set_time(now);
let total = 1_000_0000000i128;
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &total, &(ctx.env.ledger().sequence() + 500));
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
let id = ctx.client().create_stream(
&ctx.sender,
&CreateStreamParams {
@@ -590,11 +645,11 @@ fn test_minimum_duration_stream() {
cliff_time: now,
cliff_amount: 0,
},
- ).unwrap();
+ );
ctx.set_time(now + 1);
- assert_eq!(ctx.client().get_withdrawable(&id).unwrap(), total);
- ctx.client().withdraw(&id, &total).unwrap();
+ assert_eq!(ctx.client().get_withdrawable(&id), total);
+ ctx.client().withdraw(&id, &total);
assert_eq!(ctx.token().balance(&ctx.recipient), total);
}
@@ -602,6 +657,10 @@ fn test_minimum_duration_stream() {
// 7. SELF-STREAM (sender == recipient)
// ═══════════════════════════════════════════════════════════════════
+// Self-streams (sender == recipient) are rejected outright at creation
+// (see test_create_stream_self_rejected in test.rs), so there is no
+// cancel/withdraw fund-accounting path to exercise for them — creation
+// itself must fail first.
#[test]
fn test_self_stream_cancel_no_double_pay() {
let ctx = Ctx::new();
@@ -609,8 +668,13 @@ fn test_self_stream_cancel_no_double_pay() {
ctx.set_time(now);
let total = 1_000_0000000i128;
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &total, &(ctx.env.ledger().sequence() + 500));
- let id = ctx.client().create_stream(
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
+ let result = ctx.client().try_create_stream(
&ctx.sender,
&CreateStreamParams {
recipient: ctx.sender.clone(),
@@ -621,17 +685,9 @@ fn test_self_stream_cancel_no_double_pay() {
cliff_time: now,
cliff_amount: 0,
},
- ).unwrap();
-
- let balance_before = ctx.token().balance(&ctx.sender);
- ctx.set_time(now + 500);
- ctx.client().cancel(&id).unwrap();
-
- let balance_after = ctx.token().balance(&ctx.sender);
- let contract_balance = ctx.token().balance(&ctx.contract_id);
+ );
- assert_eq!(balance_after - balance_before + contract_balance, total);
- assert!(contract_balance < 1000);
+ assert_eq!(result, Err(Ok(StreamError::SelfStream)));
}
#[test]
@@ -641,8 +697,13 @@ fn test_self_stream_withdraw() {
ctx.set_time(now);
let total = 1_000_0000000i128;
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &total, &(ctx.env.ledger().sequence() + 500));
- let id = ctx.client().create_stream(
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
+ let result = ctx.client().try_create_stream(
&ctx.sender,
&CreateStreamParams {
recipient: ctx.sender.clone(),
@@ -653,14 +714,11 @@ fn test_self_stream_withdraw() {
cliff_time: now,
cliff_amount: 0,
},
- ).unwrap();
+ );
- ctx.set_time(now + 2000);
- ctx.client().withdraw(&id, &total).unwrap();
- assert_eq!(ctx.token().balance(&ctx.contract_id), 0);
+ assert_eq!(result, Err(Ok(StreamError::SelfStream)));
}
-
// ═══════════════════════════════════════════════════════════════════
// 8. CREATE PARAM VALIDATION
// ═══════════════════════════════════════════════════════════════════
@@ -671,7 +729,12 @@ fn test_cliff_before_start_time() {
let now = 1_000_000u64;
ctx.set_time(now);
let total = 1_000_0000000i128;
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &total, &(ctx.env.ledger().sequence() + 500));
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
let result = ctx.client().try_create_stream(
&ctx.sender,
&CreateStreamParams {
@@ -693,7 +756,12 @@ fn test_cliff_after_end_time() {
let now = 1_000_000u64;
ctx.set_time(now);
let total = 1_000_0000000i128;
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &total, &(ctx.env.ledger().sequence() + 500));
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
let result = ctx.client().try_create_stream(
&ctx.sender,
&CreateStreamParams {
@@ -715,7 +783,12 @@ fn test_cliff_amount_exceeds_total() {
let now = 1_000_000u64;
ctx.set_time(now);
let total = 1_000_0000000i128;
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &total, &(ctx.env.ledger().sequence() + 500));
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
let result = ctx.client().try_create_stream(
&ctx.sender,
&CreateStreamParams {
@@ -737,7 +810,12 @@ fn test_negative_cliff_amount() {
let now = 1_000_000u64;
ctx.set_time(now);
let total = 1_000_0000000i128;
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &total, &(ctx.env.ledger().sequence() + 500));
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
let result = ctx.client().try_create_stream(
&ctx.sender,
&CreateStreamParams {
@@ -779,7 +857,12 @@ fn test_end_time_equals_start_time() {
let now = 1_000_000u64;
ctx.set_time(now);
let total = 1_000_0000000i128;
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &total, &(ctx.env.ledger().sequence() + 500));
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
let result = ctx.client().try_create_stream(
&ctx.sender,
&CreateStreamParams {
@@ -801,14 +884,19 @@ fn test_end_time_equals_start_time() {
/// Reject dust stream: very small amount over long duration results in zero rate.
#[test]
-#[should_panic(expected = "stream amount too small for duration — rate would be 0")]
+#[should_panic(expected = "stream amount too small for duration")]
fn test_dust_stream_rejected() {
let ctx = Ctx::new();
let now = 1_000_000u64;
ctx.set_time(now);
// 100 stroops over 1 year (31,536,000 seconds) = 0 per second due to integer division
let total = 100i128;
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &total, &(ctx.env.ledger().sequence() + 500));
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
ctx.client().create_stream(
&ctx.sender,
&CreateStreamParams {
@@ -825,14 +913,19 @@ fn test_dust_stream_rejected() {
/// Reject dust stream with cliff: linear amount too small for duration.
#[test]
-#[should_panic(expected = "stream amount too small for duration — rate would be 0")]
+#[should_panic(expected = "stream amount too small for duration")]
fn test_dust_stream_with_cliff_rejected() {
let ctx = Ctx::new();
let now = 1_000_000u64;
ctx.set_time(now);
// Total 1000, but cliff takes 999, leaving only 1 for linear over 1 year
let total = 1000i128;
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &total, &(ctx.env.ledger().sequence() + 500));
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
ctx.client().create_stream(
&ctx.sender,
&CreateStreamParams {
@@ -854,7 +947,12 @@ fn test_all_cliff_stream_accepted() {
let now = 1_000_000u64;
ctx.set_time(now);
let total = 1_000_0000000i128;
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &total, &(ctx.env.ledger().sequence() + 500));
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
let id = ctx.client().create_stream(
&ctx.sender,
&CreateStreamParams {
@@ -871,15 +969,23 @@ fn test_all_cliff_stream_accepted() {
assert!(id > 0);
}
-/// Top-up that would result in zero rate should be rejected.
+/// A tiny top-up with only a moment of the stream left must still succeed:
+/// `top_up` re-anchors its vesting math off `unlocked_amount` (the same
+/// function withdraw/cancel use), which guarantees `remaining >=
+/// remaining_seconds` given `create_stream`'s own rate>=1 invariant, so a
+/// legitimate top-up can never actually produce a zero rate here.
#[test]
-#[should_panic(expected = "stream amount too small for remaining duration — rate would be 0")]
-fn test_top_up_dust_stream_rejected() {
+fn test_top_up_near_end_succeeds() {
let ctx = Ctx::new();
let now = 1_000_000u64;
ctx.set_time(now);
let total = 1_000_0000000i128;
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &total, &(ctx.env.ledger().sequence() + 500));
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
let id = ctx.client().create_stream(
&ctx.sender,
&CreateStreamParams {
@@ -896,10 +1002,83 @@ fn test_top_up_dust_stream_rejected() {
// Advance to near end
ctx.set_time(now + 999);
- // Try to add tiny amount that would result in zero rate
let tiny_addition = 1i128;
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &tiny_addition, &(ctx.env.ledger().sequence() + 500));
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &tiny_addition,
+ &(ctx.env.ledger().sequence() + 500),
+ );
ctx.client().top_up(&id, &tiny_addition);
+
+ let stream = ctx.client().get_stream(&id);
+ assert_eq!(stream.deposited_amount, total + tiny_addition);
+ assert!(stream.amount_per_second > 0);
+}
+
+/// Regression test for a real bug: `top_up` used to recompute vesting via a
+/// separate formula (`amount_per_second * elapsed-since-cliff`) that
+/// diverged from `unlocked_amount` (what withdraw/cancel actually use).
+/// After a rate increase, that separate formula retroactively re-applied
+/// the new (higher) rate across the *entire* elapsed time instead of just
+/// the time since the last top-up, overstating how much had vested. Two
+/// top-ups in a row could make it think the stream was almost fully vested
+/// when it wasn't, spuriously rejecting a perfectly reasonable follow-up
+/// top-up as "dust". This also meant, more broadly, that funds added via
+/// top-up did not actually stream out progressively before the fix — they
+/// were invisible to `unlocked_amount` until `end_time`.
+#[test]
+fn test_top_up_twice_then_withdraw_mid_stream() {
+ let ctx = Ctx::new();
+ let now = 1_000_000u64;
+ ctx.set_time(now);
+ let total = 1000i128;
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
+ let id = ctx.client().create_stream(
+ &ctx.sender,
+ &CreateStreamParams {
+ recipient: ctx.recipient.clone(),
+ token: ctx.token_id.clone(),
+ total_amount: total,
+ start_time: now,
+ end_time: now + 1000,
+ cliff_time: now,
+ cliff_amount: 0,
+ },
+ );
+
+ // First top-up, 990 tokens deposited: pulls the rate up sharply.
+ ctx.set_time(now + 990);
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &990i128,
+ &(ctx.env.ledger().sequence() + 500),
+ );
+ ctx.client().top_up(&id, &990i128);
+
+ // Second top-up a second later must not panic, and must not think the
+ // stream is already almost fully vested.
+ ctx.set_time(now + 991);
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &1i128,
+ &(ctx.env.ledger().sequence() + 500),
+ );
+ ctx.client().top_up(&id, &1i128);
+
+ // With 9 seconds still remaining, funds must be streaming out
+ // progressively rather than sitting locked until end_time.
+ ctx.set_time(now + 995);
+ let withdrawable = ctx.client().get_withdrawable(&id);
+ assert!(withdrawable > 0);
+ assert!(withdrawable < ctx.client().get_stream(&id).deposited_amount);
}
// ═══════════════════════════════════════════════════════════════════
@@ -914,7 +1093,12 @@ fn test_recipient_is_contract_rejected() {
let now = 1_000_000u64;
ctx.set_time(now);
let total = 1_000_0000000i128;
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &total, &(ctx.env.ledger().sequence() + 500));
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
ctx.client().create_stream(
&ctx.sender,
&CreateStreamParams {
@@ -936,7 +1120,12 @@ fn test_valid_recipient_accepted() {
let now = 1_000_000u64;
ctx.set_time(now);
let total = 1_000_0000000i128;
- ctx.token().approve(&ctx.sender, &ctx.contract_id, &total, &(ctx.env.ledger().sequence() + 500));
+ ctx.token().approve(
+ &ctx.sender,
+ &ctx.contract_id,
+ &total,
+ &(ctx.env.ledger().sequence() + 500),
+ );
let id = ctx.client().create_stream(
&ctx.sender,
&CreateStreamParams {
diff --git a/contracts/streaming/test_snapshots/test/test_cancel_midway.1.json b/contracts/streaming/test_snapshots/test/test_cancel_midway.1.json
index f1ee4c7..27bb3d0 100644
--- a/contracts/streaming/test_snapshots/test/test_cancel_midway.1.json
+++ b/contracts/streaming/test_snapshots/test/test_cancel_midway.1.json
@@ -48,7 +48,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -260,7 +278,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -291,7 +309,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -318,7 +336,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -345,7 +363,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -525,6 +543,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -544,7 +574,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "1033654523790656264"
+ "nonce": "2032731177588607455"
}
},
"durability": "temporary",
@@ -564,7 +594,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "2032731177588607455"
+ "nonce": "4270020994084947596"
}
},
"durability": "temporary",
@@ -595,6 +625,26 @@
},
"live_until": 6311999
},
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
{
"entry": {
"last_modified_ledger_seq": 0,
diff --git a/contracts/streaming/test_snapshots/test/test_cancel_twice.1.json b/contracts/streaming/test_snapshots/test/test_cancel_twice.1.json
index 3e601c6..3d6cdfb 100644
--- a/contracts/streaming/test_snapshots/test/test_cancel_twice.1.json
+++ b/contracts/streaming/test_snapshots/test/test_cancel_twice.1.json
@@ -48,7 +48,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -257,7 +275,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -288,7 +306,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -315,7 +333,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -342,7 +360,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -522,6 +540,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -541,7 +571,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "1033654523790656264"
+ "nonce": "2032731177588607455"
}
},
"durability": "temporary",
@@ -561,7 +591,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "2032731177588607455"
+ "nonce": "4270020994084947596"
}
},
"durability": "temporary",
@@ -592,6 +622,26 @@
},
"live_until": 6311999
},
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
{
"entry": {
"last_modified_ledger_seq": 0,
diff --git a/contracts/streaming/test_snapshots/test/test_create_stream_basic.1.json b/contracts/streaming/test_snapshots/test/test_create_stream_basic.1.json
index a80641a..be020bd 100644
--- a/contracts/streaming/test_snapshots/test/test_create_stream_basic.1.json
+++ b/contracts/streaming/test_snapshots/test/test_create_stream_basic.1.json
@@ -48,7 +48,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -240,7 +258,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -271,7 +289,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -451,6 +469,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -470,7 +500,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "1033654523790656264"
+ "nonce": "2032731177588607455"
}
},
"durability": "temporary",
@@ -501,6 +531,26 @@
},
"live_until": 6311999
},
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
{
"entry": {
"last_modified_ledger_seq": 0,
diff --git a/contracts/streaming/test_snapshots/test/test_create_stream_invalid_times.1.json b/contracts/streaming/test_snapshots/test/test_create_stream_invalid_times.1.json
index a38cb36..8571dce 100644
--- a/contracts/streaming/test_snapshots/test/test_create_stream_invalid_times.1.json
+++ b/contracts/streaming/test_snapshots/test/test_create_stream_invalid_times.1.json
@@ -48,7 +48,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -156,6 +174,18 @@
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -173,6 +203,26 @@
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
"key": {
"ledger_key_nonce": {
"nonce": "1033654523790656264"
diff --git a/contracts/streaming/test_snapshots/test/test_create_stream_self_rejected.1.json b/contracts/streaming/test_snapshots/test/test_create_stream_self_rejected.1.json
index a38cb36..8571dce 100644
--- a/contracts/streaming/test_snapshots/test/test_create_stream_self_rejected.1.json
+++ b/contracts/streaming/test_snapshots/test/test_create_stream_self_rejected.1.json
@@ -48,7 +48,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -156,6 +174,18 @@
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -173,6 +203,26 @@
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
"key": {
"ledger_key_nonce": {
"nonce": "1033654523790656264"
diff --git a/contracts/streaming/test_snapshots/test/test_create_stream_with_cliff.1.json b/contracts/streaming/test_snapshots/test/test_create_stream_with_cliff.1.json
index b5b1d45..cb2b004 100644
--- a/contracts/streaming/test_snapshots/test/test_create_stream_with_cliff.1.json
+++ b/contracts/streaming/test_snapshots/test/test_create_stream_with_cliff.1.json
@@ -48,7 +48,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -239,7 +257,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -270,7 +288,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -450,6 +468,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -469,7 +499,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "1033654523790656264"
+ "nonce": "2032731177588607455"
}
},
"durability": "temporary",
@@ -500,6 +530,26 @@
},
"live_until": 6311999
},
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
{
"entry": {
"last_modified_ledger_seq": 0,
diff --git a/contracts/streaming/test_snapshots/test/test_create_stream_zero_amount.1.json b/contracts/streaming/test_snapshots/test/test_create_stream_zero_amount.1.json
index 4bccb1c..34a3048 100644
--- a/contracts/streaming/test_snapshots/test/test_create_stream_zero_amount.1.json
+++ b/contracts/streaming/test_snapshots/test/test_create_stream_zero_amount.1.json
@@ -48,7 +48,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -156,6 +174,18 @@
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -173,6 +203,26 @@
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
"key": {
"ledger_key_nonce": {
"nonce": "1033654523790656264"
diff --git a/contracts/streaming/test_snapshots/test/test_incrementing_ids.1.json b/contracts/streaming/test_snapshots/test/test_incrementing_ids.1.json
index 80d5ccd..1854ad6 100644
--- a/contracts/streaming/test_snapshots/test/test_incrementing_ids.1.json
+++ b/contracts/streaming/test_snapshots/test/test_incrementing_ids.1.json
@@ -48,7 +48,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -457,7 +475,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -494,7 +512,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -954,6 +972,18 @@
"val": {
"u64": "3"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -973,7 +1003,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "1033654523790656264"
+ "nonce": "2032731177588607455"
}
},
"durability": "temporary",
@@ -993,7 +1023,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "2032731177588607455"
+ "nonce": "4270020994084947596"
}
},
"durability": "temporary",
@@ -1013,7 +1043,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "4270020994084947596"
+ "nonce": "4837995959683129791"
}
},
"durability": "temporary",
@@ -1033,7 +1063,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "4837995959683129791"
+ "nonce": "5806905060045992000"
}
},
"durability": "temporary",
@@ -1084,6 +1114,26 @@
},
"live_until": 6311999
},
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
{
"entry": {
"last_modified_ledger_seq": 0,
@@ -1432,6 +1482,14 @@
],
"data": {
"map": [
+ {
+ "key": {
+ "symbol": "cliff_time"
+ },
+ "val": {
+ "u64": "1000000"
+ }
+ },
{
"key": {
"symbol": "deposited_amount"
@@ -1440,6 +1498,38 @@
"i128": "10000000000"
}
},
+ {
+ "key": {
+ "symbol": "end_time"
+ },
+ "val": {
+ "u64": "1001000"
+ }
+ },
+ {
+ "key": {
+ "symbol": "recipient"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ },
+ {
+ "key": {
+ "symbol": "sender"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ },
+ {
+ "key": {
+ "symbol": "start_time"
+ },
+ "val": {
+ "u64": "1000000"
+ }
+ },
{
"key": {
"symbol": "stream_id"
@@ -1447,6 +1537,22 @@
"val": {
"u64": "3"
}
+ },
+ {
+ "key": {
+ "symbol": "timestamp"
+ },
+ "val": {
+ "u64": "1000000"
+ }
+ },
+ {
+ "key": {
+ "symbol": "token"
+ },
+ "val": {
+ "address": "CACMVW2KK4H5FZDFF2AUCAKQTEJMZZWJUIZF23XMRVYQBSXYLHZ6BKWN"
+ }
}
]
}
diff --git a/contracts/streaming/test_snapshots/test/test_initialize_twice_panics.1.json b/contracts/streaming/test_snapshots/test/test_initialize_twice_panics.1.json
index 9e045c1..442d162 100644
--- a/contracts/streaming/test_snapshots/test/test_initialize_twice_panics.1.json
+++ b/contracts/streaming/test_snapshots/test/test_initialize_twice_panics.1.json
@@ -48,7 +48,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[]
],
"ledger": {
@@ -128,6 +146,18 @@
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -138,6 +168,26 @@
},
"live_until": 17280
},
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
{
"entry": {
"last_modified_ledger_seq": 0,
diff --git a/contracts/streaming/test_snapshots/test/test_migrate.1.json b/contracts/streaming/test_snapshots/test/test_migrate.1.json
index e78cfdf..8d6a294 100644
--- a/contracts/streaming/test_snapshots/test/test_migrate.1.json
+++ b/contracts/streaming/test_snapshots/test/test_migrate.1.json
@@ -48,8 +48,40 @@
}
]
],
- [],
- []
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "migrate",
+ "args": []
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ]
],
"ledger": {
"protocol_version": 26,
@@ -128,6 +160,18 @@
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -138,6 +182,46 @@
},
"live_until": 17280
},
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
{
"entry": {
"last_modified_ledger_seq": 0,
diff --git a/contracts/streaming/test_snapshots/test/test_stream_indexes.1.json b/contracts/streaming/test_snapshots/test/test_stream_indexes.1.json
index a9f3fcd..9221f6f 100644
--- a/contracts/streaming/test_snapshots/test/test_stream_indexes.1.json
+++ b/contracts/streaming/test_snapshots/test/test_stream_indexes.1.json
@@ -48,7 +48,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -353,7 +371,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -387,7 +405,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -707,6 +725,18 @@
"val": {
"u64": "2"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -726,7 +756,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "1033654523790656264"
+ "nonce": "2032731177588607455"
}
},
"durability": "temporary",
@@ -746,7 +776,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "2032731177588607455"
+ "nonce": "4270020994084947596"
}
},
"durability": "temporary",
@@ -766,7 +796,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "4270020994084947596"
+ "nonce": "4837995959683129791"
}
},
"durability": "temporary",
@@ -786,7 +816,27 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "4837995959683129791"
+ "nonce": "8370022561469687789"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
}
},
"durability": "temporary",
diff --git a/contracts/streaming/test_snapshots/test/test_top_up_at_start_doubles_rate.1.json b/contracts/streaming/test_snapshots/test/test_top_up_at_start_doubles_rate.1.json
index e0c0bd6..7f672bf 100644
--- a/contracts/streaming/test_snapshots/test/test_top_up_at_start_doubles_rate.1.json
+++ b/contracts/streaming/test_snapshots/test/test_top_up_at_start_doubles_rate.1.json
@@ -48,7 +48,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -289,7 +307,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -320,7 +338,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -411,7 +429,7 @@
"symbol": "linear_amount"
},
"val": {
- "i128": "10000000000"
+ "i128": "20000000000"
}
},
{
@@ -500,6 +518,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -519,7 +549,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "1033654523790656264"
+ "nonce": "2032731177588607455"
}
},
"durability": "temporary",
@@ -539,7 +569,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "2032731177588607455"
+ "nonce": "4270020994084947596"
}
},
"durability": "temporary",
@@ -559,7 +589,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "4270020994084947596"
+ "nonce": "4837995959683129791"
}
},
"durability": "temporary",
@@ -579,7 +609,27 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "4837995959683129791"
+ "nonce": "8370022561469687789"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
}
},
"durability": "temporary",
diff --git a/contracts/streaming/test_snapshots/test/test_top_up_increases_deposited_amount.1.json b/contracts/streaming/test_snapshots/test/test_top_up_increases_deposited_amount.1.json
index aab17a1..82927b4 100644
--- a/contracts/streaming/test_snapshots/test/test_top_up_increases_deposited_amount.1.json
+++ b/contracts/streaming/test_snapshots/test/test_top_up_increases_deposited_amount.1.json
@@ -48,7 +48,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -288,7 +306,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -319,7 +337,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -410,7 +428,7 @@
"symbol": "linear_amount"
},
"val": {
- "i128": "10000000000"
+ "i128": "15000000000"
}
},
{
@@ -499,6 +517,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -518,7 +548,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "1033654523790656264"
+ "nonce": "2032731177588607455"
}
},
"durability": "temporary",
@@ -538,7 +568,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "2032731177588607455"
+ "nonce": "4270020994084947596"
}
},
"durability": "temporary",
@@ -558,7 +588,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "4270020994084947596"
+ "nonce": "4837995959683129791"
}
},
"durability": "temporary",
@@ -578,7 +608,27 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "4837995959683129791"
+ "nonce": "8370022561469687789"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
}
},
"durability": "temporary",
diff --git a/contracts/streaming/test_snapshots/test/test_top_up_mid_stream_recalculates_rate.1.json b/contracts/streaming/test_snapshots/test/test_top_up_mid_stream_recalculates_rate.1.json
index 63cc36c..06d3b00 100644
--- a/contracts/streaming/test_snapshots/test/test_top_up_mid_stream_recalculates_rate.1.json
+++ b/contracts/streaming/test_snapshots/test/test_top_up_mid_stream_recalculates_rate.1.json
@@ -48,7 +48,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -206,6 +224,7 @@
}
]
],
+ [],
[]
],
"ledger": {
@@ -288,7 +307,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -319,7 +338,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -362,7 +381,7 @@
"symbol": "cliff_amount"
},
"val": {
- "i128": "0"
+ "i128": "5000000000"
}
},
{
@@ -370,7 +389,7 @@
"symbol": "cliff_time"
},
"val": {
- "u64": "1000000"
+ "u64": "1000500"
}
},
{
@@ -386,7 +405,7 @@
"symbol": "duration"
},
"val": {
- "i128": "1000"
+ "i128": "500"
}
},
{
@@ -434,7 +453,7 @@
"symbol": "start_time"
},
"val": {
- "u64": "1000000"
+ "u64": "1000500"
}
},
{
@@ -499,6 +518,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -518,7 +549,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "1033654523790656264"
+ "nonce": "2032731177588607455"
}
},
"durability": "temporary",
@@ -538,7 +569,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "2032731177588607455"
+ "nonce": "4270020994084947596"
}
},
"durability": "temporary",
@@ -558,7 +589,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "4270020994084947596"
+ "nonce": "4837995959683129791"
}
},
"durability": "temporary",
@@ -578,7 +609,27 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "4837995959683129791"
+ "nonce": "8370022561469687789"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
}
},
"durability": "temporary",
diff --git a/contracts/streaming/test_snapshots/test/test_upgrade_succeeds_with_admin_auth.1.json b/contracts/streaming/test_snapshots/test/test_upgrade_succeeds_with_admin_auth.1.json
index e78cfdf..fae2ec2 100644
--- a/contracts/streaming/test_snapshots/test/test_upgrade_succeeds_with_admin_auth.1.json
+++ b/contracts/streaming/test_snapshots/test/test_upgrade_succeeds_with_admin_auth.1.json
@@ -48,7 +48,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[]
],
"ledger": {
@@ -128,6 +146,18 @@
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -138,6 +168,26 @@
},
"live_until": 17280
},
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
{
"entry": {
"last_modified_ledger_seq": 0,
diff --git a/contracts/streaming/test_snapshots/test/test_version.1.json b/contracts/streaming/test_snapshots/test/test_version.1.json
index e78cfdf..fae2ec2 100644
--- a/contracts/streaming/test_snapshots/test/test_version.1.json
+++ b/contracts/streaming/test_snapshots/test/test_version.1.json
@@ -48,7 +48,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[]
],
"ledger": {
@@ -128,6 +146,18 @@
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -138,6 +168,26 @@
},
"live_until": 17280
},
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
{
"entry": {
"last_modified_ledger_seq": 0,
diff --git a/contracts/streaming/test_snapshots/test/test_withdraw_cliff_before_time.1.json b/contracts/streaming/test_snapshots/test/test_withdraw_cliff_before_time.1.json
index cd909bb..1592992 100644
--- a/contracts/streaming/test_snapshots/test/test_withdraw_cliff_before_time.1.json
+++ b/contracts/streaming/test_snapshots/test/test_withdraw_cliff_before_time.1.json
@@ -48,7 +48,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -239,7 +257,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -270,7 +288,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -450,6 +468,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -469,7 +499,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "1033654523790656264"
+ "nonce": "2032731177588607455"
}
},
"durability": "temporary",
@@ -500,6 +530,26 @@
},
"live_until": 6311999
},
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
{
"entry": {
"last_modified_ledger_seq": 0,
diff --git a/contracts/streaming/test_snapshots/test/test_withdraw_from_cancelled_stream.1.json b/contracts/streaming/test_snapshots/test/test_withdraw_from_cancelled_stream.1.json
index 1634850..882df22 100644
--- a/contracts/streaming/test_snapshots/test/test_withdraw_from_cancelled_stream.1.json
+++ b/contracts/streaming/test_snapshots/test/test_withdraw_from_cancelled_stream.1.json
@@ -48,7 +48,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -257,7 +275,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -288,7 +306,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -315,7 +333,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -342,7 +360,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -522,6 +540,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -541,7 +571,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "1033654523790656264"
+ "nonce": "2032731177588607455"
}
},
"durability": "temporary",
@@ -561,7 +591,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "2032731177588607455"
+ "nonce": "4270020994084947596"
}
},
"durability": "temporary",
@@ -592,6 +622,26 @@
},
"live_until": 6311999
},
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
{
"entry": {
"last_modified_ledger_seq": 0,
diff --git a/contracts/streaming/test_snapshots/test/test_withdraw_full_after_end.1.json b/contracts/streaming/test_snapshots/test/test_withdraw_full_after_end.1.json
index c3dd81a..ea8d98e 100644
--- a/contracts/streaming/test_snapshots/test/test_withdraw_full_after_end.1.json
+++ b/contracts/streaming/test_snapshots/test/test_withdraw_full_after_end.1.json
@@ -48,7 +48,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -262,7 +280,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -293,7 +311,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -320,7 +338,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -347,7 +365,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -527,6 +545,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -546,7 +576,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "1033654523790656264"
+ "nonce": "2032731177588607455"
}
},
"durability": "temporary",
@@ -586,7 +616,27 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
"key": {
"ledger_key_nonce": {
- "nonce": "2032731177588607455"
+ "nonce": "4270020994084947596"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
}
},
"durability": "temporary",
diff --git a/contracts/streaming/test_snapshots/test/test_withdraw_partial.1.json b/contracts/streaming/test_snapshots/test/test_withdraw_partial.1.json
index b477005..399f1f4 100644
--- a/contracts/streaming/test_snapshots/test/test_withdraw_partial.1.json
+++ b/contracts/streaming/test_snapshots/test/test_withdraw_partial.1.json
@@ -48,7 +48,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -262,7 +280,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -293,7 +311,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -473,6 +491,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -492,7 +522,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "1033654523790656264"
+ "nonce": "2032731177588607455"
}
},
"durability": "temporary",
@@ -532,7 +562,27 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
"key": {
"ledger_key_nonce": {
- "nonce": "2032731177588607455"
+ "nonce": "4270020994084947596"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
}
},
"durability": "temporary",
diff --git a/contracts/streaming/test_snapshots/test/test_withdraw_too_much.1.json b/contracts/streaming/test_snapshots/test/test_withdraw_too_much.1.json
index 4cbbcc6..6c00c51 100644
--- a/contracts/streaming/test_snapshots/test/test_withdraw_too_much.1.json
+++ b/contracts/streaming/test_snapshots/test/test_withdraw_too_much.1.json
@@ -48,7 +48,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -238,7 +256,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -269,7 +287,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -449,6 +467,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -468,7 +498,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "1033654523790656264"
+ "nonce": "2032731177588607455"
}
},
"durability": "temporary",
@@ -499,6 +529,26 @@
},
"live_until": 6311999
},
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
{
"entry": {
"last_modified_ledger_seq": 0,
diff --git a/contracts/streaming/test_snapshots/test_security/test_auth_attacker_cannot_cancel.1.json b/contracts/streaming/test_snapshots/test_security/test_auth_attacker_cannot_cancel.1.json
index 63348fd..435abeb 100644
--- a/contracts/streaming/test_snapshots/test_security/test_auth_attacker_cannot_cancel.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_auth_attacker_cannot_cancel.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -261,7 +279,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -292,7 +310,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -472,6 +490,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -511,7 +541,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "4837995959683129791"
+ "nonce": "4270020994084947596"
}
},
"durability": "temporary",
@@ -545,6 +575,26 @@
},
"live_until": 4095
},
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
{
"entry": {
"last_modified_ledger_seq": 0,
diff --git a/contracts/streaming/test_snapshots/test_security/test_auth_attacker_cannot_withdraw.1.json b/contracts/streaming/test_snapshots/test_security/test_auth_attacker_cannot_withdraw.1.json
index 65bb4a6..7dd91c5 100644
--- a/contracts/streaming/test_snapshots/test_security/test_auth_attacker_cannot_withdraw.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_auth_attacker_cannot_withdraw.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -261,7 +279,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -292,7 +310,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -472,6 +490,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -511,7 +541,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "4837995959683129791"
+ "nonce": "4270020994084947596"
}
},
"durability": "temporary",
@@ -545,6 +575,26 @@
},
"live_until": 4095
},
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
{
"entry": {
"last_modified_ledger_seq": 0,
diff --git a/contracts/streaming/test_snapshots/test_security/test_auth_non_admin_cannot_upgrade.1.json b/contracts/streaming/test_snapshots/test_security/test_auth_non_admin_cannot_upgrade.1.json
index 5a3d070..2678724 100644
--- a/contracts/streaming/test_snapshots/test_security/test_auth_non_admin_cannot_upgrade.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_auth_non_admin_cannot_upgrade.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[],
[]
],
@@ -151,6 +169,18 @@
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -184,6 +214,26 @@
},
"live_until": 4095
},
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
{
"entry": {
"last_modified_ledger_seq": 0,
diff --git a/contracts/streaming/test_snapshots/test_security/test_auth_recipient_cannot_cancel.1.json b/contracts/streaming/test_snapshots/test_security/test_auth_recipient_cannot_cancel.1.json
index b241954..08d06ee 100644
--- a/contracts/streaming/test_snapshots/test_security/test_auth_recipient_cannot_cancel.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_auth_recipient_cannot_cancel.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -261,7 +279,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -292,7 +310,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -472,6 +490,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -511,7 +541,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "4837995959683129791"
+ "nonce": "4270020994084947596"
}
},
"durability": "temporary",
@@ -545,6 +575,26 @@
},
"live_until": 4095
},
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
{
"entry": {
"last_modified_ledger_seq": 0,
diff --git a/contracts/streaming/test_snapshots/test_security/test_auth_sender_cannot_withdraw.1.json b/contracts/streaming/test_snapshots/test_security/test_auth_sender_cannot_withdraw.1.json
index f8f8405..1429b16 100644
--- a/contracts/streaming/test_snapshots/test_security/test_auth_sender_cannot_withdraw.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_auth_sender_cannot_withdraw.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -261,7 +279,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -292,7 +310,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -472,6 +490,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -532,6 +562,26 @@
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4270020994084947596"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
"key": {
"ledger_key_nonce": {
"nonce": "4837995959683129791"
diff --git a/contracts/streaming/test_snapshots/test_security/test_cancel_after_full_withdrawal.1.json b/contracts/streaming/test_snapshots/test_security/test_cancel_after_full_withdrawal.1.json
index 5cb4f84..28ed139 100644
--- a/contracts/streaming/test_snapshots/test_security/test_cancel_after_full_withdrawal.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_cancel_after_full_withdrawal.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -305,7 +323,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -339,7 +357,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -366,7 +384,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -393,7 +411,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -573,6 +591,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -612,7 +642,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "4837995959683129791"
+ "nonce": "4270020994084947596"
}
},
"durability": "temporary",
@@ -632,7 +662,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "8370022561469687789"
+ "nonce": "6277191135259896685"
}
},
"durability": "temporary",
@@ -652,7 +682,27 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
"key": {
"ledger_key_nonce": {
- "nonce": "4270020994084947596"
+ "nonce": "8370022561469687789"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
}
},
"durability": "temporary",
diff --git a/contracts/streaming/test_snapshots/test_security/test_cancel_at_end_time.1.json b/contracts/streaming/test_snapshots/test_security/test_cancel_at_end_time.1.json
index 4b1feaa..8816fec 100644
--- a/contracts/streaming/test_snapshots/test_security/test_cancel_at_end_time.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_cancel_at_end_time.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -282,7 +300,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -313,7 +331,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -340,7 +358,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -367,7 +385,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -547,6 +565,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -604,6 +634,26 @@
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "8370022561469687789"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
"key": {
"ledger_key_nonce": {
"nonce": "4837995959683129791"
diff --git a/contracts/streaming/test_snapshots/test_security/test_cancel_before_start_full_refund.1.json b/contracts/streaming/test_snapshots/test_security/test_cancel_before_start_full_refund.1.json
index c5d5e7c..40ce5d1 100644
--- a/contracts/streaming/test_snapshots/test_security/test_cancel_before_start_full_refund.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_cancel_before_start_full_refund.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -282,7 +300,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -313,7 +331,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -340,7 +358,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -367,7 +385,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -547,6 +565,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -604,6 +634,26 @@
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "8370022561469687789"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
"key": {
"ledger_key_nonce": {
"nonce": "4837995959683129791"
diff --git a/contracts/streaming/test_snapshots/test_security/test_cancel_conservation_with_prior_withdrawal.1.json b/contracts/streaming/test_snapshots/test_security/test_cancel_conservation_with_prior_withdrawal.1.json
index 6ea0d0d..ee5730b 100644
--- a/contracts/streaming/test_snapshots/test_security/test_cancel_conservation_with_prior_withdrawal.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_cancel_conservation_with_prior_withdrawal.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -306,7 +324,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -337,7 +355,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -364,7 +382,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -391,7 +409,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -571,6 +589,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -610,7 +640,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "4837995959683129791"
+ "nonce": "4270020994084947596"
}
},
"durability": "temporary",
@@ -630,7 +660,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "8370022561469687789"
+ "nonce": "6277191135259896685"
}
},
"durability": "temporary",
@@ -650,7 +680,27 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
"key": {
"ledger_key_nonce": {
- "nonce": "4270020994084947596"
+ "nonce": "8370022561469687789"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
}
},
"durability": "temporary",
diff --git a/contracts/streaming/test_snapshots/test_security/test_cancel_nonexistent_stream.1.json b/contracts/streaming/test_snapshots/test_security/test_cancel_nonexistent_stream.1.json
index 5e29d13..ae75c77 100644
--- a/contracts/streaming/test_snapshots/test_security/test_cancel_nonexistent_stream.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_cancel_nonexistent_stream.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[]
],
"ledger": {
@@ -150,6 +168,18 @@
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -160,6 +190,26 @@
},
"live_until": 17280
},
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
{
"entry": {
"last_modified_ledger_seq": 0,
diff --git a/contracts/streaming/test_snapshots/test_security/test_cliff_after_end_time.1.json b/contracts/streaming/test_snapshots/test_security/test_cliff_after_end_time.1.json
index e66a81f..e4e42c2 100644
--- a/contracts/streaming/test_snapshots/test_security/test_cliff_after_end_time.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_cliff_after_end_time.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -178,6 +196,18 @@
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -195,6 +225,26 @@
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "2032731177588607455"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
"key": {
"ledger_key_nonce": {
"nonce": "4837995959683129791"
diff --git a/contracts/streaming/test_snapshots/test_security/test_cliff_amount_equals_total.1.json b/contracts/streaming/test_snapshots/test_security/test_cliff_amount_equals_total.1.json
index c5452fd..13f94a9 100644
--- a/contracts/streaming/test_snapshots/test_security/test_cliff_amount_equals_total.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_cliff_amount_equals_total.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -262,7 +280,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -293,7 +311,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -473,6 +491,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -510,6 +540,26 @@
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4270020994084947596"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
"key": {
"ledger_key_nonce": {
"nonce": "4837995959683129791"
diff --git a/contracts/streaming/test_snapshots/test_security/test_cliff_amount_exceeds_total.1.json b/contracts/streaming/test_snapshots/test_security/test_cliff_amount_exceeds_total.1.json
index e66a81f..e4e42c2 100644
--- a/contracts/streaming/test_snapshots/test_security/test_cliff_amount_exceeds_total.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_cliff_amount_exceeds_total.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -178,6 +196,18 @@
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -195,6 +225,26 @@
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "2032731177588607455"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
"key": {
"ledger_key_nonce": {
"nonce": "4837995959683129791"
diff --git a/contracts/streaming/test_snapshots/test_security/test_cliff_before_start_time.1.json b/contracts/streaming/test_snapshots/test_security/test_cliff_before_start_time.1.json
index e66a81f..e4e42c2 100644
--- a/contracts/streaming/test_snapshots/test_security/test_cliff_before_start_time.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_cliff_before_start_time.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -178,6 +196,18 @@
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -195,6 +225,26 @@
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "2032731177588607455"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
"key": {
"ledger_key_nonce": {
"nonce": "4837995959683129791"
diff --git a/contracts/streaming/test_snapshots/test_security/test_cliff_time_equals_end_time.1.json b/contracts/streaming/test_snapshots/test_security/test_cliff_time_equals_end_time.1.json
index 078fce1..925cc1d 100644
--- a/contracts/streaming/test_snapshots/test_security/test_cliff_time_equals_end_time.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_cliff_time_equals_end_time.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -261,7 +279,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -292,7 +310,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -472,6 +490,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -509,6 +539,26 @@
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4270020994084947596"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
"key": {
"ledger_key_nonce": {
"nonce": "4837995959683129791"
diff --git a/contracts/streaming/test_snapshots/test_security/test_end_time_equals_start_time.1.json b/contracts/streaming/test_snapshots/test_security/test_end_time_equals_start_time.1.json
index e66a81f..e4e42c2 100644
--- a/contracts/streaming/test_snapshots/test_security/test_end_time_equals_start_time.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_end_time_equals_start_time.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -178,6 +196,18 @@
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -195,6 +225,26 @@
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "2032731177588607455"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
"key": {
"ledger_key_nonce": {
"nonce": "4837995959683129791"
diff --git a/contracts/streaming/test_snapshots/test_security/test_get_nonexistent_stream.1.json b/contracts/streaming/test_snapshots/test_security/test_get_nonexistent_stream.1.json
index e338029..429a782 100644
--- a/contracts/streaming/test_snapshots/test_security/test_get_nonexistent_stream.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_get_nonexistent_stream.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[]
],
"ledger": {
@@ -150,6 +168,18 @@
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -160,6 +190,26 @@
},
"live_until": 17280
},
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
{
"entry": {
"last_modified_ledger_seq": 0,
diff --git a/contracts/streaming/test_snapshots/test_security/test_large_amounts_no_overflow.1.json b/contracts/streaming/test_snapshots/test_security/test_large_amounts_no_overflow.1.json
index 4c6a8d4..d5b144b 100644
--- a/contracts/streaming/test_snapshots/test_security/test_large_amounts_no_overflow.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_large_amounts_no_overflow.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4",
@@ -283,7 +301,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -314,7 +332,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -494,6 +512,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -513,7 +543,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "2032731177588607455"
+ "nonce": "4270020994084947596"
}
},
"durability": "temporary",
@@ -533,7 +563,27 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "4270020994084947596"
+ "nonce": "8370022561469687789"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
}
},
"durability": "temporary",
@@ -573,7 +623,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4",
"key": {
"ledger_key_nonce": {
- "nonce": "4837995959683129791"
+ "nonce": "2032731177588607455"
}
},
"durability": "temporary",
diff --git a/contracts/streaming/test_snapshots/test_security/test_minimum_duration_stream.1.json b/contracts/streaming/test_snapshots/test_security/test_minimum_duration_stream.1.json
index b2148e6..4abd117 100644
--- a/contracts/streaming/test_snapshots/test_security/test_minimum_duration_stream.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_minimum_duration_stream.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -283,7 +301,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -314,7 +332,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -341,7 +359,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -368,7 +386,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -548,6 +566,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -587,7 +617,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "4837995959683129791"
+ "nonce": "4270020994084947596"
}
},
"durability": "temporary",
@@ -607,7 +637,27 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
"key": {
"ledger_key_nonce": {
- "nonce": "4270020994084947596"
+ "nonce": "8370022561469687789"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
}
},
"durability": "temporary",
diff --git a/contracts/streaming/test_snapshots/test_security/test_negative_cliff_amount.1.json b/contracts/streaming/test_snapshots/test_security/test_negative_cliff_amount.1.json
index e66a81f..e4e42c2 100644
--- a/contracts/streaming/test_snapshots/test_security/test_negative_cliff_amount.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_negative_cliff_amount.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -178,6 +196,18 @@
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -195,6 +225,26 @@
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "2032731177588607455"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
"key": {
"ledger_key_nonce": {
"nonce": "4837995959683129791"
diff --git a/contracts/streaming/test_snapshots/test_security/test_negative_total_amount.1.json b/contracts/streaming/test_snapshots/test_security/test_negative_total_amount.1.json
index 5e29d13..ae75c77 100644
--- a/contracts/streaming/test_snapshots/test_security/test_negative_total_amount.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_negative_total_amount.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[]
],
"ledger": {
@@ -150,6 +168,18 @@
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -160,6 +190,26 @@
},
"live_until": 17280
},
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
{
"entry": {
"last_modified_ledger_seq": 0,
diff --git a/contracts/streaming/test_snapshots/test_security/test_no_overdraw_multiple_partial_withdrawals.1.json b/contracts/streaming/test_snapshots/test_security/test_no_overdraw_multiple_partial_withdrawals.1.json
index 18729b7..c7647c0 100644
--- a/contracts/streaming/test_snapshots/test_security/test_no_overdraw_multiple_partial_withdrawals.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_no_overdraw_multiple_partial_withdrawals.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -492,7 +510,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -523,7 +541,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -550,7 +568,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -577,7 +595,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -757,6 +775,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -796,7 +826,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "4837995959683129791"
+ "nonce": "4270020994084947596"
}
},
"durability": "temporary",
@@ -876,7 +906,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
"key": {
"ledger_key_nonce": {
- "nonce": "3126073502131104533"
+ "nonce": "2781962168096793370"
}
},
"durability": "temporary",
@@ -896,7 +926,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
"key": {
"ledger_key_nonce": {
- "nonce": "4270020994084947596"
+ "nonce": "3126073502131104533"
}
},
"durability": "temporary",
@@ -1007,6 +1037,26 @@
},
"live_until": 6311999
},
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
{
"entry": {
"last_modified_ledger_seq": 0,
diff --git a/contracts/streaming/test_snapshots/test_security/test_nothing_withdrawable_before_cliff.1.json b/contracts/streaming/test_snapshots/test_security/test_nothing_withdrawable_before_cliff.1.json
index 45c87c6..f5b0100 100644
--- a/contracts/streaming/test_snapshots/test_security/test_nothing_withdrawable_before_cliff.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_nothing_withdrawable_before_cliff.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -260,7 +278,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -291,7 +309,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -471,6 +489,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -508,6 +538,26 @@
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4270020994084947596"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
"key": {
"ledger_key_nonce": {
"nonce": "4837995959683129791"
diff --git a/contracts/streaming/test_snapshots/test_security/test_rounding_dust_stays_in_contract.1.json b/contracts/streaming/test_snapshots/test_security/test_rounding_dust_stays_in_contract.1.json
index 549c407..3ca7911 100644
--- a/contracts/streaming/test_snapshots/test_security/test_rounding_dust_stays_in_contract.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_rounding_dust_stays_in_contract.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -284,7 +302,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -315,7 +333,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -342,7 +360,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -369,7 +387,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -549,6 +567,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -588,7 +618,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "4837995959683129791"
+ "nonce": "4270020994084947596"
}
},
"durability": "temporary",
@@ -608,7 +638,27 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
"key": {
"ledger_key_nonce": {
- "nonce": "4270020994084947596"
+ "nonce": "8370022561469687789"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
}
},
"durability": "temporary",
diff --git a/contracts/streaming/test_snapshots/test_security/test_self_stream_cancel_no_double_pay.1.json b/contracts/streaming/test_snapshots/test_security/test_self_stream_cancel_no_double_pay.1.json
index e66a81f..e4e42c2 100644
--- a/contracts/streaming/test_snapshots/test_security/test_self_stream_cancel_no_double_pay.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_self_stream_cancel_no_double_pay.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -178,6 +196,18 @@
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -195,6 +225,26 @@
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "2032731177588607455"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
"key": {
"ledger_key_nonce": {
"nonce": "4837995959683129791"
diff --git a/contracts/streaming/test_snapshots/test_security/test_self_stream_withdraw.1.json b/contracts/streaming/test_snapshots/test_security/test_self_stream_withdraw.1.json
index e66a81f..e4e42c2 100644
--- a/contracts/streaming/test_snapshots/test_security/test_self_stream_withdraw.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_self_stream_withdraw.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -178,6 +196,18 @@
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -195,6 +225,26 @@
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "2032731177588607455"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
"key": {
"ledger_key_nonce": {
"nonce": "4837995959683129791"
diff --git a/contracts/streaming/test_snapshots/test_security/test_withdraw_exactly_at_cliff.1.json b/contracts/streaming/test_snapshots/test_security/test_withdraw_exactly_at_cliff.1.json
index c7de8d7..4f5bea4 100644
--- a/contracts/streaming/test_snapshots/test_security/test_withdraw_exactly_at_cliff.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_withdraw_exactly_at_cliff.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -283,7 +301,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -314,7 +332,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -494,6 +512,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -533,7 +563,7 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
"key": {
"ledger_key_nonce": {
- "nonce": "4837995959683129791"
+ "nonce": "4270020994084947596"
}
},
"durability": "temporary",
@@ -553,7 +583,27 @@
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
"key": {
"ledger_key_nonce": {
- "nonce": "4270020994084947596"
+ "nonce": "8370022561469687789"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
}
},
"durability": "temporary",
diff --git a/contracts/streaming/test_snapshots/test_security/test_withdraw_more_than_withdrawable.1.json b/contracts/streaming/test_snapshots/test_security/test_withdraw_more_than_withdrawable.1.json
index 9f8e3ca..e77f6f2 100644
--- a/contracts/streaming/test_snapshots/test_security/test_withdraw_more_than_withdrawable.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_withdraw_more_than_withdrawable.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -261,7 +279,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -292,7 +310,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -472,6 +490,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -509,6 +539,26 @@
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4270020994084947596"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
"key": {
"ledger_key_nonce": {
"nonce": "4837995959683129791"
diff --git a/contracts/streaming/test_snapshots/test_security/test_withdraw_negative_amount.1.json b/contracts/streaming/test_snapshots/test_security/test_withdraw_negative_amount.1.json
index 875e85d..c5d53eb 100644
--- a/contracts/streaming/test_snapshots/test_security/test_withdraw_negative_amount.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_withdraw_negative_amount.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -260,7 +278,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -291,7 +309,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -471,6 +489,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -508,6 +538,26 @@
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4270020994084947596"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
"key": {
"ledger_key_nonce": {
"nonce": "4837995959683129791"
diff --git a/contracts/streaming/test_snapshots/test_security/test_withdraw_nonexistent_stream.1.json b/contracts/streaming/test_snapshots/test_security/test_withdraw_nonexistent_stream.1.json
index 5e29d13..ae75c77 100644
--- a/contracts/streaming/test_snapshots/test_security/test_withdraw_nonexistent_stream.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_withdraw_nonexistent_stream.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[]
],
"ledger": {
@@ -150,6 +168,18 @@
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -160,6 +190,26 @@
},
"live_until": 17280
},
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
{
"entry": {
"last_modified_ledger_seq": 0,
diff --git a/contracts/streaming/test_snapshots/test_security/test_withdraw_zero.1.json b/contracts/streaming/test_snapshots/test_security/test_withdraw_zero.1.json
index 875e85d..c5d53eb 100644
--- a/contracts/streaming/test_snapshots/test_security/test_withdraw_zero.1.json
+++ b/contracts/streaming/test_snapshots/test_security/test_withdraw_zero.1.json
@@ -70,7 +70,25 @@
}
]
],
- [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
[
[
"CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
@@ -260,7 +278,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -291,7 +309,7 @@
},
"ext": "v0"
},
- "live_until": 17280
+ "live_until": 518400
},
{
"entry": {
@@ -471,6 +489,18 @@
"val": {
"u64": "1"
}
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Paused"
+ }
+ ]
+ },
+ "val": {
+ "bool": false
+ }
}
]
}
@@ -508,6 +538,26 @@
"contract_data": {
"ext": "v0",
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4270020994084947596"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ "live_until": 6311999
+ },
+ {
+ "entry": {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM",
"key": {
"ledger_key_nonce": {
"nonce": "4837995959683129791"
diff --git a/e2e/visual.spec.ts b/e2e/visual.spec.ts
index a097f73..5397a13 100644
--- a/e2e/visual.spec.ts
+++ b/e2e/visual.spec.ts
@@ -1,195 +1,256 @@
-import { test, expect, type Page } from '@playwright/test'
+import { test, expect, type Page } from "@playwright/test";
-const DEMO_ADDRESS = 'GBQ2X7KFY3R4VZ6N5LJ7WQH3M2PD8C9SAUTV4EXAMPLE0WALLET00ADDR'
-const FIXED_NOW = 1720000000000 // 2024-10-?? UTC; keep dates stable for screenshots
+const DEMO_ADDRESS =
+ "GBQ2X7KFY3R4VZ6N5LJ7WQH3M2PD8C9SAUTV4EXAMPLE0WALLET00ADDR";
+const FIXED_NOW = 1720000000000; // 2024-10-?? UTC; keep dates stable for screenshots
async function preparePage(
page: Page,
- options: { theme?: 'light' | 'dark'; connect?: boolean } = {},
+ options: { theme?: "light" | "dark"; connect?: boolean } = {},
) {
- const theme = options.theme ?? 'light'
+ const theme = options.theme ?? "light";
await page.addInitScript(
({ theme, connect, walletAddress, now }) => {
- localStorage.setItem('theme', theme)
+ localStorage.setItem("theme", theme);
if (connect) {
- localStorage.setItem('walletId', 'xbull')
- ;(window as any).xBullSDK = {
+ localStorage.setItem("walletId", "xbull");
+ (window as any).xBullSDK = {
connect: async () => ({ publicKey: walletAddress }),
- signXDR: async () => 'AAAAAgAAAAA...dummy-signature...',
- }
+ signXDR: async () => "AAAAAgAAAAA...dummy-signature...",
+ };
}
- const OriginalDate = Date
+ const OriginalDate = Date;
class MockDate extends OriginalDate {
constructor(...args: any[]) {
if (args.length === 0) {
- return super(now)
+ super(now);
+ } else {
+ // Date has multiple overloaded constructors; a variadic args array
+ // can't be typed as a fixed tuple, so this spread can't type-check.
+ // @ts-expect-error
+ super(...args);
}
- return super(...args)
}
static now() {
- return now
+ return now;
}
static parse(value: string) {
- return OriginalDate.parse(value)
+ return OriginalDate.parse(value);
}
- static UTC(...args: any[]) {
- return OriginalDate.UTC(...args)
+ static UTC(...args: Parameters) {
+ return OriginalDate.UTC(...args);
}
}
// @ts-ignore
- window.Date = MockDate
- if (theme === 'dark') {
- document.documentElement.classList.add('dark')
+ window.Date = MockDate;
+ if (theme === "dark") {
+ document.documentElement.classList.add("dark");
} else {
- document.documentElement.classList.remove('dark')
+ document.documentElement.classList.remove("dark");
}
},
- { theme, connect: options.connect ?? false, walletAddress: DEMO_ADDRESS, now: FIXED_NOW },
- )
+ {
+ theme,
+ connect: options.connect ?? false,
+ walletAddress: DEMO_ADDRESS,
+ now: FIXED_NOW,
+ },
+ );
}
-for (const theme of ['light', 'dark'] as const) {
+for (const theme of ["light", "dark"] as const) {
test(`landing page screenshot (${theme})`, async ({ page }) => {
- await preparePage(page, { theme })
- await page.goto('/')
- await expect(page.locator('text=FlowStar')).toBeVisible()
- await expect(page).toHaveScreenshot(`landing-${theme}.png`, { fullPage: true })
- })
+ await preparePage(page, { theme });
+ await page.goto("/");
+ await expect(page.locator("text=FlowStar")).toBeVisible();
+ await expect(page).toHaveScreenshot(`landing-${theme}.png`, {
+ fullPage: true,
+ });
+ });
}
-test.describe('Dashboard visual states', () => {
- test('dashboard empty state', async ({ page }) => {
- await preparePage(page, { theme: 'light' })
- await page.goto('/app')
- await expect(page.locator('text=Connect your wallet')).toBeVisible()
- await expect(page).toHaveScreenshot('dashboard-empty.png', { fullPage: true })
- })
-
- test('dashboard connected with streams', async ({ page }) => {
- await preparePage(page, { theme: 'light', connect: true })
- await page.goto('/app')
- await expect(page.locator('text=Dashboard')).toBeVisible()
- await expect(page.locator('text=New stream')).toBeVisible()
- await expect(page).toHaveScreenshot('dashboard-with-streams.png', { fullPage: true })
- })
-
- test('dashboard tabs: receiving and sent', async ({ page }) => {
- await preparePage(page, { theme: 'light', connect: true })
- await page.goto('/app')
- await expect(page.locator('text=Dashboard')).toBeVisible()
-
- await page.locator('button:has-text("Receiving")').first().click()
- await expect(page.locator('text=Receiving')).toBeVisible()
- await expect(page).toHaveScreenshot('dashboard-receiving-tab.png', { fullPage: true })
-
- await page.locator('button:has-text("Sending")').first().click()
- await expect(page.locator('text=Sending')).toBeVisible()
- await expect(page).toHaveScreenshot('dashboard-sent-tab.png', { fullPage: true })
- })
-})
-
-test.describe('Create stream form', () => {
- test('empty form screenshot', async ({ page }) => {
- await preparePage(page, { theme: 'light', connect: true })
- await page.goto('/app/create')
- await expect(page.locator('text=Create a stream')).toBeVisible()
- await expect(page).toHaveScreenshot('create-stream-empty.png', { fullPage: true })
- })
-
- test('filled form screenshot', async ({ page }) => {
- await preparePage(page, { theme: 'light', connect: true })
- await page.goto('/app/create')
- await page.fill('#recipient', 'GD7HQZX4...PAYROLL...4FJ2K')
- await page.fill('#amount', '1234')
- await page.fill('#startDate', '2025-01-01T00:00')
- await page.fill('#endDate', '2025-12-31T23:59')
- await page.click('button:has-text("Create stream")')
- await expect(page.locator('text=Review transaction').or(page.locator('text=Create stream'))).toBeVisible()
- await expect(page).toHaveScreenshot('create-stream-filled.png', { fullPage: true })
- })
-
- test('validation error screenshot', async ({ page }) => {
- await preparePage(page, { theme: 'light', connect: true })
- await page.goto('/app/create')
- await page.fill('#recipient', 'invalid-address')
- await page.fill('#amount', '1234')
- await page.click('button:has-text("Create stream")')
- await expect(page.locator('text=Invalid recipient address').or(page.locator('text=invalid'))).toBeVisible()
- await expect(page).toHaveScreenshot('create-stream-validation.png', { fullPage: true })
- })
-})
-
-test.describe('Stream detail pages', () => {
- test('active stream screenshot', async ({ page }) => {
- await preparePage(page, { theme: 'light', connect: true })
- await page.goto('/app/stream/2')
- await expect(page.locator('text=Stream details').or(page.locator('text=Withdraw'))).toBeVisible()
- await expect(page).toHaveScreenshot('stream-detail-active.png', { fullPage: true })
- })
-
- test('completed stream screenshot', async ({ page }) => {
- await preparePage(page, { theme: 'light', connect: true })
- await page.goto('/app/stream/3')
- await expect(page.locator('text=Stream not found')).not.toBeVisible()
- await expect(page.locator('text=Stream details').or(page.locator('text=Withdraw'))).toBeVisible()
- await expect(page).toHaveScreenshot('stream-detail-completed.png', { fullPage: true })
- })
-
- test('cancelled stream screenshot', async ({ page }) => {
- await preparePage(page, { theme: 'light', connect: true })
- await page.goto('/app/stream/5')
- await expect(page.locator('text=Cancelled').or(page.locator('text=Stream details'))).toBeVisible()
- await expect(page).toHaveScreenshot('stream-detail-cancelled.png', { fullPage: true })
- })
-})
-
-test.describe('Batch create page', () => {
- test('empty batch create screenshot', async ({ page }) => {
- await preparePage(page, { theme: 'light', connect: true })
- await page.goto('/app/create/batch')
- await expect(page.locator('text=Batch create streams')).toBeVisible()
- await expect(page).toHaveScreenshot('batch-create-empty.png', { fullPage: true })
- })
-
- test('batch create with CSV screenshot', async ({ page }) => {
- await preparePage(page, { theme: 'light', connect: true })
- await page.goto('/app/create/batch')
- const csv = 'recipient,amount,start_time,end_time,cliff_time,cliff_amount\nGD7HQZX4...PAYROLL...4FJ2K,100,2025-01-01T00:00,2025-12-31T23:59,2025-01-01T00:00,0\n'
+test.describe("Dashboard visual states", () => {
+ test("dashboard empty state", async ({ page }) => {
+ await preparePage(page, { theme: "light" });
+ await page.goto("/app");
+ await expect(page.locator("text=Connect your wallet")).toBeVisible();
+ await expect(page).toHaveScreenshot("dashboard-empty.png", {
+ fullPage: true,
+ });
+ });
+
+ test("dashboard connected with streams", async ({ page }) => {
+ await preparePage(page, { theme: "light", connect: true });
+ await page.goto("/app");
+ await expect(page.locator("text=Dashboard")).toBeVisible();
+ await expect(page.locator("text=New stream")).toBeVisible();
+ await expect(page).toHaveScreenshot("dashboard-with-streams.png", {
+ fullPage: true,
+ });
+ });
+
+ test("dashboard tabs: receiving and sent", async ({ page }) => {
+ await preparePage(page, { theme: "light", connect: true });
+ await page.goto("/app");
+ await expect(page.locator("text=Dashboard")).toBeVisible();
+
+ await page.locator('button:has-text("Receiving")').first().click();
+ await expect(page.locator("text=Receiving")).toBeVisible();
+ await expect(page).toHaveScreenshot("dashboard-receiving-tab.png", {
+ fullPage: true,
+ });
+
+ await page.locator('button:has-text("Sending")').first().click();
+ await expect(page.locator("text=Sending")).toBeVisible();
+ await expect(page).toHaveScreenshot("dashboard-sent-tab.png", {
+ fullPage: true,
+ });
+ });
+});
+
+test.describe("Create stream form", () => {
+ test("empty form screenshot", async ({ page }) => {
+ await preparePage(page, { theme: "light", connect: true });
+ await page.goto("/app/create");
+ await expect(page.locator("text=Create a stream")).toBeVisible();
+ await expect(page).toHaveScreenshot("create-stream-empty.png", {
+ fullPage: true,
+ });
+ });
+
+ test("filled form screenshot", async ({ page }) => {
+ await preparePage(page, { theme: "light", connect: true });
+ await page.goto("/app/create");
+ await page.fill("#recipient", "GD7HQZX4...PAYROLL...4FJ2K");
+ await page.fill("#amount", "1234");
+ await page.fill("#startDate", "2025-01-01T00:00");
+ await page.fill("#endDate", "2025-12-31T23:59");
+ await page.click('button:has-text("Create stream")');
+ await expect(
+ page
+ .locator("text=Review transaction")
+ .or(page.locator("text=Create stream")),
+ ).toBeVisible();
+ await expect(page).toHaveScreenshot("create-stream-filled.png", {
+ fullPage: true,
+ });
+ });
+
+ test("validation error screenshot", async ({ page }) => {
+ await preparePage(page, { theme: "light", connect: true });
+ await page.goto("/app/create");
+ await page.fill("#recipient", "invalid-address");
+ await page.fill("#amount", "1234");
+ await page.click('button:has-text("Create stream")');
+ await expect(
+ page
+ .locator("text=Invalid recipient address")
+ .or(page.locator("text=invalid")),
+ ).toBeVisible();
+ await expect(page).toHaveScreenshot("create-stream-validation.png", {
+ fullPage: true,
+ });
+ });
+});
+
+test.describe("Stream detail pages", () => {
+ test("active stream screenshot", async ({ page }) => {
+ await preparePage(page, { theme: "light", connect: true });
+ await page.goto("/app/stream/2");
+ await expect(
+ page.locator("text=Stream details").or(page.locator("text=Withdraw")),
+ ).toBeVisible();
+ await expect(page).toHaveScreenshot("stream-detail-active.png", {
+ fullPage: true,
+ });
+ });
+
+ test("completed stream screenshot", async ({ page }) => {
+ await preparePage(page, { theme: "light", connect: true });
+ await page.goto("/app/stream/3");
+ await expect(page.locator("text=Stream not found")).not.toBeVisible();
+ await expect(
+ page.locator("text=Stream details").or(page.locator("text=Withdraw")),
+ ).toBeVisible();
+ await expect(page).toHaveScreenshot("stream-detail-completed.png", {
+ fullPage: true,
+ });
+ });
+
+ test("cancelled stream screenshot", async ({ page }) => {
+ await preparePage(page, { theme: "light", connect: true });
+ await page.goto("/app/stream/5");
+ await expect(
+ page.locator("text=Cancelled").or(page.locator("text=Stream details")),
+ ).toBeVisible();
+ await expect(page).toHaveScreenshot("stream-detail-cancelled.png", {
+ fullPage: true,
+ });
+ });
+});
+
+test.describe("Batch create page", () => {
+ test("empty batch create screenshot", async ({ page }) => {
+ await preparePage(page, { theme: "light", connect: true });
+ await page.goto("/app/create/batch");
+ await expect(page.locator("text=Batch create streams")).toBeVisible();
+ await expect(page).toHaveScreenshot("batch-create-empty.png", {
+ fullPage: true,
+ });
+ });
+
+ test("batch create with CSV screenshot", async ({ page }) => {
+ await preparePage(page, { theme: "light", connect: true });
+ await page.goto("/app/create/batch");
+ const csv =
+ "recipient,amount,start_time,end_time,cliff_time,cliff_amount\nGD7HQZX4...PAYROLL...4FJ2K,100,2025-01-01T00:00,2025-12-31T23:59,2025-01-01T00:00,0\n";
await page.setInputFiles('input[type="file"]#csvFile', {
- name: 'batch-streams.csv',
- mimeType: 'text/csv',
- buffer: Buffer.from(csv, 'utf-8'),
- })
- await expect(page.locator('text=CSV parse warnings').or(page.locator('text=Preview rows'))).toBeVisible()
- await expect(page).toHaveScreenshot('batch-create-with-csv.png', { fullPage: true })
- })
-})
-
-test.describe('Wallet and modal dialogs', () => {
- test('wallet connect dialog screenshot', async ({ page }) => {
- await preparePage(page, { theme: 'light' })
- await page.goto('/app')
- await page.click('button:has-text("Connect wallet")')
- await expect(page.locator('text=Connect a wallet')).toBeVisible()
- await expect(page).toHaveScreenshot('wallet-connect-dialog.png', { fullPage: true })
- })
-
- test('withdraw dialog screenshot', async ({ page }) => {
- await preparePage(page, { theme: 'light', connect: true })
- await page.goto('/app/stream/2')
- await page.locator('button:has-text("Withdraw")').first().click()
- await expect(page.locator('text=Withdraw funds')).toBeVisible()
- await expect(page).toHaveScreenshot('withdraw-dialog.png', { fullPage: true })
- })
-
- test('cancel dialog screenshot', async ({ page }) => {
- await preparePage(page, { theme: 'light', connect: true })
- await page.goto('/app/stream/1')
- await page.locator('button:has-text("Cancel")').first().click()
- await expect(page.locator('text=Cancel stream')).toBeVisible()
- await expect(page).toHaveScreenshot('cancel-dialog.png', { fullPage: true })
- })
-})
+ name: "batch-streams.csv",
+ mimeType: "text/csv",
+ buffer: Buffer.from(csv, "utf-8"),
+ });
+ await expect(
+ page
+ .locator("text=CSV parse warnings")
+ .or(page.locator("text=Preview rows")),
+ ).toBeVisible();
+ await expect(page).toHaveScreenshot("batch-create-with-csv.png", {
+ fullPage: true,
+ });
+ });
+});
+
+test.describe("Wallet and modal dialogs", () => {
+ test("wallet connect dialog screenshot", async ({ page }) => {
+ await preparePage(page, { theme: "light" });
+ await page.goto("/app");
+ await page.click('button:has-text("Connect wallet")');
+ await expect(page.locator("text=Connect a wallet")).toBeVisible();
+ await expect(page).toHaveScreenshot("wallet-connect-dialog.png", {
+ fullPage: true,
+ });
+ });
+
+ test("withdraw dialog screenshot", async ({ page }) => {
+ await preparePage(page, { theme: "light", connect: true });
+ await page.goto("/app/stream/2");
+ await page.locator('button:has-text("Withdraw")').first().click();
+ await expect(page.locator("text=Withdraw funds")).toBeVisible();
+ await expect(page).toHaveScreenshot("withdraw-dialog.png", {
+ fullPage: true,
+ });
+ });
+
+ test("cancel dialog screenshot", async ({ page }) => {
+ await preparePage(page, { theme: "light", connect: true });
+ await page.goto("/app/stream/1");
+ await page.locator('button:has-text("Cancel")').first().click();
+ await expect(page.locator("text=Cancel stream")).toBeVisible();
+ await expect(page).toHaveScreenshot("cancel-dialog.png", {
+ fullPage: true,
+ });
+ });
+});
diff --git a/hooks/use-auto-withdraw.ts b/hooks/use-auto-withdraw.ts
index 110bf4c..b16b74b 100644
--- a/hooks/use-auto-withdraw.ts
+++ b/hooks/use-auto-withdraw.ts
@@ -1,190 +1,200 @@
-'use client'
+"use client";
-import { useState, useEffect, useCallback, useRef } from 'react'
-import { withdrawFromStream } from '@/lib/contract'
-import { getWithdrawableAmount } from '@/lib/stream-utils'
-import type { StreamData } from '@/types/stream'
-import { useNetwork } from '@/components/providers/network-provider'
+import { useState, useEffect, useCallback, useRef } from "react";
+import { withdrawFromStream } from "@/lib/contract";
+import { getWithdrawableAmount } from "@/lib/stream-utils";
+import type { StreamData } from "@/types/stream";
+import { useNetwork } from "@/components/providers/network-provider";
-export type WithdrawStrategy = 'time-based' | 'threshold-based' | 'gas-optimized' | 'max'
+export type WithdrawStrategy =
+ "time-based" | "threshold-based" | "gas-optimized" | "max";
interface WithdrawalHistoryEntry {
- timestamp: number
- amount: string
- txHash?: string
- error?: string
+ timestamp: number;
+ amount: string;
+ txHash?: string;
+ error?: string;
}
interface AutoWithdrawSettings {
- enabled: boolean
- strategy: WithdrawStrategy
- intervalHours: number
- minAmountRaw: string
- maxSafetyLimitRaw: string
- thresholdPercentage: number
- withdrawalHistory: WithdrawalHistoryEntry[]
+ enabled: boolean;
+ strategy: WithdrawStrategy;
+ intervalHours: number;
+ minAmountRaw: string;
+ maxSafetyLimitRaw: string;
+ thresholdPercentage: number;
+ withdrawalHistory: WithdrawalHistoryEntry[];
}
const DEFAULT_SETTINGS: AutoWithdrawSettings = {
enabled: false,
- strategy: 'time-based',
+ strategy: "time-based",
intervalHours: 24,
- minAmountRaw: '0',
- maxSafetyLimitRaw: '0',
+ minAmountRaw: "0",
+ maxSafetyLimitRaw: "0",
thresholdPercentage: 50,
withdrawalHistory: [],
-}
+};
function storageKey(streamId: string) {
- return `flowstar:auto-withdraw:${streamId}`
+ return `flowstar:auto-withdraw:${streamId}`;
}
function loadSettings(streamId: string): AutoWithdrawSettings {
try {
- const stored = localStorage.getItem(storageKey(streamId))
- if (!stored) return DEFAULT_SETTINGS
- return { ...DEFAULT_SETTINGS, ...JSON.parse(stored) }
+ const stored = localStorage.getItem(storageKey(streamId));
+ if (!stored) return DEFAULT_SETTINGS;
+ return { ...DEFAULT_SETTINGS, ...JSON.parse(stored) };
} catch {
- return DEFAULT_SETTINGS
+ return DEFAULT_SETTINGS;
}
}
function saveSettings(streamId: string, settings: AutoWithdrawSettings) {
- localStorage.setItem(storageKey(streamId), JSON.stringify(settings))
+ localStorage.setItem(storageKey(streamId), JSON.stringify(settings));
}
export function useAutoWithdraw(stream: StreamData | null) {
- const { network } = useNetwork()
- const [settings, setSettings] = useState(DEFAULT_SETTINGS)
- const [lastAutoWithdraw, setLastAutoWithdraw] = useState(null)
- const [autoWithdrawPending, setAutoWithdrawPending] = useState(false)
- const intervalRef = useRef | null>(null)
+ const { network } = useNetwork();
+ const [settings, setSettings] =
+ useState(DEFAULT_SETTINGS);
+ const [lastAutoWithdraw, setLastAutoWithdraw] = useState(null);
+ const [autoWithdrawPending, setAutoWithdrawPending] = useState(false);
+ const intervalRef = useRef | null>(null);
useEffect(() => {
- if (stream) setSettings(loadSettings(stream.id))
- }, [stream?.id])
+ if (stream) setSettings(loadSettings(stream.id));
+ }, [stream?.id]);
const updateSettings = useCallback(
(update: Partial) => {
- if (!stream) return
- const next = { ...settings, ...update }
- setSettings(next)
- saveSettings(stream.id, next)
+ if (!stream) return;
+ const next = { ...settings, ...update };
+ setSettings(next);
+ saveSettings(stream.id, next);
},
[stream, settings],
- )
+ );
const addWithdrawalHistory = useCallback(
(entry: WithdrawalHistoryEntry) => {
- if (!stream) return
+ if (!stream) return;
setSettings((prev) => ({
...prev,
- withdrawalHistory: [
- entry,
- ...prev.withdrawalHistory.slice(0, 99),
- ],
- }))
+ withdrawalHistory: [entry, ...prev.withdrawalHistory.slice(0, 99)],
+ }));
saveSettings(stream.id, {
...settings,
- withdrawalHistory: [
- entry,
- ...settings.withdrawalHistory.slice(0, 99),
- ],
- })
+ withdrawalHistory: [entry, ...settings.withdrawalHistory.slice(0, 99)],
+ });
},
[stream, settings],
- )
+ );
const calculateWithdrawAmount = useCallback(
(withdrawable: bigint, stream: StreamData): bigint => {
- const minAmount = BigInt(settings.minAmountRaw || '0')
- const maxLimit = BigInt(settings.maxSafetyLimitRaw || '0')
+ const minAmount = BigInt(settings.minAmountRaw || "0");
+ const maxLimit = BigInt(settings.maxSafetyLimitRaw || "0");
- if (withdrawable <= 0n) return 0n
- if (minAmount > 0n && withdrawable < minAmount) return 0n
+ if (withdrawable <= 0n) return 0n;
+ if (minAmount > 0n && withdrawable < minAmount) return 0n;
- let amount = withdrawable
+ let amount = withdrawable;
switch (settings.strategy) {
- case 'threshold-based': {
- const threshold = (stream.depositedAmount * BigInt(settings.thresholdPercentage)) / 100n
- if (withdrawable < threshold) return 0n
- amount = withdrawable
- break
+ case "threshold-based": {
+ const threshold =
+ (stream.depositedAmount * BigInt(settings.thresholdPercentage)) /
+ 100n;
+ if (withdrawable < threshold) return 0n;
+ amount = withdrawable;
+ break;
}
- case 'gas-optimized': {
- const lastWithdraw = settings.withdrawalHistory[0]
+ case "gas-optimized": {
+ const lastWithdraw = settings.withdrawalHistory[0];
const daysSinceLastWithdraw = lastWithdraw
? (Date.now() - lastWithdraw.timestamp) / (1000 * 60 * 60 * 24)
- : Infinity
- if (daysSinceLastWithdraw < 1) return 0n
- amount = withdrawable
- break
+ : Infinity;
+ if (daysSinceLastWithdraw < 1) return 0n;
+ amount = withdrawable;
+ break;
}
- case 'max': {
- amount = withdrawable
- break
+ case "max": {
+ amount = withdrawable;
+ break;
}
- case 'time-based':
+ case "time-based":
default: {
- amount = withdrawable
- break
+ amount = withdrawable;
+ break;
}
}
if (maxLimit > 0n && amount > maxLimit) {
- amount = maxLimit
+ amount = maxLimit;
}
- return amount
+ return amount;
},
[settings],
- )
+ );
useEffect(() => {
if (intervalRef.current) {
- clearInterval(intervalRef.current)
- intervalRef.current = null
+ clearInterval(intervalRef.current);
+ intervalRef.current = null;
}
- if (!settings.enabled || !stream || stream.cancelled) return
+ if (!settings.enabled || !stream || stream.cancelled) return;
- const intervalMs = settings.intervalHours * 60 * 60 * 1000
+ const intervalMs = settings.intervalHours * 60 * 60 * 1000;
async function tryWithdraw() {
- if (!stream || autoWithdrawPending) return
- const now = Math.floor(Date.now() / 1000)
- const withdrawable = getWithdrawableAmount(stream, now)
- const amount = calculateWithdrawAmount(withdrawable, stream)
+ if (!stream || autoWithdrawPending) return;
+ const now = Math.floor(Date.now() / 1000);
+ const withdrawable = getWithdrawableAmount(stream, now);
+ const amount = calculateWithdrawAmount(withdrawable, stream);
- if (amount <= 0n) return
+ if (amount <= 0n) return;
- setAutoWithdrawPending(true)
+ setAutoWithdrawPending(true);
try {
- const txHash = await withdrawFromStream(stream.id, amount, network)
- setLastAutoWithdraw(Date.now())
+ const txHash = await withdrawFromStream(stream.id, amount, network);
+ setLastAutoWithdraw(Date.now());
addWithdrawalHistory({
timestamp: Date.now(),
amount: amount.toString(),
- txHash,
- })
+ txHash: txHash ?? undefined,
+ });
} catch (error) {
addWithdrawalHistory({
timestamp: Date.now(),
amount: amount.toString(),
error: String(error),
- })
+ });
} finally {
- setAutoWithdrawPending(false)
+ setAutoWithdrawPending(false);
}
}
- intervalRef.current = setInterval(tryWithdraw, intervalMs)
+ intervalRef.current = setInterval(tryWithdraw, intervalMs);
return () => {
- if (intervalRef.current) clearInterval(intervalRef.current)
- }
- }, [settings.enabled, settings.intervalHours, settings.strategy, settings.minAmountRaw, settings.maxSafetyLimitRaw, settings.thresholdPercentage, stream, autoWithdrawPending, calculateWithdrawAmount, addWithdrawalHistory, network])
+ if (intervalRef.current) clearInterval(intervalRef.current);
+ };
+ }, [
+ settings.enabled,
+ settings.intervalHours,
+ settings.strategy,
+ settings.minAmountRaw,
+ settings.maxSafetyLimitRaw,
+ settings.thresholdPercentage,
+ stream,
+ autoWithdrawPending,
+ calculateWithdrawAmount,
+ addWithdrawalHistory,
+ network,
+ ]);
return {
settings,
@@ -193,5 +203,5 @@ export function useAutoWithdraw(stream: StreamData | null) {
autoWithdrawPending,
withdrawalHistory: settings.withdrawalHistory,
addWithdrawalHistory,
- }
+ };
}
diff --git a/hooks/use-contract.ts b/hooks/use-contract.ts
index a39045d..f0c9028 100644
--- a/hooks/use-contract.ts
+++ b/hooks/use-contract.ts
@@ -1,176 +1,186 @@
-'use client'
+"use client";
-import { useState, useCallback } from 'react'
-import { toast } from 'sonner'
+import { useState, useCallback } from "react";
+import { toast } from "sonner";
import {
createStream as createStreamCall,
withdrawFromStream,
cancelStream as cancelStreamCall,
estimateCreateStreamFee,
type TxStep,
-} from '@/lib/contract'
-import type { FeeEstimate } from '@/lib/contract'
-import { invalidateStreams } from '@/hooks/use-streams'
-import { useWallet } from '@/hooks/use-wallet'
-import { getWithdrawableAmount } from '@/lib/stream-utils'
-import { mapError, categoryLabel } from '@/lib/error-messages'
-import type { CreateStreamInput, StreamData } from '@/types/stream'
+} from "@/lib/contract";
+import type { FeeEstimate } from "@/lib/contract";
+import { invalidateStreams } from "@/hooks/use-streams";
+import { useWallet } from "@/hooks/use-wallet";
+import { useNetwork } from "@/components/providers/network-provider";
+import { getWithdrawableAmount } from "@/lib/stream-utils";
+import { mapError, categoryLabel } from "@/lib/error-messages";
+import type { CreateStreamInput, StreamData } from "@/types/stream";
export interface WithdrawAllResult {
- succeeded: number
- failed: number
+ succeeded: number;
+ failed: number;
}
const TX_STEP_LABELS: Record = {
- simulating: 'Simulating transaction…',
- signing: 'Please sign in your wallet',
- submitting: 'Transaction submitted — waiting for confirmation',
- confirming: 'Confirming on-chain…',
-}
+ simulating: "Simulating transaction…",
+ signing: "Please sign in your wallet",
+ submitting: "Transaction submitted — waiting for confirmation",
+ confirming: "Confirming on-chain…",
+};
function showErrorToast(err: unknown, toastId?: string | number) {
- const mapped = mapError(err)
- const category = categoryLabel(mapped.category)
+ const mapped = mapError(err);
+ const category = categoryLabel(mapped.category);
const opts = {
description: mapped.suggestion,
duration: 7000,
...(toastId ? { id: toastId } : {}),
action: mapped.details
? {
- label: 'Details',
+ label: "Details",
onClick: () => {
- const short = mapped.details!.length > 200
- ? mapped.details!.slice(0, 200) + '…'
- : mapped.details!
- toast.info(short, { duration: 10000 })
+ const short =
+ mapped.details!.length > 200
+ ? mapped.details!.slice(0, 200) + "…"
+ : mapped.details!;
+ toast.info(short, { duration: 10000 });
},
}
: undefined,
- }
- toast.error(mapped.message, opts)
- return `[${category}] ${mapped.message}`
+ };
+ toast.error(mapped.message, opts);
+ return `[${category}] ${mapped.message}`;
}
export function useContract() {
- const { address, isConnected } = useWallet()
- const { network } = useNetwork()
- const [pending, setPending] = useState(false)
- const [error, setError] = useState(null)
+ const { address, isConnected } = useWallet();
+ const { network } = useNetwork();
+ const [pending, setPending] = useState(false);
+ const [error, setError] = useState(null);
const run = useCallback(
- async (
+ async (
label: string,
fn: (onStep: (step: TxStep) => void) => Promise,
): Promise => {
- if (!isConnected || !address) throw new Error('Connect a wallet first.')
- setPending(true)
- setError(null)
- const toastId = toast.loading('Simulating transaction…')
+ if (!isConnected || !address) throw new Error("Connect a wallet first.");
+ setPending(true);
+ setError(null);
+ const toastId = toast.loading("Simulating transaction…");
try {
const result = await fn((step) => {
- toast.loading(TX_STEP_LABELS[step], { id: toastId })
- })
- toast.success(`${label} confirmed!`, { id: toastId, duration: 4000 })
- invalidateStreams()
- return result
+ toast.loading(TX_STEP_LABELS[step], { id: toastId });
+ });
+ toast.success(`${label} confirmed!`, { id: toastId, duration: 4000 });
+ invalidateStreams();
+ return result;
} catch (err) {
- showErrorToast(err, toastId)
- const mapped = mapError(err)
- const category = categoryLabel(mapped.category)
- const displayMessage = `[${category}] ${mapped.message}`
- setError(displayMessage)
- throw err
+ showErrorToast(err, toastId);
+ const mapped = mapError(err);
+ const category = categoryLabel(mapped.category);
+ const displayMessage = `[${category}] ${mapped.message}`;
+ setError(displayMessage);
+ throw err;
} finally {
- setPending(false)
+ setPending(false);
}
},
[address, isConnected],
- )
+ );
const createStream = useCallback(
(input: CreateStreamInput) =>
- run('Create stream', (onStep) => createStreamCall(input, address!, network, onStep)),
+ run("Create stream", (onStep) =>
+ createStreamCall(input, address!, network, onStep),
+ ),
[run, address, network],
- )
-
- // const withdraw = useCallback(
- // (id: string, amount: bigint) => run(() => withdrawFromStream(id, amount, network)),
- // (input: CreateStreamInput) => run(() => createStreamCall(network, input, address!)),
- // [run, network, address],
- // )
+ );
- const withdraw = useCallback(
- (id: string, amount: bigint) => run(() => withdrawFromStream(id, amount, network)),
const withdraw = useCallback(
(id: string, amount: bigint) =>
- run('Withdraw', (onStep) => withdrawFromStream(id, amount, network, onStep)),
+ run("Withdraw", (onStep) =>
+ withdrawFromStream(id, amount, network, onStep),
+ ),
[run, network],
- )
+ );
const cancel = useCallback(
- (id: string) => run(() => cancelStreamCall(id, network)),
(id: string) =>
- run('Cancel stream', (onStep) => cancelStreamCall(id, network, onStep)),
+ run("Cancel stream", (onStep) => cancelStreamCall(id, network, onStep)),
[run, network],
- )
+ );
const estimateFee = useCallback(
async (input: CreateStreamInput): Promise => {
- if (!isConnected || !address) return null
+ if (!isConnected || !address) return null;
try {
- return await estimateCreateStreamFee(network, input, address)
+ return await estimateCreateStreamFee(network, input, address);
} catch {
- return null
+ return null;
}
},
[address, isConnected, network],
- )
+ );
const withdrawAll = useCallback(
async (
streams: StreamData[],
onProgress?: (current: number, total: number) => void,
): Promise => {
- if (!isConnected || !address) throw new Error('Connect a wallet first.')
+ if (!isConnected || !address) throw new Error("Connect a wallet first.");
- const now = Math.floor(Date.now() / 1000)
- const withdrawable = streams.filter((s) => getWithdrawableAmount(s, now) > 0n)
- if (withdrawable.length === 0) return { succeeded: 0, failed: 0 }
+ const now = Math.floor(Date.now() / 1000);
+ const withdrawable = streams.filter(
+ (s) => getWithdrawableAmount(s, now) > 0n,
+ );
+ if (withdrawable.length === 0) return { succeeded: 0, failed: 0 };
- setPending(true)
- setError(null)
+ setPending(true);
+ setError(null);
- let succeeded = 0
- let failed = 0
+ let succeeded = 0;
+ let failed = 0;
for (let i = 0; i < withdrawable.length; i++) {
- onProgress?.(i + 1, withdrawable.length)
- const s = withdrawable[i]
+ onProgress?.(i + 1, withdrawable.length);
+ const s = withdrawable[i];
try {
- const amount = getWithdrawableAmount(s, Math.floor(Date.now() / 1000))
- await withdrawFromStream(s.id, amount, network)
- succeeded++
+ const amount = getWithdrawableAmount(
+ s,
+ Math.floor(Date.now() / 1000),
+ );
+ await withdrawFromStream(s.id, amount, network);
+ succeeded++;
} catch (err) {
- failed++
- const mapped = mapError(err)
+ failed++;
+ const mapped = mapError(err);
toast.error(`Stream #${s.id}: ${mapped.message}`, {
description: mapped.suggestion,
duration: 5000,
- })
+ });
}
}
- invalidateStreams()
- setPending(false)
+ invalidateStreams();
+ setPending(false);
if (failed > 0 && succeeded === 0) {
- setError('All withdrawals failed. See error toasts for details.')
+ setError("All withdrawals failed. See error toasts for details.");
}
- return { succeeded, failed }
+ return { succeeded, failed };
},
[address, isConnected, network],
- )
-
- return { createStream, withdraw, cancel, withdrawAll, estimateFee, pending, error }
+ );
+
+ return {
+ createStream,
+ withdraw,
+ cancel,
+ withdrawAll,
+ estimateFee,
+ pending,
+ error,
+ };
}
diff --git a/hooks/use-stream-polling.ts b/hooks/use-stream-polling.ts
deleted file mode 100644
index e5c9cbb..0000000
--- a/hooks/use-stream-polling.ts
+++ /dev/null
@@ -1,270 +0,0 @@
-'use client'
-
-import { useEffect, useRef, useCallback, useState } from 'react'
-import { fetchStream, fetchStreamsForAddress } from '@/lib/contract'
-import type { StreamData } from '@/types/stream'
-
-interface StreamPollingOptions {
- enabled?: boolean
- pollInterval?: number
- onUpdate?: (stream: StreamData) => void
- onError?: (error: Error) => void
-}
-
-interface StreamsDashboardPollingOptions {
- enabled?: boolean
- pollInterval?: number
- onUpdate?: (streams: StreamData[]) => void
- onError?: (error: Error) => void
-}
-
-const DEFAULT_ACTIVE_POLL_INTERVAL = 5000 // 5 seconds for active streams
-const DEFAULT_DASHBOARD_POLL_INTERVAL = 30000 // 30 seconds for dashboard
-const RETRY_DELAY = 3000 // 3 seconds before retry on error
-const MAX_RETRIES = 3
-
-function isStreamActive(stream: StreamData): boolean {
- const now = BigInt(Math.floor(Date.now() / 1000))
- return !stream.cancelled && now < stream.endTime
-}
-
-export function useStreamPolling(
- streamId: string | null,
- options?: StreamPollingOptions,
-): { stream: StreamData | null; loading: boolean; error: Error | null } {
- const [stream, setStream] = useState(null)
- const [loading, setLoading] = useState(false)
- const [error, setError] = useState(null)
-
- const pollIntervalRef = useRef(null)
- const retryCountRef = useRef(0)
-
- const {
- enabled = true,
- pollInterval = DEFAULT_ACTIVE_POLL_INTERVAL,
- onUpdate,
- onError,
- } = options ?? {}
-
- const poll = useCallback(async () => {
- if (!streamId) return
-
- try {
- const data = await fetchStream(streamId)
- if (data) {
- setStream(data)
- setError(null)
- retryCountRef.current = 0
- onUpdate?.(data)
-
- // Adjust polling interval based on stream activity
- const now = BigInt(Math.floor(Date.now() / 1000))
- if (data.cancelled || now >= data.endTime) {
- // Stop polling completed or cancelled streams
- if (pollIntervalRef.current) {
- clearInterval(pollIntervalRef.current)
- pollIntervalRef.current = null
- }
- }
- }
- } catch (err) {
- const error = err instanceof Error ? err : new Error('Polling failed')
- setError(error)
- onError?.(error)
-
- retryCountRef.current += 1
- if (retryCountRef.current >= MAX_RETRIES) {
- if (pollIntervalRef.current) {
- clearInterval(pollIntervalRef.current)
- pollIntervalRef.current = null
- }
- }
- }
- }, [streamId, onUpdate, onError])
-
- useEffect(() => {
- if (!enabled || !streamId) {
- if (pollIntervalRef.current) {
- clearInterval(pollIntervalRef.current)
- pollIntervalRef.current = null
- }
- return
- }
-
- // Initial poll
- setLoading(true)
- poll().finally(() => setLoading(false))
-
- // Set up periodic polling
- pollIntervalRef.current = setInterval(poll, pollInterval)
-
- return () => {
- if (pollIntervalRef.current) {
- clearInterval(pollIntervalRef.current)
- pollIntervalRef.current = null
- }
- }
- }, [enabled, streamId, poll, pollInterval])
-
- return { stream, loading, error }
-}
-
-export function useStreamsDashboardPolling(
- address: string | null,
- options?: StreamsDashboardPollingOptions,
-): { streams: StreamData[]; loading: boolean; error: Error | null } {
- const [streams, setStreams] = useState([])
- const [loading, setLoading] = useState(false)
- const [error, setError] = useState(null)
-
- const pollIntervalRef = useRef(null)
- const retryCountRef = useRef(0)
-
- const {
- enabled = true,
- pollInterval = DEFAULT_DASHBOARD_POLL_INTERVAL,
- onUpdate,
- onError,
- } = options ?? {}
-
- const poll = useCallback(async () => {
- if (!address) return
-
- try {
- const data = await fetchStreamsForAddress(address)
- setStreams(data)
- setError(null)
- retryCountRef.current = 0
- onUpdate?.(data)
- } catch (err) {
- const error = err instanceof Error ? err : new Error('Dashboard polling failed')
- setError(error)
- onError?.(error)
-
- retryCountRef.current += 1
- if (retryCountRef.current >= MAX_RETRIES) {
- if (pollIntervalRef.current) {
- clearInterval(pollIntervalRef.current)
- pollIntervalRef.current = null
- }
- }
- }
- }, [address, onUpdate, onError])
-
- useEffect(() => {
- if (!enabled || !address) {
- if (pollIntervalRef.current) {
- clearInterval(pollIntervalRef.current)
- pollIntervalRef.current = null
- }
- return
- }
-
- // Initial poll
- setLoading(true)
- poll().finally(() => setLoading(false))
-
- // Set up periodic polling
- pollIntervalRef.current = setInterval(poll, pollInterval)
-
- return () => {
- if (pollIntervalRef.current) {
- clearInterval(pollIntervalRef.current)
- pollIntervalRef.current = null
- }
- }
- }, [enabled, address, poll, pollInterval])
-
- return { streams, loading, error }
-}
-
-export function useAdaptiveStreamPolling(
- streamId: string | null,
- options?: StreamPollingOptions & { adaptiveInterval?: boolean },
-): { stream: StreamData | null; loading: boolean; error: Error | null } {
- const [stream, setStream] = useState(null)
- const [loading, setLoading] = useState(false)
- const [error, setError] = useState(null)
- const [currentInterval, setCurrentInterval] = useState(DEFAULT_ACTIVE_POLL_INTERVAL)
-
- const pollIntervalRef = useRef(null)
- const retryCountRef = useRef(0)
-
- const {
- enabled = true,
- pollInterval = DEFAULT_ACTIVE_POLL_INTERVAL,
- adaptiveInterval = true,
- onUpdate,
- onError,
- } = options ?? {}
-
- const poll = useCallback(async () => {
- if (!streamId) return
-
- try {
- const data = await fetchStream(streamId)
- if (data) {
- setStream(data)
- setError(null)
- retryCountRef.current = 0
- onUpdate?.(data)
-
- // Adapt polling interval based on stream activity
- if (adaptiveInterval) {
- const now = BigInt(Math.floor(Date.now() / 1000))
- if (data.cancelled || now >= data.endTime) {
- // Stop polling completed or cancelled streams
- if (pollIntervalRef.current) {
- clearInterval(pollIntervalRef.current)
- pollIntervalRef.current = null
- }
- } else if (now >= data.cliffTime && now < data.endTime) {
- // Active streaming: use 5 second interval
- setCurrentInterval(DEFAULT_ACTIVE_POLL_INTERVAL)
- } else {
- // Before cliff or waiting for start: use 30 second interval
- setCurrentInterval(DEFAULT_DASHBOARD_POLL_INTERVAL)
- }
- }
- }
- } catch (err) {
- const error = err instanceof Error ? err : new Error('Polling failed')
- setError(error)
- onError?.(error)
-
- retryCountRef.current += 1
- if (retryCountRef.current >= MAX_RETRIES) {
- if (pollIntervalRef.current) {
- clearInterval(pollIntervalRef.current)
- pollIntervalRef.current = null
- }
- }
- }
- }, [streamId, adaptiveInterval, onUpdate, onError])
-
- useEffect(() => {
- if (!enabled || !streamId) {
- if (pollIntervalRef.current) {
- clearInterval(pollIntervalRef.current)
- pollIntervalRef.current = null
- }
- return
- }
-
- // Initial poll
- setLoading(true)
- poll().finally(() => setLoading(false))
-
- // Set up periodic polling with current interval
- pollIntervalRef.current = setInterval(poll, currentInterval)
-
- return () => {
- if (pollIntervalRef.current) {
- clearInterval(pollIntervalRef.current)
- pollIntervalRef.current = null
- }
- }
- }, [enabled, streamId, currentInterval, poll])
-
- return { stream, loading, error }
-}
diff --git a/hooks/use-token-price.ts b/hooks/use-token-price.ts
index ff746ab..93fb23d 100644
--- a/hooks/use-token-price.ts
+++ b/hooks/use-token-price.ts
@@ -1,141 +1,145 @@
-'use client'
+"use client";
-import { useState, useEffect, useRef } from 'react'
-import type { StreamData } from '@/types/stream'
+import { useState, useEffect, useRef } from "react";
+import type { StreamData } from "@/types/stream";
interface TokenPrice {
- usdPrice: number | null
- lastUpdated: number | null
- loading: boolean
- stale: boolean
+ usdPrice: number | null;
+ lastUpdated: number | null;
+ loading: boolean;
+ stale: boolean;
}
-const PRICE_CACHE: Record = {}
-const STALE_THRESHOLD_MS = 5 * 60 * 1000 // 5 minutes
-const STALENESS_WARNING_MS = 2 * 60 * 1000 // warn if price is >2 min old on display
+const PRICE_CACHE: Record = {};
+const STALE_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
+const STALENESS_WARNING_MS = 2 * 60 * 1000; // warn if price is >2 min old on display
async function fetchXlmUsdPrice(): Promise {
const res = await fetch(
- 'https://api.stellar.expert/explorer/public/asset/XLM/price',
- { next: { revalidate: 60 } }
- )
- if (!res.ok) throw new Error('price fetch failed')
- const json = await res.json()
- return Number(json.price ?? json.close ?? json.last)
+ "https://api.stellar.expert/explorer/public/asset/XLM/price",
+ { next: { revalidate: 60 } },
+ );
+ if (!res.ok) throw new Error("price fetch failed");
+ const json = await res.json();
+ return Number(json.price ?? json.close ?? json.last);
}
export function formatUsd(value: number): string {
if (value < 1) {
- return '$' + value.toFixed(4)
+ return "$" + value.toFixed(4);
}
if (value < 1000) {
- return '$' + value.toFixed(2)
+ return "$" + value.toFixed(2);
}
- return '$' + value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
+ return (
+ "$" +
+ value.toLocaleString("en-US", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ })
+ );
}
export function useTokenPrice(symbol: string): TokenPrice {
- const [price, setPrice] = useState(null)
- const [fetchedAt, setFetchedAt] = useState(null)
- const [loading, setLoading] = useState(false)
- const timerRef = useRef | null>(null)
+ const [price, setPrice] = useState(null);
+ const [fetchedAt, setFetchedAt] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const timerRef = useRef | null>(null);
- const isStablecoin = symbol === 'USDC' || symbol === 'EURC'
+ const isStablecoin = symbol === "USDC" || symbol === "EURC";
useEffect(() => {
if (isStablecoin) {
- setPrice(1)
- setFetchedAt(Date.now())
- return
+ setPrice(1);
+ setFetchedAt(Date.now());
+ return;
}
- if (symbol !== 'XLM') {
- setPrice(null)
- setFetchedAt(null)
- return
+ if (symbol !== "XLM") {
+ setPrice(null);
+ setFetchedAt(null);
+ return;
}
- const cached = PRICE_CACHE[symbol]
+ const cached = PRICE_CACHE[symbol];
if (cached && Date.now() - cached.fetchedAt < STALE_THRESHOLD_MS) {
- setPrice(cached.price)
- setFetchedAt(cached.fetchedAt)
- return
+ setPrice(cached.price);
+ setFetchedAt(cached.fetchedAt);
+ return;
}
- setLoading(true)
+ setLoading(true);
fetchXlmUsdPrice()
.then((p) => {
- PRICE_CACHE[symbol] = { price: p, fetchedAt: Date.now() }
- setPrice(p)
- setFetchedAt(Date.now())
+ PRICE_CACHE[symbol] = { price: p, fetchedAt: Date.now() };
+ setPrice(p);
+ setFetchedAt(Date.now());
})
.catch(() => {
- setPrice(null)
+ setPrice(null);
})
- .finally(() => setLoading(false))
+ .finally(() => setLoading(false));
timerRef.current = setTimeout(() => {
- setPrice(null)
- setFetchedAt(null)
- }, STALE_THRESHOLD_MS)
+ setPrice(null);
+ setFetchedAt(null);
+ }, STALE_THRESHOLD_MS);
return () => {
- if (timerRef.current) clearTimeout(timerRef.current)
- }
- }, [symbol, isStablecoin])
-
- const stale = fetchedAt !== null && Date.now() - fetchedAt > STALENESS_WARNING_MS
+ if (timerRef.current) clearTimeout(timerRef.current);
+ };
+ }, [symbol, isStablecoin]);
- return { usdPrice: price, lastUpdated: fetchedAt, loading, stale }
-}
+ const stale =
+ fetchedAt !== null && Date.now() - fetchedAt > STALENESS_WARNING_MS;
-/** Format a USD value with context-aware decimal places. */
-export function formatUsd(value: number): string {
- if (value < 1) {
- return '$' + value.toFixed(4)
- }
- if (value < 1000) {
- return '$' + value.toFixed(2)
- }
- return '$' + value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
+ return { usdPrice: price, lastUpdated: fetchedAt, loading, stale };
}
interface PortfolioValue {
- totalUsd: number | null
- loading: boolean
- stale: boolean
+ totalUsd: number | null;
+ loading: boolean;
+ stale: boolean;
}
// Internal hook to get price for a single symbol — called conditionally per unique symbol.
// We collect unique symbols and call useTokenPrice for each (hooks must not be called conditionally,
// so we support up to a fixed set of known symbols).
// We support a fixed set of symbols to avoid conditional hook calls.
-const KNOWN_SYMBOLS = ['XLM', 'USDC', 'EURC']
+const KNOWN_SYMBOLS = ["XLM", "USDC", "EURC"];
export function usePortfolioValue(streams: StreamData[]): PortfolioValue {
- const xlm = useTokenPrice('XLM')
- const usdc = useTokenPrice('USDC')
- const eurc = useTokenPrice('EURC')
-
- const priceMap: Record = { XLM: xlm, USDC: usdc, EURC: eurc }
-
- const usedSymbols = [...new Set(streams.map((s) => s.token.symbol))]
-
- const loading = usedSymbols.some((sym) => KNOWN_SYMBOLS.includes(sym) && priceMap[sym]?.loading)
- const stale = usedSymbols.some((sym) => KNOWN_SYMBOLS.includes(sym) && priceMap[sym]?.stale)
-
- let totalUsd: number | null = 0
+ const xlm = useTokenPrice("XLM");
+ const usdc = useTokenPrice("USDC");
+ const eurc = useTokenPrice("EURC");
+
+ const priceMap: Record = {
+ XLM: xlm,
+ USDC: usdc,
+ EURC: eurc,
+ };
+
+ const usedSymbols = [...new Set(streams.map((s) => s.token.symbol))];
+
+ const loading = usedSymbols.some(
+ (sym) => KNOWN_SYMBOLS.includes(sym) && priceMap[sym]?.loading,
+ );
+ const stale = usedSymbols.some(
+ (sym) => KNOWN_SYMBOLS.includes(sym) && priceMap[sym]?.stale,
+ );
+
+ let totalUsd: number | null = 0;
for (const stream of streams) {
- const tp = priceMap[stream.token.symbol]
+ const tp = priceMap[stream.token.symbol];
if (!tp || tp.usdPrice === null) {
- totalUsd = null
- break
+ totalUsd = null;
+ break;
}
- const locked = stream.depositedAmount - stream.withdrawnAmount
- const human = Number(locked) / Math.pow(10, stream.token.decimals)
- totalUsd = (totalUsd ?? 0) + human * tp.usdPrice
+ const locked = stream.depositedAmount - stream.withdrawnAmount;
+ const human = Number(locked) / Math.pow(10, stream.token.decimals);
+ totalUsd = (totalUsd ?? 0) + human * tp.usdPrice;
}
- return { totalUsd, loading, stale }
+ return { totalUsd, loading, stale };
}
diff --git a/hooks/useNetworkGuard.ts b/hooks/useNetworkGuard.ts
deleted file mode 100644
index 65f8ac2..0000000
--- a/hooks/useNetworkGuard.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { useState, useCallback } from 'react';
-import { APP_NETWORK, isNetworkMatch, getNetworkName } from '@/lib/network';
-
-export interface NetworkGuardState {
- mismatch: boolean;
- walletNetwork: string | null;
- appNetwork: string;
- check: () => Promise;
-}
-
-export function useNetworkGuard(): NetworkGuardState {
- const [walletNetwork, setWalletNetwork] = useState(null);
- const [mismatch, setMismatch] = useState(false);
-
- const check = useCallback(async (): Promise => {
- try {
- // Freighter API — adjust if you use a different wallet kit
- const { freighterApi } = await import('@stellar/freighter-api');
- const network = await freighterApi.getNetwork();
- setWalletNetwork(network);
-
- const ok = isNetworkMatch(network);
- setMismatch(!ok);
- return ok;
- } catch {
- return false;
- }
- }, []);
-
- return {
- mismatch,
- walletNetwork,
- appNetwork: getNetworkName(APP_NETWORK),
- check,
- };
-}
\ No newline at end of file
diff --git a/lib/airdrop.ts b/lib/airdrop.ts
deleted file mode 100644
index ffb97f4..0000000
--- a/lib/airdrop.ts
+++ /dev/null
@@ -1,221 +0,0 @@
-import { NETWORK } from '@/lib/stellar'
-import type { StreamData } from '@/types/stream'
-
-export interface TokenHolder {
- address: string
- balance: bigint
-}
-
-export interface AirdropConfig {
- tokenAddress: string
- amountPerHolder: bigint
- duration: bigint
- cliff: bigint
- cliffAmount: bigint
-}
-
-export interface BatchCreateProgress {
- total: number
- completed: number
- failed: number
- current: string
- errors: Map
-}
-
-const MAX_BATCH_SIZE = 100
-const BATCH_TIMEOUT = 30000 // 30 seconds per batch request
-
-export async function fetchTokenHolders(
- tokenAddress: string,
- limit: number = MAX_BATCH_SIZE,
-): Promise {
- try {
- const response = await fetch(
- `${NETWORK.horizonUrl}/accounts?signer=${tokenAddress}&limit=${limit}`,
- { timeout: BATCH_TIMEOUT },
- )
-
- if (!response.ok) {
- throw new Error(`Failed to fetch token holders: ${response.statusText}`)
- }
-
- const data = await response.json() as { _embedded?: { records?: Array<{ id: string }> } }
- const records = data._embedded?.records ?? []
-
- return records
- .slice(0, limit)
- .map((record) => ({
- address: record.id,
- balance: BigInt(0),
- }))
- } catch (error) {
- console.error('Error fetching token holders:', error)
- throw error
- }
-}
-
-export function validateBatchConfig(config: AirdropConfig): { valid: boolean; errors: string[] } {
- const errors: string[] = []
-
- if (!config.tokenAddress || config.tokenAddress.length === 0) {
- errors.push('Token address is required')
- }
-
- if (config.amountPerHolder <= 0n) {
- errors.push('Amount per holder must be greater than 0')
- }
-
- if (config.duration <= 0n) {
- errors.push('Duration must be greater than 0')
- }
-
- if (config.cliff < 0n) {
- errors.push('Cliff time cannot be negative')
- }
-
- if (config.cliffAmount < 0n) {
- errors.push('Cliff amount cannot be negative')
- }
-
- if (config.cliffAmount > config.amountPerHolder) {
- errors.push('Cliff amount cannot exceed amount per holder')
- }
-
- return {
- valid: errors.length === 0,
- errors,
- }
-}
-
-export function createBatchStreamParams(
- holders: TokenHolder[],
- config: AirdropConfig,
- startTime: bigint = BigInt(Math.floor(Date.now() / 1000)),
-): Array<{
- recipient: string
- amount: bigint
- startTime: bigint
- endTime: bigint
- cliffTime: bigint
- cliffAmount: bigint
-}> {
- const params: Array<{
- recipient: string
- amount: bigint
- startTime: bigint
- endTime: bigint
- cliffTime: bigint
- cliffAmount: bigint
- }> = []
-
- for (const holder of holders) {
- params.push({
- recipient: holder.address,
- amount: config.amountPerHolder,
- startTime,
- endTime: startTime + config.duration,
- cliffTime: startTime + config.cliff,
- cliffAmount: config.cliffAmount,
- })
- }
-
- return params
-}
-
-export interface BatchRow {
- index: number
- recipient: string
- amount: string
- startTime: string
- endTime: string
- cliffDuration?: string
- cliffAmount?: string
- errors: string[]
-}
-
-export function parseAirdropCsv(csvText: string): { rows: BatchRow[]; errors: string[] } {
- const lines = csvText
- .replace(/\r\n/g, '\n')
- .split('\n')
- .map((line) => line.trim())
- .filter((line) => line.length > 0)
-
- const errors: string[] = []
- const rows: BatchRow[] = []
-
- if (lines.length === 0) {
- return { rows, errors }
- }
-
- // Parse header to find column indices
- const headerLine = lines[0]
- const headers = headerLine
- .split(',')
- .map((h) => h.trim().toLowerCase())
-
- const colIndices = {
- recipient: headers.indexOf('recipient'),
- amount: headers.indexOf('amount'),
- startTime: headers.indexOf('start_time'),
- endTime: headers.indexOf('end_time'),
- cliffDuration: headers.indexOf('cliff_duration'),
- cliffAmount: headers.indexOf('cliff_amount'),
- }
-
- // Validate required columns
- if (
- colIndices.recipient === -1
- || colIndices.amount === -1
- || colIndices.startTime === -1
- || colIndices.endTime === -1
- ) {
- errors.push(
- 'CSV must have columns: recipient, amount, start_time, end_time',
- )
- return { rows, errors }
- }
-
- // Parse data rows
- for (let i = 1; i < lines.length; i += 1) {
- const values = lines[i].split(',').map((v) => v.trim())
-
- const row: BatchRow = {
- index: i,
- recipient: values[colIndices.recipient] ?? '',
- amount: values[colIndices.amount] ?? '',
- startTime: values[colIndices.startTime] ?? '',
- endTime: values[colIndices.endTime] ?? '',
- cliffDuration: colIndices.cliffDuration !== -1 ? values[colIndices.cliffDuration] : undefined,
- cliffAmount: colIndices.cliffAmount !== -1 ? values[colIndices.cliffAmount] : undefined,
- errors: [],
- }
-
- // Validate row
- if (!row.recipient) row.errors.push('Missing recipient')
- if (!row.amount) row.errors.push('Missing amount')
- if (!row.startTime) row.errors.push('Missing start_time')
- if (!row.endTime) row.errors.push('Missing end_time')
-
- // Validate format
- if (row.recipient && !row.recipient.startsWith('G')) {
- row.errors.push('Invalid recipient address format')
- }
-
- try {
- if (row.amount) BigInt(row.amount)
- } catch {
- row.errors.push('Amount must be a valid integer')
- }
-
- try {
- if (row.startTime) BigInt(row.startTime)
- if (row.endTime) BigInt(row.endTime)
- } catch {
- row.errors.push('Times must be valid UNIX timestamps')
- }
-
- rows.push(row)
- }
-
- return { rows, errors }
-}
diff --git a/lib/contract.ts b/lib/contract.ts
index 0774994..86460cd 100644
--- a/lib/contract.ts
+++ b/lib/contract.ts
@@ -6,74 +6,88 @@ import {
scValToNative,
xdr,
rpc as StellarRpc,
-} from '@stellar/stellar-sdk'
-import { type NetworkName, getNetworkConfig, getServer, getAllTokens } from '@/lib/stellar'
-import { mockStore } from '@/lib/mock-data'
-import type { CreateStreamInput, StreamData, TokenInfo } from '@/types/stream'
+} from "@stellar/stellar-sdk";
+import {
+ type NetworkName,
+ getNetworkConfig,
+ getServer,
+ getAllTokens,
+} from "@/lib/stellar";
+import { mockStore } from "@/lib/mock-data";
+import type { CreateStreamInput, StreamData, TokenInfo } from "@/types/stream";
// ─── Helpers ──────────────────────────────────────────────────────────────────
/** Wallet sign callback — must be set by WalletProvider before any write. */
-let _signTransaction: ((xdr: string) => Promise) | null = null
+let _signTransaction: ((xdr: string) => Promise) | null = null;
export function setSignTransaction(fn: (xdr: string) => Promise) {
- _signTransaction = fn
+ _signTransaction = fn;
}
async function signTx(xdrStr: string): Promise {
- if (!_signTransaction) throw new Error('Wallet not connected')
- return _signTransaction(xdrStr)
+ if (!_signTransaction) throw new Error("Wallet not connected");
+ return _signTransaction(xdrStr);
}
-const FEE_BUFFER = 1.15 // 15% above minimum to ensure inclusion
+const FEE_BUFFER = 1.15; // 15% above minimum to ensure inclusion
// ─── Retry / timeout ──────────────────────────────────────────────────────────
-const REQUEST_TIMEOUT_MS = 30_000
-const POLL_TIMEOUT_MS = 60_000
-const MAX_RETRIES = 3
-const RETRY_DELAYS_MS = [1_000, 2_000, 4_000] as const
+const REQUEST_TIMEOUT_MS = 30_000;
+const POLL_TIMEOUT_MS = 60_000;
+const MAX_RETRIES = 3;
+const RETRY_DELAYS_MS = [1_000, 2_000, 4_000] as const;
function isRetryableError(err: unknown): boolean {
- if (err instanceof TypeError) return true // network failure
- if (err instanceof Error && (err.message.includes('503') || err.message.includes('429'))) return true
+ if (err instanceof TypeError) return true; // network failure
+ if (
+ err instanceof Error &&
+ (err.message.includes("503") || err.message.includes("429"))
+ )
+ return true;
const status =
(err as { status?: number })?.status ??
- (err as { response?: { status?: number } })?.response?.status
- return status === 429 || status === 503
+ (err as { response?: { status?: number } })?.response?.status;
+ return status === 429 || status === 503;
}
-async function fetchWithRetry(url: string, init: RequestInit): Promise {
- let lastErr: unknown
+async function fetchWithRetry(
+ url: string,
+ init: RequestInit,
+): Promise {
+ let lastErr: unknown;
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
- const controller = new AbortController()
- const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS)
+ const controller = new AbortController();
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
try {
- const res = await fetch(url, { ...init, signal: controller.signal })
- if (res.status !== 429 && res.status !== 503) return res
- lastErr = new Error(`HTTP ${res.status}`)
+ const res = await fetch(url, { ...init, signal: controller.signal });
+ if (res.status !== 429 && res.status !== 503) return res;
+ lastErr = new Error(`HTTP ${res.status}`);
} catch (err) {
- lastErr = err
+ lastErr = err;
} finally {
- clearTimeout(timer)
+ clearTimeout(timer);
}
- if (attempt < MAX_RETRIES) await new Promise((r) => setTimeout(r, RETRY_DELAYS_MS[attempt]))
+ if (attempt < MAX_RETRIES)
+ await new Promise((r) => setTimeout(r, RETRY_DELAYS_MS[attempt]));
}
- throw lastErr
+ throw lastErr;
}
async function withRetry(fn: () => Promise): Promise {
- let lastErr: unknown
+ let lastErr: unknown;
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
try {
- return await fn()
+ return await fn();
} catch (err) {
- lastErr = err
- if (!isRetryableError(err)) throw err
+ lastErr = err;
+ if (!isRetryableError(err)) throw err;
}
- if (attempt < MAX_RETRIES) await new Promise((r) => setTimeout(r, RETRY_DELAYS_MS[attempt]))
+ if (attempt < MAX_RETRIES)
+ await new Promise((r) => setTimeout(r, RETRY_DELAYS_MS[attempt]));
}
- throw lastErr
+ throw lastErr;
}
/** Build and simulate a contract call, returning the prepared tx and estimated fee. */
@@ -84,33 +98,33 @@ async function buildAndSimulate(
signerAddress: string,
contractAddress: string,
) {
- const config = getNetworkConfig(network)
- const server = getServer(network)
- const contract = new Contract(contractAddress)
- const account = await withRetry(() => server.getAccount(signerAddress))
+ const config = getNetworkConfig(network);
+ const server = getServer(network);
+ const contract = new Contract(contractAddress);
+ const account = await withRetry(() => server.getAccount(signerAddress));
const tx = new TransactionBuilder(account, {
- fee: '100000',
+ fee: "100000",
networkPassphrase: config.passphrase,
})
.addOperation(contract.call(method, ...args))
.setTimeout(180)
- .build()
+ .build();
- const sim = await withRetry(() => server.simulateTransaction(tx))
+ const sim = await withRetry(() => server.simulateTransaction(tx));
if (StellarRpc.Api.isSimulationError(sim)) {
- throw new Error(`Simulation failed: ${sim.error}`)
+ throw new Error(`Simulation failed: ${sim.error}`);
}
- const successSim = sim as StellarRpc.Api.SimulateTransactionSuccessResponse
- const minFee = Number(successSim.minResourceFee ?? 0)
- const estimatedFee = Math.ceil(minFee * FEE_BUFFER)
- const prepared = StellarRpc.assembleTransaction(tx, sim).build()
+ const successSim = sim as StellarRpc.Api.SimulateTransactionSuccessResponse;
+ const minFee = Number(successSim.minResourceFee ?? 0);
+ const estimatedFee = Math.ceil(minFee * FEE_BUFFER);
+ const prepared = StellarRpc.assembleTransaction(tx, sim).build();
- return { prepared, estimatedFee, minFee }
+ return { prepared, estimatedFee, minFee };
}
-export type TxStep = 'simulating' | 'signing' | 'submitting' | 'confirming'
+export type TxStep = "simulating" | "signing" | "submitting" | "confirming";
/** Build, simulate, sign, and submit a contract call. Returns the transaction hash. */
async function invoke(
@@ -121,69 +135,79 @@ async function invoke(
contractAddress: string,
onStep?: (step: TxStep) => void,
): Promise {
- const config = getNetworkConfig(network)
- onStep?.('simulating')
- const { prepared } = await buildAndSimulate(network, method, args, signerAddress, contractAddress)
- onStep?.('signing')
- const signedXdr = await signTx(prepared.toXDR())
- onStep?.('submitting')
+ const config = getNetworkConfig(network);
+ onStep?.("simulating");
+ const { prepared } = await buildAndSimulate(
+ network,
+ method,
+ args,
+ signerAddress,
+ contractAddress,
+ );
+ onStep?.("signing");
+ const signedXdr = await signTx(prepared.toXDR());
+ onStep?.("submitting");
// Submit the signed XDR directly via the RPC JSON-RPC endpoint.
// We bypass TransactionBuilder.fromXDR because Freighter may return a
// FeeBumpTransaction envelope (type 4) which fromXDR can't handle.
const rpcResponse = await fetchWithRetry(config.rpcUrl, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
body: JSON.stringify({
- jsonrpc: '2.0',
+ jsonrpc: "2.0",
id: 1,
- method: 'sendTransaction',
+ method: "sendTransaction",
params: { transaction: signedXdr },
}),
- })
- const rpcJson = await rpcResponse.json() as {
- result?: { hash: string; status: string; errorResultXdr?: string }
- error?: { message: string }
- }
+ });
+ const rpcJson = (await rpcResponse.json()) as {
+ result?: { hash: string; status: string; errorResultXdr?: string };
+ error?: { message: string };
+ };
if (rpcJson.error) {
- throw new Error(`Transaction failed: ${rpcJson.error.message}`)
+ throw new Error(`Transaction failed: ${rpcJson.error.message}`);
}
- const sendResult = rpcJson.result!
- if (sendResult.status === 'ERROR') {
- throw new Error(`Transaction failed: ${sendResult.errorResultXdr ?? 'unknown error'}`)
+ const sendResult = rpcJson.result!;
+ if (sendResult.status === "ERROR") {
+ throw new Error(
+ `Transaction failed: ${sendResult.errorResultXdr ?? "unknown error"}`,
+ );
}
// Poll until finalized (max 60 s)
- const hash = sendResult.hash
- onStep?.('confirming')
- let pollStatus = 'PENDING'
- const pollDeadline = Date.now() + POLL_TIMEOUT_MS
-
- while (pollStatus !== 'SUCCESS' && pollStatus !== 'FAILED') {
- if (Date.now() >= pollDeadline) throw new Error('Transaction confirmation timed out after 60s')
- await new Promise((r) => setTimeout(r, 2000))
+ const hash = sendResult.hash;
+ onStep?.("confirming");
+ let pollStatus = "PENDING";
+ const pollDeadline = Date.now() + POLL_TIMEOUT_MS;
+
+ while (pollStatus !== "SUCCESS" && pollStatus !== "FAILED") {
+ if (Date.now() >= pollDeadline)
+ throw new Error("Transaction confirmation timed out after 60s");
+ await new Promise((r) => setTimeout(r, 2000));
const pollRes = await fetchWithRetry(config.rpcUrl, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
body: JSON.stringify({
- jsonrpc: '2.0',
+ jsonrpc: "2.0",
id: 1,
- method: 'getTransaction',
+ method: "getTransaction",
params: { hash },
}),
- })
- const pollJson = await pollRes.json() as {
- result?: { status: string; resultMetaXdr?: string }
- error?: { message: string }
- }
- if (pollJson.error) throw new Error(`Poll failed: ${pollJson.error.message}`)
- pollStatus = pollJson.result!.status
+ });
+ const pollJson = (await pollRes.json()) as {
+ result?: { status: string; resultMetaXdr?: string };
+ error?: { message: string };
+ };
+ if (pollJson.error)
+ throw new Error(`Poll failed: ${pollJson.error.message}`);
+ pollStatus = pollJson.result!.status;
}
- if (pollStatus === 'FAILED') throw new Error('Transaction failed on-chain')
+ if (pollStatus === "FAILED") throw new Error("Transaction failed on-chain");
- return hash
+ return hash;
}
/** Simulate a read-only call (no signing). */
@@ -193,44 +217,48 @@ async function query(
args: xdr.ScVal[],
contractAddress: string,
): Promise {
- const config = getNetworkConfig(network)
- const server = getServer(network)
- const contract = new Contract(contractAddress)
+ const config = getNetworkConfig(network);
+ const server = getServer(network);
+ const contract = new Contract(contractAddress);
// Use a dummy account for simulation reads
const dummyKeypair = {
- accountId: () => 'GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN',
+ accountId: () => "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN",
sequence: () => BigInt(0),
incrementSequenceNumber: () => {},
- }
- const account = await withRetry(() => server.getAccount(dummyKeypair.accountId()))
+ };
+ const account = await withRetry(() =>
+ server.getAccount(dummyKeypair.accountId()),
+ );
const tx = new TransactionBuilder(account, {
- fee: '100',
+ fee: "100",
networkPassphrase: config.passphrase,
})
.addOperation(contract.call(method, ...args))
.setTimeout(10)
- .build()
+ .build();
- const sim = await withRetry(() => server.simulateTransaction(tx))
+ const sim = await withRetry(() => server.simulateTransaction(tx));
if (StellarRpc.Api.isSimulationError(sim)) {
- throw new Error(`Query failed: ${sim.error}`)
+ throw new Error(`Query failed: ${sim.error}`);
}
- return (sim as StellarRpc.Api.SimulateTransactionSuccessResponse).result?.retval
- ?? xdr.ScVal.scvVoid()
+ return (
+ (sim as StellarRpc.Api.SimulateTransactionSuccessResponse).result?.retval ??
+ xdr.ScVal.scvVoid()
+ );
}
/** Map a contract Stream ScVal → StreamData. */
function scValToStreamData(network: NetworkName, val: xdr.ScVal): StreamData {
- const raw = scValToNative(val) as Record
+ const raw = scValToNative(val) as Record;
- const tokenAddress = String(raw.token)
- const knownTokens = getAllTokens(network)
- const knownToken = knownTokens.find((t) => t.address === tokenAddress)
+ const tokenAddress = String(raw.token);
+ const knownTokens = getAllTokens(network);
+ const knownToken = knownTokens.find((t) => t.address === tokenAddress);
const token: TokenInfo = knownToken ?? {
address: tokenAddress,
- symbol: 'UNK',
+ symbol: "UNK",
decimals: 7,
- }
+ };
return {
id: String(raw.id),
@@ -245,24 +273,24 @@ function scValToStreamData(network: NetworkName, val: xdr.ScVal): StreamData {
cliffAmount: BigInt(raw.cliff_amount as string | number),
amountPerSecond: BigInt(raw.amount_per_second as string | number),
cancelled: Boolean(raw.cancelled),
- }
+ };
}
// ─── Public API ───────────────────────────────────────────────────────────────
export interface FeeEstimate {
- minFee: number
- estimatedFee: number
- estimatedFeeXlm: string
+ minFee: number;
+ estimatedFee: number;
+ estimatedFeeXlm: string;
}
export interface SimulationPreview {
- success: boolean
- estimatedFeeXlm: string
- estimatedFeeUsd: string
- cpuInstructions: number
- memoryBytes: number
- errorMessage?: string
+ success: boolean;
+ estimatedFeeXlm: string;
+ estimatedFeeUsd: string;
+ cpuInstructions: number;
+ memoryBytes: number;
+ errorMessage?: string;
}
/** Run a dry-run simulation and return a structured preview for display. */
@@ -271,85 +299,88 @@ export async function simulateCreateStreamPreview(
input: CreateStreamInput,
sender: string,
): Promise {
- const config = getNetworkConfig(network)
- const isMockMode = !config.streamContractId
+ const config = getNetworkConfig(network);
+ const isMockMode = !config.streamContractId;
if (isMockMode) {
return {
success: true,
- estimatedFeeXlm: '0.0115',
- estimatedFeeUsd: '~$0.001',
+ estimatedFeeXlm: "0.0115",
+ estimatedFeeUsd: "~$0.001",
cpuInstructions: 42_000,
memoryBytes: 128,
- }
+ };
}
try {
- const server = getServer(network)
- const contract = new Contract(config.streamContractId)
- const account = await withRetry(() => server.getAccount(sender))
+ const server = getServer(network);
+ const contract = new Contract(config.streamContractId);
+ const account = await withRetry(() => server.getAccount(sender));
const params = xdr.ScVal.scvMap(
[
- ['cliff_amount', nativeToScVal(input.cliffAmount, { type: 'i128' })],
- ['cliff_time', nativeToScVal(input.cliffTime, { type: 'u64' })],
- ['end_time', nativeToScVal(input.endTime, { type: 'u64' })],
- ['recipient', new Address(input.recipient).toScVal()],
- ['start_time', nativeToScVal(input.startTime, { type: 'u64' })],
- ['token', new Address(input.token.address).toScVal()],
- ['total_amount', nativeToScVal(input.totalAmount, { type: 'i128' })],
- ].map(([k, v]) =>
- new xdr.ScMapEntry({
- key: xdr.ScVal.scvSymbol(k as string),
- val: v as xdr.ScVal,
- }),
+ ["cliff_amount", nativeToScVal(input.cliffAmount, { type: "i128" })],
+ ["cliff_time", nativeToScVal(input.cliffTime, { type: "u64" })],
+ ["end_time", nativeToScVal(input.endTime, { type: "u64" })],
+ ["recipient", new Address(input.recipient).toScVal()],
+ ["start_time", nativeToScVal(input.startTime, { type: "u64" })],
+ ["token", new Address(input.token.address).toScVal()],
+ ["total_amount", nativeToScVal(input.totalAmount, { type: "i128" })],
+ ].map(
+ ([k, v]) =>
+ new xdr.ScMapEntry({
+ key: xdr.ScVal.scvSymbol(k as string),
+ val: v as xdr.ScVal,
+ }),
),
- )
+ );
const tx = new TransactionBuilder(account, {
- fee: '100000',
+ fee: "100000",
networkPassphrase: config.passphrase,
})
- .addOperation(contract.call('create_stream', new Address(sender).toScVal(), params))
+ .addOperation(
+ contract.call("create_stream", new Address(sender).toScVal(), params),
+ )
.setTimeout(180)
- .build()
+ .build();
- const sim = await withRetry(() => server.simulateTransaction(tx))
+ const sim = await withRetry(() => server.simulateTransaction(tx));
if (StellarRpc.Api.isSimulationError(sim)) {
return {
success: false,
- estimatedFeeXlm: '0',
- estimatedFeeUsd: '—',
+ estimatedFeeXlm: "0",
+ estimatedFeeUsd: "—",
cpuInstructions: 0,
memoryBytes: 0,
errorMessage: sim.error,
- }
+ };
}
- const successSim = sim as StellarRpc.Api.SimulateTransactionSuccessResponse
- const minFee = Number(successSim.minResourceFee ?? 0)
- const estimatedFee = Math.ceil(minFee * FEE_BUFFER)
- const feeXlm = (estimatedFee / 1e7).toFixed(4)
- const feeUsd = `~$${(estimatedFee / 1e7 * 0.08).toFixed(4)}`
- const cost = successSim.cost ?? { cpuInsns: '0', memBytes: '0' }
+ const successSim = sim as StellarRpc.Api.SimulateTransactionSuccessResponse;
+ const minFee = Number(successSim.minResourceFee ?? 0);
+ const estimatedFee = Math.ceil(minFee * FEE_BUFFER);
+ const feeXlm = (estimatedFee / 1e7).toFixed(4);
+ const feeUsd = `~$${((estimatedFee / 1e7) * 0.08).toFixed(4)}`;
+ const resources = successSim.transactionData.build().resources();
return {
success: true,
estimatedFeeXlm: feeXlm,
estimatedFeeUsd: feeUsd,
- cpuInstructions: Number(cost.cpuInsns ?? 0),
- memoryBytes: Number(cost.memBytes ?? 0),
- }
+ cpuInstructions: resources.instructions(),
+ memoryBytes: resources.readBytes() + resources.writeBytes(),
+ };
} catch (err) {
return {
success: false,
- estimatedFeeXlm: '0',
- estimatedFeeUsd: '—',
+ estimatedFeeXlm: "0",
+ estimatedFeeUsd: "—",
cpuInstructions: 0,
memoryBytes: 0,
errorMessage: err instanceof Error ? err.message : String(err),
- }
+ };
}
}
@@ -358,248 +389,275 @@ export async function estimateCreateStreamFee(
input: CreateStreamInput,
sender: string,
): Promise {
- const config = getNetworkConfig(network)
- const isMockMode = !config.streamContractId
-
+ const config = getNetworkConfig(network);
+ const isMockMode = !config.streamContractId;
+
if (isMockMode) {
- return { minFee: 100000, estimatedFee: 115000, estimatedFeeXlm: '0.0115' }
+ return { minFee: 100000, estimatedFee: 115000, estimatedFeeXlm: "0.0115" };
}
const params = xdr.ScVal.scvMap(
[
- ['cliff_amount', nativeToScVal(input.cliffAmount, { type: 'i128' })],
- ['cliff_time', nativeToScVal(input.cliffTime, { type: 'u64' })],
- ['end_time', nativeToScVal(input.endTime, { type: 'u64' })],
- ['recipient', new Address(input.recipient).toScVal()],
- ['start_time', nativeToScVal(input.startTime, { type: 'u64' })],
- ['token', new Address(input.token.address).toScVal()],
- ['total_amount', nativeToScVal(input.totalAmount, { type: 'i128' })],
- ].map(([k, v]) =>
- new xdr.ScMapEntry({
- key: xdr.ScVal.scvSymbol(k as string),
- val: v as xdr.ScVal,
- }),
+ ["cliff_amount", nativeToScVal(input.cliffAmount, { type: "i128" })],
+ ["cliff_time", nativeToScVal(input.cliffTime, { type: "u64" })],
+ ["end_time", nativeToScVal(input.endTime, { type: "u64" })],
+ ["recipient", new Address(input.recipient).toScVal()],
+ ["start_time", nativeToScVal(input.startTime, { type: "u64" })],
+ ["token", new Address(input.token.address).toScVal()],
+ ["total_amount", nativeToScVal(input.totalAmount, { type: "i128" })],
+ ].map(
+ ([k, v]) =>
+ new xdr.ScMapEntry({
+ key: xdr.ScVal.scvSymbol(k as string),
+ val: v as xdr.ScVal,
+ }),
),
- )
+ );
const { minFee, estimatedFee } = await buildAndSimulate(
network,
- 'create_stream',
+ "create_stream",
[new Address(sender).toScVal(), params],
sender,
config.streamContractId,
- )
+ );
return {
minFee,
estimatedFee,
estimatedFeeXlm: (estimatedFee / 1e7).toFixed(4),
- }
+ };
}
export async function createStream(
input: CreateStreamInput,
sender: string,
- network: NetworkName = 'testnet',
+ network: NetworkName = "testnet",
onStep?: (step: TxStep) => void,
): Promise {
- const config = getNetworkConfig(network)
- const isMockMode = !config.streamContractId
-
+ const config = getNetworkConfig(network);
+ const isMockMode = !config.streamContractId;
+
if (isMockMode) {
- await new Promise((r) => setTimeout(r, 700))
- return mockStore.create(input, sender).id
+ await new Promise((r) => setTimeout(r, 700));
+ return mockStore.create(input, sender).id;
}
// Step 1: approve the streaming contract to pull `totalAmount` from the sender.
// The allowance needs to outlast the simulation ledger — set it to current + 500 ledgers.
- const server = getServer(network)
- const currentLedger = (await withRetry(() => server.getLatestLedger())).sequence
- const expirationLedger = currentLedger + 500
+ const server = getServer(network);
+ const currentLedger = (await withRetry(() => server.getLatestLedger()))
+ .sequence;
+ const expirationLedger = currentLedger + 500;
await invoke(
network,
- 'approve',
+ "approve",
[
- new Address(sender).toScVal(), // from
- new Address(config.streamContractId).toScVal(), // spender
- nativeToScVal(input.totalAmount, { type: 'i128' }), // amount
- nativeToScVal(expirationLedger, { type: 'u32' }), // expiration_ledger
+ new Address(sender).toScVal(), // from
+ new Address(config.streamContractId).toScVal(), // spender
+ nativeToScVal(input.totalAmount, { type: "i128" }), // amount
+ nativeToScVal(expirationLedger, { type: "u32" }), // expiration_ledger
],
sender,
input.token.address, // invoke on the token contract, not the streaming contract
onStep,
- )
+ );
// Step 2: create the stream.
const params = xdr.ScVal.scvMap(
[
- ['cliff_amount', nativeToScVal(input.cliffAmount, { type: 'i128' })],
- ['cliff_time', nativeToScVal(input.cliffTime, { type: 'u64' })],
- ['end_time', nativeToScVal(input.endTime, { type: 'u64' })],
- ['recipient', new Address(input.recipient).toScVal()],
- ['start_time', nativeToScVal(input.startTime, { type: 'u64' })],
- ['token', new Address(input.token.address).toScVal()],
- ['total_amount', nativeToScVal(input.totalAmount, { type: 'i128' })],
- ].map(([k, v]) =>
- new xdr.ScMapEntry({
- key: xdr.ScVal.scvSymbol(k as string),
- val: v as xdr.ScVal,
- }),
+ ["cliff_amount", nativeToScVal(input.cliffAmount, { type: "i128" })],
+ ["cliff_time", nativeToScVal(input.cliffTime, { type: "u64" })],
+ ["end_time", nativeToScVal(input.endTime, { type: "u64" })],
+ ["recipient", new Address(input.recipient).toScVal()],
+ ["start_time", nativeToScVal(input.startTime, { type: "u64" })],
+ ["token", new Address(input.token.address).toScVal()],
+ ["total_amount", nativeToScVal(input.totalAmount, { type: "i128" })],
+ ].map(
+ ([k, v]) =>
+ new xdr.ScMapEntry({
+ key: xdr.ScVal.scvSymbol(k as string),
+ val: v as xdr.ScVal,
+ }),
),
- )
+ );
await invoke(
network,
- 'create_stream',
+ "create_stream",
[new Address(sender).toScVal(), params],
sender,
config.streamContractId,
onStep,
- )
+ );
// SDK v13 can't parse TransactionMetaV4 (protocol 22+) so returnValue is void.
// Instead, query the sender's stream list and return the highest ID — that's the new stream.
- const sentResult = await query(network, 'get_sent_streams', [new Address(sender).toScVal(), nativeToScVal(0, { type: 'u32' }), nativeToScVal(1000, { type: 'u32' })], config.streamContractId)
- const ids = scValToNative(sentResult) as bigint[]
- if (!ids || ids.length === 0) throw new Error('Stream created but could not retrieve ID')
- const newId = ids.reduce((a, b) => (a > b ? a : b))
- return String(newId)
+ const sentResult = await query(
+ network,
+ "get_sent_streams",
+ [
+ new Address(sender).toScVal(),
+ nativeToScVal(0, { type: "u32" }),
+ nativeToScVal(1000, { type: "u32" }),
+ ],
+ config.streamContractId,
+ );
+ const ids = scValToNative(sentResult) as bigint[];
+ if (!ids || ids.length === 0)
+ throw new Error("Stream created but could not retrieve ID");
+ const newId = ids.reduce((a, b) => (a > b ? a : b));
+ return String(newId);
}
export async function withdrawFromStream(
id: string,
amount: bigint,
- network: NetworkName = 'testnet',
+ network: NetworkName = "testnet",
onStep?: (step: TxStep) => void,
): Promise {
- const config = getNetworkConfig(network)
- const isMockMode = !config.streamContractId
-
+ const config = getNetworkConfig(network);
+ const isMockMode = !config.streamContractId;
+
if (isMockMode) {
- await new Promise((r) => setTimeout(r, 700))
- mockStore.withdraw(id, amount)
- return null
+ await new Promise((r) => setTimeout(r, 700));
+ mockStore.withdraw(id, amount);
+ return null;
}
- const stream = await fetchStream(network, id)
- if (!stream) throw new Error('Stream not found')
+ const stream = await fetchStream(network, id);
+ if (!stream) throw new Error("Stream not found");
return invoke(
network,
- 'withdraw',
+ "withdraw",
[
- nativeToScVal(BigInt(id), { type: 'u64' }),
- nativeToScVal(amount, { type: 'i128' }),
+ nativeToScVal(BigInt(id), { type: "u64" }),
+ nativeToScVal(amount, { type: "i128" }),
],
stream.recipient,
config.streamContractId,
onStep,
- )
+ );
}
export async function cancelStream(
id: string,
- network: NetworkName = 'testnet',
+ network: NetworkName = "testnet",
onStep?: (step: TxStep) => void,
): Promise {
- const config = getNetworkConfig(network)
- const isMockMode = !config.streamContractId
-
+ const config = getNetworkConfig(network);
+ const isMockMode = !config.streamContractId;
+
if (isMockMode) {
- await new Promise((r) => setTimeout(r, 700))
- mockStore.cancel(id)
- return null
+ await new Promise((r) => setTimeout(r, 700));
+ mockStore.cancel(id);
+ return null;
}
- const stream = await fetchStream(network, id)
- if (!stream) throw new Error('Stream not found')
+ const stream = await fetchStream(network, id);
+ if (!stream) throw new Error("Stream not found");
return invoke(
network,
- 'cancel',
- [nativeToScVal(BigInt(id), { type: 'u64' })],
+ "cancel",
+ [nativeToScVal(BigInt(id), { type: "u64" })],
stream.sender,
config.streamContractId,
onStep,
- )
+ );
}
export async function getTokenMetadata(
tokenAddress: string,
- network: NetworkName = 'testnet',
+ network: NetworkName = "testnet",
): Promise {
try {
- const config = getNetworkConfig(network)
- const server = getServer(network)
- const contract = new Contract(tokenAddress)
+ const config = getNetworkConfig(network);
+ const server = getServer(network);
+ const contract = new Contract(tokenAddress);
const dummyAccount = await withRetry(() =>
- server.getAccount('GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN'),
- )
+ server.getAccount(
+ "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN",
+ ),
+ );
const buildSimTx = (method: string) => {
const tx = new TransactionBuilder(dummyAccount, {
- fee: '100',
+ fee: "100",
networkPassphrase: config.passphrase,
})
.addOperation(contract.call(method))
.setTimeout(10)
- .build()
- return withRetry(() => server.simulateTransaction(tx))
- }
+ .build();
+ return withRetry(() => server.simulateTransaction(tx));
+ };
const [symSim, decSim] = await Promise.all([
- buildSimTx('symbol'),
- buildSimTx('decimals'),
- ])
-
- if (StellarRpc.Api.isSimulationError(symSim) || StellarRpc.Api.isSimulationError(decSim)) {
- return null
+ buildSimTx("symbol"),
+ buildSimTx("decimals"),
+ ]);
+
+ if (
+ StellarRpc.Api.isSimulationError(symSim) ||
+ StellarRpc.Api.isSimulationError(decSim)
+ ) {
+ return null;
}
- const symResult = (symSim as StellarRpc.Api.SimulateTransactionSuccessResponse).result?.retval
- const decResult = (decSim as StellarRpc.Api.SimulateTransactionSuccessResponse).result?.retval
+ const symResult = (
+ symSim as StellarRpc.Api.SimulateTransactionSuccessResponse
+ ).result?.retval;
+ const decResult = (
+ decSim as StellarRpc.Api.SimulateTransactionSuccessResponse
+ ).result?.retval;
- if (!symResult || !decResult) return null
+ if (!symResult || !decResult) return null;
- const symbol = scValToNative(symResult) as string
- const decimals = Number(scValToNative(decResult))
+ const symbol = scValToNative(symResult) as string;
+ const decimals = Number(scValToNative(decResult));
- return { address: tokenAddress, symbol, decimals }
+ return { address: tokenAddress, symbol, decimals };
} catch {
- return null
+ return null;
}
}
export async function getTokenBalance(
tokenAddress: string,
accountAddress: string,
- network: NetworkName = 'testnet',
+ network: NetworkName = "testnet",
): Promise {
- const config = getNetworkConfig(network)
- const isMockMode = !config.streamContractId
-
- if (isMockMode) return BigInt(1_000_000_0000000) // 1,000,000 units mock
+ const config = getNetworkConfig(network);
+ const isMockMode = !config.streamContractId;
+
+ if (isMockMode) return BigInt(1_000_000_0000000); // 1,000,000 units mock
try {
- const server = getServer(network)
- const contract = new Contract(tokenAddress)
+ const server = getServer(network);
+ const contract = new Contract(tokenAddress);
const account = await withRetry(() =>
- server.getAccount('GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN'),
- )
+ server.getAccount(
+ "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN",
+ ),
+ );
const tx = new TransactionBuilder(account, {
- fee: '100',
+ fee: "100",
networkPassphrase: config.passphrase,
})
- .addOperation(contract.call('balance', new Address(accountAddress).toScVal()))
+ .addOperation(
+ contract.call("balance", new Address(accountAddress).toScVal()),
+ )
.setTimeout(10)
- .build()
-
- const sim = await withRetry(() => server.simulateTransaction(tx))
- if (StellarRpc.Api.isSimulationError(sim)) return 0n
- const retval = (sim as StellarRpc.Api.SimulateTransactionSuccessResponse).result?.retval
- if (!retval) return 0n
- return BigInt(scValToNative(retval) as string | number)
+ .build();
+
+ const sim = await withRetry(() => server.simulateTransaction(tx));
+ if (StellarRpc.Api.isSimulationError(sim)) return 0n;
+ const retval = (sim as StellarRpc.Api.SimulateTransactionSuccessResponse)
+ .result?.retval;
+ if (!retval) return 0n;
+ return BigInt(scValToNative(retval) as string | number);
} catch {
- return 0n
+ return 0n;
}
}
@@ -608,36 +666,39 @@ export async function bumpStreamTtl(
id: string,
signerAddress: string,
): Promise {
- const config = getNetworkConfig(network)
- const isMockMode = !config.streamContractId
-
- if (isMockMode) return
+ const config = getNetworkConfig(network);
+ const isMockMode = !config.streamContractId;
+
+ if (isMockMode) return;
await invoke(
network,
- 'bump_stream',
- [nativeToScVal(BigInt(id), { type: 'u64' })],
+ "bump_stream",
+ [nativeToScVal(BigInt(id), { type: "u64" })],
signerAddress,
config.streamContractId,
- )
+ );
}
export async function fetchStream(
network: NetworkName,
id: string,
): Promise {
- const config = getNetworkConfig(network)
- const isMockMode = !config.streamContractId
-
- if (isMockMode) return mockStore.getById(id) ?? null
+ const config = getNetworkConfig(network);
+ const isMockMode = !config.streamContractId;
+
+ if (isMockMode) return mockStore.getById(id) ?? null;
try {
- const result = await query(network, 'get_stream', [
- nativeToScVal(BigInt(id), { type: 'u64' }),
- ], config.streamContractId)
- return scValToStreamData(network, result)
+ const result = await query(
+ network,
+ "get_stream",
+ [nativeToScVal(BigInt(id), { type: "u64" })],
+ config.streamContractId,
+ );
+ return scValToStreamData(network, result);
} catch {
- return null
+ return null;
}
}
@@ -645,27 +706,47 @@ export async function fetchStreamsForAddress(
network: NetworkName,
address: string,
): Promise {
- const config = getNetworkConfig(network)
- const isMockMode = !config.streamContractId
-
+ const config = getNetworkConfig(network);
+ const isMockMode = !config.streamContractId;
+
if (isMockMode) {
return mockStore
.getAll()
- .filter((s) => s.sender === address || s.recipient === address)
+ .filter((s) => s.sender === address || s.recipient === address);
}
const [sentIds, receivedIds] = await Promise.all([
- query(network, 'get_sent_streams', [new Address(address).toScVal(), nativeToScVal(0, { type: 'u32' }), nativeToScVal(1000, { type: 'u32' })], config.streamContractId),
- query(network, 'get_received_streams', [new Address(address).toScVal(), nativeToScVal(0, { type: 'u32' }), nativeToScVal(1000, { type: 'u32' })], config.streamContractId),
- ])
+ query(
+ network,
+ "get_sent_streams",
+ [
+ new Address(address).toScVal(),
+ nativeToScVal(0, { type: "u32" }),
+ nativeToScVal(1000, { type: "u32" }),
+ ],
+ config.streamContractId,
+ ),
+ query(
+ network,
+ "get_received_streams",
+ [
+ new Address(address).toScVal(),
+ nativeToScVal(0, { type: "u32" }),
+ nativeToScVal(1000, { type: "u32" }),
+ ],
+ config.streamContractId,
+ ),
+ ]);
const allIds = [
...(scValToNative(sentIds) as bigint[]),
...(scValToNative(receivedIds) as bigint[]),
- ]
+ ];
// Deduplicate (self-streams appear in both)
- const unique = [...new Set(allIds.map(String))]
+ const unique = [...new Set(allIds.map(String))];
- const streams = await Promise.all(unique.map((id) => fetchStream(network, id)))
- return streams.filter((s): s is StreamData => s !== null)
+ const streams = await Promise.all(
+ unique.map((id) => fetchStream(network, id)),
+ );
+ return streams.filter((s): s is StreamData => s !== null);
}
diff --git a/lib/stream-utils.ts b/lib/stream-utils.ts
index 375c55b..5b3cda2 100644
--- a/lib/stream-utils.ts
+++ b/lib/stream-utils.ts
@@ -1,4 +1,4 @@
-import type { StreamData, StreamStatus } from '@/types/stream'
+import type { StreamData, StreamStatus } from "@/types/stream";
/**
* Computes how much of the stream has unlocked as of `nowSeconds`.
@@ -10,16 +10,16 @@ export function getUnlockedAmount(
stream: StreamData,
nowSeconds: number = Math.floor(Date.now() / 1000),
): bigint {
- const now = BigInt(nowSeconds)
- if (now < stream.cliffTime) return 0n
- if (now >= stream.endTime) return stream.depositedAmount
+ const now = BigInt(nowSeconds);
+ if (now < stream.cliffTime) return 0n;
+ if (now >= stream.endTime) return stream.depositedAmount;
- const elapsed = now - stream.startTime
- const linear = elapsed > 0n ? elapsed * stream.amountPerSecond : 0n
- const unlocked = stream.cliffAmount + linear
+ const elapsed = now - stream.startTime;
+ const linear = elapsed > 0n ? elapsed * stream.amountPerSecond : 0n;
+ const unlocked = stream.cliffAmount + linear;
// Never report more than was deposited.
- return unlocked > stream.depositedAmount ? stream.depositedAmount : unlocked
+ return unlocked > stream.depositedAmount ? stream.depositedAmount : unlocked;
}
/** Amount currently available for the recipient to withdraw. */
@@ -27,36 +27,46 @@ export function getWithdrawableAmount(
stream: StreamData,
nowSeconds?: number,
): bigint {
- const unlocked = getUnlockedAmount(stream, nowSeconds)
- const available = unlocked - stream.withdrawnAmount
- return available > 0n ? available : 0n
+ const unlocked = getUnlockedAmount(stream, nowSeconds);
+ const available = unlocked - stream.withdrawnAmount;
+ return available > 0n ? available : 0n;
}
/** Amount still locked in the stream. */
-export function getLockedAmount(stream: StreamData, nowSeconds?: number): bigint {
- return stream.depositedAmount - getUnlockedAmount(stream, nowSeconds)
+export function getLockedAmount(
+ stream: StreamData,
+ nowSeconds?: number,
+): bigint {
+ return stream.depositedAmount - getUnlockedAmount(stream, nowSeconds);
}
export function getStreamStatus(
stream: StreamData,
nowSeconds: number = Math.floor(Date.now() / 1000),
): StreamStatus {
- if (stream.cancelled) return 'cancelled'
- const now = BigInt(nowSeconds)
- if (now < stream.startTime) return 'scheduled'
- if (now >= stream.endTime) return 'completed'
- return 'streaming'
+ if (stream.cancelled) return "cancelled";
+ const now = BigInt(nowSeconds);
+ if (now < stream.startTime) return "scheduled";
+ if (now >= stream.endTime) return "completed";
+ return "streaming";
}
/** Progress (0–1) of unlocked vs deposited. */
-export function getStreamProgress(stream: StreamData, nowSeconds?: number): number {
- if (stream.depositedAmount === 0n) return 0
- const unlocked = getUnlockedAmount(stream, nowSeconds)
- return clamp(Number((unlocked * 10000n) / stream.depositedAmount) / 10000, 0, 1)
+export function getStreamProgress(
+ stream: StreamData,
+ nowSeconds?: number,
+): number {
+ if (stream.depositedAmount === 0n) return 0;
+ const unlocked = getUnlockedAmount(stream, nowSeconds);
+ return clamp(
+ Number((unlocked * 10000n) / stream.depositedAmount) / 10000,
+ 0,
+ 1,
+ );
}
function clamp(n: number, min: number, max: number) {
- return Math.min(Math.max(n, min), max)
+ return Math.min(Math.max(n, min), max);
}
/**
@@ -69,41 +79,57 @@ export function formatTokenAmount(
decimals: number,
maxFractionDigits = 4,
): string {
- const negative = raw < 0n
- const abs = negative ? -raw : raw
- const base = 10n ** BigInt(decimals)
- const whole = abs / base
- const frac = abs % base
+ const negative = raw < 0n;
+ const abs = negative ? -raw : raw;
+ const base = 10n ** BigInt(decimals);
+ const whole = abs / base;
+ const frac = abs % base;
- const wholeStr = whole.toLocaleString('en-US')
+ const wholeStr = whole.toLocaleString("en-US");
if (decimals === 0 || maxFractionDigits === 0) {
- return `${negative ? '-' : ''}${wholeStr}`
+ return `${negative ? "-" : ""}${wholeStr}`;
}
- let fracStr = frac.toString().padStart(decimals, '0').slice(0, maxFractionDigits)
- fracStr = fracStr.replace(/0+$/, '')
+ let fracStr = frac
+ .toString()
+ .padStart(decimals, "0")
+ .slice(0, maxFractionDigits);
+ fracStr = fracStr.replace(/0+$/, "");
- return `${negative ? '-' : ''}${wholeStr}${fracStr ? '.' + fracStr : ''}`
+ return `${negative ? "-" : ""}${wholeStr}${fracStr ? "." + fracStr : ""}`;
+}
+
+/**
+ * Formats a raw bigint amount into a compact human-readable string (e.g. "1.2K", "3.4M").
+ */
+export function formatCompactAmount(raw: bigint, decimals: number): string {
+ const value = Number(raw) / Number(10n ** BigInt(decimals));
+ return new Intl.NumberFormat("en-US", {
+ notation: "compact",
+ maximumFractionDigits: 1,
+ }).format(value);
}
/** Parses a human-typed decimal string into a raw bigint of smallest units. */
export function parseTokenAmount(value: string, decimals: number): bigint {
- if (!value) return 0n
- const [whole, frac = ''] = value.replace(/,/g, '').split('.')
- const fracPadded = frac.slice(0, decimals).padEnd(decimals, '0')
- return BigInt(whole || '0') * 10n ** BigInt(decimals) + BigInt(fracPadded || '0')
+ if (!value) return 0n;
+ const [whole, frac = ""] = value.replace(/,/g, "").split(".");
+ const fracPadded = frac.slice(0, decimals).padEnd(decimals, "0");
+ return (
+ BigInt(whole || "0") * 10n ** BigInt(decimals) + BigInt(fracPadded || "0")
+ );
}
export interface FormattedRate {
- perSecond: string
- perMinute: string
- perHour: string
- perDay: string
- perMonth: string
- perYear: string
- best: string
- bestUnit: string
+ perSecond: string;
+ perMinute: string;
+ perHour: string;
+ perDay: string;
+ perMonth: string;
+ perYear: string;
+ best: string;
+ bestUnit: string;
}
export function formatRate(
@@ -111,7 +137,7 @@ export function formatRate(
decimals: number,
symbol: string,
): FormattedRate {
- const perSecond = Number(amountPerSecond) / 10 ** decimals
+ const perSecond = Number(amountPerSecond) / 10 ** decimals;
const rates = {
perSecond,
@@ -120,28 +146,28 @@ export function formatRate(
perDay: perSecond * 86400,
perMonth: perSecond * 2_592_000,
perYear: perSecond * 31_536_000,
- }
+ };
const fmt = (n: number) =>
n >= 1
- ? n.toLocaleString('en-US', { maximumFractionDigits: 2 })
- : n.toPrecision(4).replace(/\.?0+$/, '')
+ ? n.toLocaleString("en-US", { maximumFractionDigits: 2 })
+ : n.toPrecision(4).replace(/\.?0+$/, "");
const units: { key: keyof typeof rates; label: string }[] = [
- { key: 'perMinute', label: '/min' },
- { key: 'perHour', label: '/hr' },
- { key: 'perDay', label: '/day' },
- { key: 'perMonth', label: '/mo' },
- { key: 'perYear', label: '/yr' },
- ]
-
- let bestUnit = '/day'
- let bestValue = rates.perDay
+ { key: "perMinute", label: "/min" },
+ { key: "perHour", label: "/hr" },
+ { key: "perDay", label: "/day" },
+ { key: "perMonth", label: "/mo" },
+ { key: "perYear", label: "/yr" },
+ ];
+
+ let bestUnit = "/day";
+ let bestValue = rates.perDay;
for (const u of units) {
if (rates[u.key] >= 0.01) {
- bestUnit = u.label
- bestValue = rates[u.key]
- break
+ bestUnit = u.label;
+ bestValue = rates[u.key];
+ break;
}
}
@@ -154,23 +180,23 @@ export function formatRate(
perYear: `${fmt(rates.perYear)} ${symbol}/yr`,
best: `${fmt(bestValue)} ${symbol}${bestUnit}`,
bestUnit,
- }
+ };
}
export function shortenAddress(address: string, chars = 4): string {
- if (address.length <= chars * 2 + 2) return address
- return `${address.slice(0, chars + 1)}…${address.slice(-chars)}`
+ if (address.length <= chars * 2 + 2) return address;
+ return `${address.slice(0, chars + 1)}…${address.slice(-chars)}`;
}
export function formatDateTime(unixSeconds: bigint | number): string {
- const ms = Number(unixSeconds) * 1000
- return new Date(ms).toLocaleString('en-US', {
- month: 'short',
- day: 'numeric',
- year: 'numeric',
- hour: 'numeric',
- minute: '2-digit',
- })
+ const ms = Number(unixSeconds) * 1000;
+ return new Date(ms).toLocaleString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ hour: "numeric",
+ minute: "2-digit",
+ });
}
/** Returns a compact "2d 4h 13m" style duration from now until `target`. */
@@ -178,20 +204,20 @@ export function formatTimeRemaining(
targetSeconds: bigint,
nowSeconds: number = Math.floor(Date.now() / 1000),
): string {
- let diff = Number(targetSeconds) - nowSeconds
- if (diff <= 0) return 'Ended'
-
- const days = Math.floor(diff / 86400)
- diff -= days * 86400
- const hours = Math.floor(diff / 3600)
- diff -= hours * 3600
- const minutes = Math.floor(diff / 60)
- const seconds = diff - minutes * 60
-
- const parts: string[] = []
- if (days) parts.push(`${days}d`)
- if (hours || days) parts.push(`${hours}h`)
- if (!days) parts.push(`${minutes}m`)
- if (!days && !hours) parts.push(`${seconds}s`)
- return parts.join(' ')
+ let diff = Number(targetSeconds) - nowSeconds;
+ if (diff <= 0) return "Ended";
+
+ const days = Math.floor(diff / 86400);
+ diff -= days * 86400;
+ const hours = Math.floor(diff / 3600);
+ diff -= hours * 3600;
+ const minutes = Math.floor(diff / 60);
+ const seconds = diff - minutes * 60;
+
+ const parts: string[] = [];
+ if (days) parts.push(`${days}d`);
+ if (hours || days) parts.push(`${hours}h`);
+ if (!days) parts.push(`${minutes}m`);
+ if (!days && !hours) parts.push(`${seconds}s`);
+ return parts.join(" ");
}
diff --git a/next.config.mjs b/next.config.mjs
index 155877e..dd14406 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -1,11 +1,7 @@
import { withSentryConfig } from '@sentry/nextjs'
/** @type {import('next').NextConfig} */
-const nextConfig = {
- typescript: {
- ignoreBuildErrors: true,
- },
-}
+const nextConfig = {}
// Bundle analyzer — run with: ANALYZE=true npm run build
async function withBundleAnalyzer(config) {
diff --git a/package-lock.json b/package-lock.json
index 7d56e19..6247e2c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -46,7 +46,7 @@
"@vitejs/plugin-react": "^6.0.3",
"@vitest/coverage-v8": "^2.1.9",
"eslint": "^10.5.0",
- "eslint-plugin-security": "^3.0.1",
+ "eslint-plugin-security": "^4.0.1",
"globals": "^17.7.0",
"husky": "^9.0.0",
"jsdom": "^25.0.1",
@@ -58,10 +58,6 @@
"vitest": "^2.1.9"
}
},
- "node_modules/@adraffy/ens-normalize": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz",
- "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==",
"node_modules/@adobe/css-tools": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz",
@@ -69,6 +65,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@adraffy/ens-normalize": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz",
+ "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==",
+ "license": "MIT"
+ },
"node_modules/@albedo-link/intent": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@albedo-link/intent/-/intent-0.13.0.tgz",
@@ -998,15 +1000,22 @@
"license": "BSD-3-Clause"
},
"node_modules/@emnapi/runtime": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
- "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
+ "version": "1.11.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz",
+ "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
+ "node_modules/@emnapi/runtime/node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD",
+ "optional": true
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -2414,6 +2423,12 @@
"tslib": "^2.3.1"
}
},
+ "node_modules/@motionone/animation/node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/@motionone/dom": {
"version": "10.18.0",
"resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.18.0.tgz",
@@ -2428,6 +2443,12 @@
"tslib": "^2.3.1"
}
},
+ "node_modules/@motionone/dom/node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/@motionone/easing": {
"version": "10.18.0",
"resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.18.0.tgz",
@@ -2438,6 +2459,12 @@
"tslib": "^2.3.1"
}
},
+ "node_modules/@motionone/easing/node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/@motionone/generators": {
"version": "10.18.0",
"resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.18.0.tgz",
@@ -2449,6 +2476,12 @@
"tslib": "^2.3.1"
}
},
+ "node_modules/@motionone/generators/node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/@motionone/svelte": {
"version": "10.16.4",
"resolved": "https://registry.npmjs.org/@motionone/svelte/-/svelte-10.16.4.tgz",
@@ -2459,6 +2492,12 @@
"tslib": "^2.3.1"
}
},
+ "node_modules/@motionone/svelte/node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/@motionone/types": {
"version": "10.17.1",
"resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.17.1.tgz",
@@ -2476,6 +2515,12 @@
"tslib": "^2.3.1"
}
},
+ "node_modules/@motionone/utils/node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/@motionone/vue": {
"version": "10.16.4",
"resolved": "https://registry.npmjs.org/@motionone/vue/-/vue-10.16.4.tgz",
@@ -2487,6 +2532,12 @@
"tslib": "^2.3.1"
}
},
+ "node_modules/@motionone/vue/node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/@msgpack/msgpack": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz",
@@ -2497,9 +2548,9 @@
}
},
"node_modules/@next/bundle-analyzer": {
- "version": "15.5.19",
- "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-15.5.19.tgz",
- "integrity": "sha512-A+hYYxRIo5ykAkw7+pDTQA9dBp+yXmz70/jdTSm87uVP14oHQmV0lC1jXe212O6+v06WB8fwZFEgz1A+uPeHrA==",
+ "version": "15.5.20",
+ "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-15.5.20.tgz",
+ "integrity": "sha512-OyB5HvHVRz2KdD9k7Al/FHDkkjqctljKhyDtasRjbsL6MnpLxP8b2Izk01bv4ubcgurU17WRJqbUdd+HA+PttA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2513,9 +2564,9 @@
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
- "version": "16.2.9",
- "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.9.tgz",
- "integrity": "sha512-UZi8+YT/MLgTC9nrrn2Xd4lBYv1B7lVmtWHfPcthAI5Tt/C1LuDe6DfmtCtJ+WQod3ksY4VrKSvk3oMVAnL7qw==",
+ "version": "16.2.10",
+ "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.10.tgz",
+ "integrity": "sha512-Gs8D2m21VnJeFo9qvYIIqJH94frWerWYu41BprU1pLtRVF7PCQNLiFZZ3fG+iPuj3K83Cwv/rt+msLOy8Qgu3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3312,9 +3363,9 @@
}
},
"node_modules/@opentelemetry/semantic-conventions": {
- "version": "1.41.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz",
- "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==",
+ "version": "1.42.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.42.0.tgz",
+ "integrity": "sha512-icc5xCzndZfhuJMy5oqk5AvloWquR7jtae74qzpkKkhGp8BivK+oCcEXgGnjCdTfp8hA44l+w8gE8yYJbocJJw==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
@@ -4412,6 +4463,12 @@
"tslib": "^2.8.0"
}
},
+ "node_modules/@swc/helpers/node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/@tailwindcss/node": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz",
@@ -4893,9 +4950,9 @@
}
},
"node_modules/@types/node": {
- "version": "24.13.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
- "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
+ "version": "24.13.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
+ "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
@@ -5241,12 +5298,6 @@
"tslib": "1.14.1"
}
},
- "node_modules/@walletconnect/environment/node_modules/tslib": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
- "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
- "license": "0BSD"
- },
"node_modules/@walletconnect/events": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@walletconnect/events/-/events-1.0.1.tgz",
@@ -5257,12 +5308,6 @@
"tslib": "1.14.1"
}
},
- "node_modules/@walletconnect/events/node_modules/tslib": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
- "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
- "license": "0BSD"
- },
"node_modules/@walletconnect/heartbeat": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@walletconnect/heartbeat/-/heartbeat-1.2.2.tgz",
@@ -5306,12 +5351,6 @@
"tslib": "1.14.1"
}
},
- "node_modules/@walletconnect/jsonrpc-utils/node_modules/tslib": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
- "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
- "license": "0BSD"
- },
"node_modules/@walletconnect/jsonrpc-ws-connection": {
"version": "1.0.16",
"resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.16.tgz",
@@ -5416,12 +5455,6 @@
"tslib": "1.14.1"
}
},
- "node_modules/@walletconnect/safe-json/node_modules/tslib": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
- "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
- "license": "0BSD"
- },
"node_modules/@walletconnect/sign-client": {
"version": "2.23.10",
"resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.23.10.tgz",
@@ -5446,12 +5479,6 @@
"tslib": "1.14.1"
}
},
- "node_modules/@walletconnect/time/node_modules/tslib": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
- "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
- "license": "0BSD"
- },
"node_modules/@walletconnect/types": {
"version": "2.23.10",
"resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.23.10.tgz",
@@ -5529,12 +5556,6 @@
"tslib": "1.14.1"
}
},
- "node_modules/@walletconnect/window-getters/node_modules/tslib": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
- "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
- "license": "0BSD"
- },
"node_modules/@walletconnect/window-metadata": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@walletconnect/window-metadata/-/window-metadata-1.0.1.tgz",
@@ -5545,12 +5566,6 @@
"tslib": "1.14.1"
}
},
- "node_modules/@walletconnect/window-metadata/node_modules/tslib": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
- "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
- "license": "0BSD"
- },
"node_modules/abitype": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.4.tgz",
@@ -5834,6 +5849,12 @@
"node": ">=4"
}
},
+ "node_modules/ast-types/node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -5974,9 +5995,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.10.40",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz",
- "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==",
+ "version": "2.10.42",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz",
+ "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.cjs"
@@ -6050,9 +6071,9 @@
}
},
"node_modules/brace-expansion": {
- "version": "1.1.15",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
- "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6073,9 +6094,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.28.4",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz",
- "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==",
+ "version": "4.28.5",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz",
+ "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==",
"funding": [
{
"type": "opencollective",
@@ -6092,10 +6113,10 @@
],
"license": "MIT",
"dependencies": {
- "baseline-browser-mapping": "^2.10.38",
- "caniuse-lite": "^1.0.30001799",
- "electron-to-chromium": "^1.5.376",
- "node-releases": "^2.0.48",
+ "baseline-browser-mapping": "^2.10.42",
+ "caniuse-lite": "^1.0.30001800",
+ "electron-to-chromium": "^1.5.387",
+ "node-releases": "^2.0.50",
"update-browserslist-db": "^1.2.3"
},
"bin": {
@@ -6238,9 +6259,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001799",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
- "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
+ "version": "1.0.30001803",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz",
+ "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==",
"funding": [
{
"type": "opencollective",
@@ -6412,15 +6433,6 @@
"wrap-ansi": "^6.2.0"
}
},
- "node_modules/cliui/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/cliui/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -6920,6 +6932,7 @@
"engines": {
"node": ">=0.10.0"
}
+ },
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
@@ -7048,6 +7061,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/destr": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
@@ -7059,15 +7082,6 @@
"resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-5.3.0.tgz",
"integrity": "sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==",
"license": "MIT"
- "node_modules/dequal": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
- "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
},
"node_modules/detect-libc": {
"version": "2.1.2",
@@ -7092,6 +7106,8 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
+ "license": "MIT"
+ },
"node_modules/dom-accessibility-api": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
@@ -7161,9 +7177,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.381",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.381.tgz",
- "integrity": "sha512-n9Wa6yB+vDsGuA8AKbl/0z7HbvWqt5jxIdvr1IUicd0ryPrk7/xzwqLv8D9AbbvZ6avVNtXYLTfmgFHkwkyelg==",
+ "version": "1.5.389",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz",
+ "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==",
"license": "ISC"
},
"node_modules/emoji-regex": {
@@ -7468,9 +7484,9 @@
}
},
"node_modules/eslint-plugin-security": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-3.0.1.tgz",
- "integrity": "sha512-XjVGBhtDZJfyuhIxnQ/WMm385RbX3DBu7H1J7HNNhmB2tnGxMeqVSnYv79oAj992ayvIBZghsymwkYFS6cGH4Q==",
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-4.0.1.tgz",
+ "integrity": "sha512-/lZCkOxPOWaf1jXAqgICrS8St3BMBccIPvhOSUYuV6VCr1o5nFVG998FnTLt6w2Nxb8Uo0nM8fzmnhp+GY/aEg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -8149,9 +8165,9 @@
"license": "ISC"
},
"node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -8302,9 +8318,9 @@
}
},
"node_modules/glob/node_modules/brace-expansion": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
- "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
+ "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -8477,9 +8493,9 @@
}
},
"node_modules/hono": {
- "version": "4.12.27",
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz",
- "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==",
+ "version": "4.12.28",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.28.tgz",
+ "integrity": "sha512-YwUvVpSF7m1yOblFPrU3Hbo8XhPheBoiyfGuII6z19LnOr6JpDnyyp7LFNrfV56wS8tpvtBFGRISHN02pDdLOA==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
@@ -8589,9 +8605,9 @@
}
},
"node_modules/iconv-lite": {
- "version": "0.7.2",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
- "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
+ "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
@@ -9191,6 +9207,28 @@
"node": ">= 14"
}
},
+ "node_modules/jsdom/node_modules/ws": {
+ "version": "8.21.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
+ "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -9826,9 +9864,9 @@
}
},
"node_modules/lucide-react": {
- "version": "1.22.0",
- "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.22.0.tgz",
- "integrity": "sha512-c9o3l0PiNcgOQDW4F31BEYHudE7kgxVt3o30qMl36ZPwTxXlGB4QnLilhERvVM4uh/pl5MDyY1/gzZSYcHDtBg==",
+ "version": "1.23.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.23.0.tgz",
+ "integrity": "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -10231,11 +10269,6 @@
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
"license": "MIT"
},
- "node_modules/node-mock-http": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz",
- "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==",
- "license": "MIT"
"node_modules/node-fetch/node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@@ -10258,6 +10291,12 @@
"webidl-conversions": "^3.0.0"
}
},
+ "node_modules/node-mock-http": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz",
+ "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==",
+ "license": "MIT"
+ },
"node_modules/node-releases": {
"version": "2.0.50",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz",
@@ -10797,9 +10836,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
- "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"license": "MIT",
"engines": {
"node": ">=12"
@@ -10979,19 +11018,6 @@
"license": "MIT",
"engines": {
"node": ">=10.13.0"
- "node_modules/playwright/node_modules/fsevents": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
- "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/possible-typed-array-names": {
@@ -11438,6 +11464,12 @@
"node": ">= 4"
}
},
+ "node_modules/recast/node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
@@ -11885,9 +11917,9 @@
}
},
"node_modules/shadcn": {
- "version": "4.12.0",
- "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-4.12.0.tgz",
- "integrity": "sha512-o781ieQziCnXH2FKsEqxp1fnbHdbgAPO9inTSPeZ59hQfsZXuMGp3ul8oFSV5KQS4nbUK9b+DrDE6C7OvfKKQQ==",
+ "version": "4.13.0",
+ "resolved": "https://registry.npmjs.org/shadcn/-/shadcn-4.13.0.tgz",
+ "integrity": "sha512-5fuJ4jI/GcPeA/iTL4cJivCZuYQGXz/N3bIzyd+Gd/FM6xUCy2MxGG+LaDQuw2cjNy9zGPSFPTEmI048UwPTZA==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.28.0",
@@ -12645,9 +12677,9 @@
"license": "MIT"
},
"node_modules/systeminformation": {
- "version": "5.31.11",
- "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.11.tgz",
- "integrity": "sha512-I6O7iaUj23AXRgCPDDnvi3xHvdOLp4+1YMbF+X194lJwY1NeWojgHJPhslVKcmTtrLTguRk3QJK+xEdTiI3P0w==",
+ "version": "5.31.14",
+ "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.14.tgz",
+ "integrity": "sha512-nefRpMCsAI4m71/6JHH//KPaP/d5nTuRVxEtQ7N7SlBrX18DAcC+5Z1JKZYeN9Iw49qMx95BTo/gBMk3Y2H6+g==",
"license": "MIT",
"os": [
"darwin",
@@ -12701,13 +12733,6 @@
"url": "https://opencollective.com/webpack"
}
},
- "node_modules/thread-stream": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz",
- "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==",
- "license": "MIT",
- "dependencies": {
- "real-require": "^0.2.0"
"node_modules/test-exclude": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz",
@@ -12776,9 +12801,9 @@
"license": "MIT"
},
"node_modules/test-exclude/node_modules/glob/node_modules/brace-expansion": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
- "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
+ "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -12827,6 +12852,15 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/thread-stream": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz",
+ "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==",
+ "license": "MIT",
+ "dependencies": {
+ "real-require": "^0.2.0"
+ }
+ },
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
@@ -13005,9 +13039,9 @@
}
},
"node_modules/tslib": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"license": "0BSD"
},
"node_modules/tw-animate-css": {
@@ -13331,9 +13365,9 @@
}
},
"node_modules/unstorage/node_modules/lru-cache": {
- "version": "11.5.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
- "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
+ "version": "11.5.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
+ "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
@@ -13562,6 +13596,21 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/vite/node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/vite/node_modules/rollup": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
@@ -13734,32 +13783,10 @@
"node": ">= 10"
}
},
- "node_modules/webpack-bundle-analyzer/node_modules/ws": {
- "version": "7.5.11",
- "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz",
- "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8.3.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": "^5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
- }
- },
"node_modules/webpack-sources": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz",
- "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==",
+ "version": "3.5.1",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz",
+ "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
@@ -13996,17 +14023,13 @@
"version": "7.5.11",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz",
"integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==",
- "version": "8.21.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
- "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
- "dev": true,
"license": "MIT",
"engines": {
- "node": ">=10.0.0"
+ "node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
- "utf-8-validate": ">=5.0.2"
+ "utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
@@ -14122,15 +14145,6 @@
"node": ">=6"
}
},
- "node_modules/yargs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/yargs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -14237,9 +14251,9 @@
}
},
"node_modules/yocto-spinner": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-1.2.0.tgz",
- "integrity": "sha512-Yw0hUB6UA3o4YUgKy3oSe9a4cxoaZ9sBfYDw+JSxo6Id0KoJGoxzPA24qqUXYKBWABs/zDSGTz9kww7t3F0XGw==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-1.2.1.tgz",
+ "integrity": "sha512-9cbFWLhbiZp+820O4pkHGNncI7+MrUGzBOjw8NMG+ewsY+aG0DdEXnr19Smxao32YOjLZRMdn1UtaxcrXOYOIg==",
"license": "MIT",
"dependencies": {
"yoctocolors": "^2.1.1"
diff --git a/package.json b/package.json
index 1cf3d7b..9ab4394 100644
--- a/package.json
+++ b/package.json
@@ -3,8 +3,8 @@
"version": "0.1.0",
"private": true,
"scripts": {
- "dev": "next dev",
- "build": "next build",
+ "dev": "next dev --webpack",
+ "build": "next build --webpack",
"start": "next start",
"lint": "eslint .",
"test": "vitest run",
@@ -13,18 +13,18 @@
"test:e2e": "playwright test",
"test:e2e:update": "playwright test -u",
"prepare": "husky",
- "analyze": "ANALYZE=true next build"
+ "analyze": "ANALYZE=true next build --webpack"
},
"dependencies": {
"@albedo-link/intent": "^0.13.0",
- "@walletconnect/modal": "^2.6.2",
- "@walletconnect/sign-client": "^2.17.0",
"@base-ui/react": "^1.5.0",
"@sentry/nextjs": "^8.0.0",
"@stellar/freighter-api": "^6.0.1",
"@stellar/stellar-sdk": "^13.3.0",
"@vercel/analytics": "1.6.1",
"@vercel/og": "^0.11.1",
+ "@walletconnect/modal": "^2.6.2",
+ "@walletconnect/sign-client": "^2.17.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.16.0",
@@ -54,13 +54,13 @@
"@vitejs/plugin-react": "^6.0.3",
"@vitest/coverage-v8": "^2.1.9",
"eslint": "^10.5.0",
- "eslint-plugin-security": "^3.0.1",
+ "eslint-plugin-security": "^4.0.1",
"globals": "^17.7.0",
"husky": "^9.0.0",
"jsdom": "^25.0.1",
"lint-staged": "^15.0.0",
- "prettier": "^3.0.0",
"postcss": "^8.5",
+ "prettier": "^3.0.0",
"tailwindcss": "^4.2.0",
"typescript": "5.7.3",
"vitest": "^2.1.9"
diff --git a/scripts/soroban-security-check.mjs b/scripts/soroban-security-check.mjs
index 337ff25..415035c 100644
--- a/scripts/soroban-security-check.mjs
+++ b/scripts/soroban-security-check.mjs
@@ -57,9 +57,9 @@ const rules = [
check(src) {
const findings = []
// Collect all pub fn bodies; check that require_auth appears before any storage write.
- const fnRegex = /pub fn (\w+)\s*\(env:\s*Env[^)]*\)/g
+ const fnRegex = /pub fn (\w+)\s*\(_?env:\s*Env[^)]*\)/g
// Read-only patterns — skip them
- const readOnly = /^(get_|bump_stream|load_stream)/
+ const readOnly = /^(get_|bump_stream$|load_stream$|name$|version$)/
let match
while ((match = fnRegex.exec(src)) !== null) {
const name = match[1]
diff --git a/sentry.client.config.ts b/sentry.client.config.ts
index 220da92..f6c2127 100644
--- a/sentry.client.config.ts
+++ b/sentry.client.config.ts
@@ -1,10 +1,10 @@
-import * as Sentry from '@sentry/nextjs'
+import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// Only active in production — avoids noise during development.
- enabled: process.env.NODE_ENV === 'production',
+ enabled: process.env.NODE_ENV === "production",
// Capture 10% of transactions for performance monitoring.
tracesSampleRate: 0.1,
@@ -13,17 +13,20 @@ Sentry.init({
beforeSend(event) {
if (event.request?.data) {
// Never log raw transaction payloads.
- event.request.data = '[REDACTED]'
+ event.request.data = "[REDACTED]";
}
// Truncate any full Stellar addresses in breadcrumbs to the first 8 chars.
- if (event.breadcrumbs?.values) {
- event.breadcrumbs.values = event.breadcrumbs.values.map((b) => {
- if (typeof b.message === 'string') {
- b.message = b.message.replace(/\b(G[A-Z2-7]{55})\b/g, (addr) => `${addr.slice(0, 8)}…`)
+ if (event.breadcrumbs) {
+ event.breadcrumbs = event.breadcrumbs.map((b) => {
+ if (typeof b.message === "string") {
+ b.message = b.message.replace(
+ /\b(G[A-Z2-7]{55})\b/g,
+ (addr) => `${addr.slice(0, 8)}…`,
+ );
}
- return b
- })
+ return b;
+ });
}
- return event
+ return event;
},
-})
+});
diff --git a/src/lib/network.ts b/src/lib/network.ts
deleted file mode 100644
index 35f12c1..0000000
--- a/src/lib/network.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-export type StellarNetwork = 'testnet' | 'mainnet' | 'futurenet';
-
-export const APP_NETWORK =
- (process.env.NEXT_PUBLIC_STELLAR_NETWORK as StellarNetwork) ?? 'testnet';
-
-const NETWORK_NAMES: Record = {
- testnet: 'Testnet',
- mainnet: 'Mainnet',
- futurenet: 'Futurenet',
-};
-
-export function getNetworkName(network: StellarNetwork): string {
- return NETWORK_NAMES[network] ?? network;
-}
-
-export function isNetworkMatch(walletNetwork: string): boolean {
- return walletNetwork.toLowerCase().includes(APP_NETWORK.toLowerCase());
-}
\ No newline at end of file