diff --git a/frontend/src/components/landing/cta-section.tsx b/frontend/src/components/landing/cta-section.tsx index 5157a20..3ea7273 100644 --- a/frontend/src/components/landing/cta-section.tsx +++ b/frontend/src/components/landing/cta-section.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import { motion } from "framer-motion"; import { Button } from "@/components/ui/button"; +import { NETWORK_LABEL } from "@/lib/constants"; import { ArrowRight } from "lucide-react"; export function CTASection() { @@ -20,7 +21,7 @@ export function CTASection() {

Set up your first payment stream and start sending funds in real time - on Stacks testnet. + on Stacks {NETWORK_LABEL.toLowerCase()}.

diff --git a/frontend/src/components/landing/footer.tsx b/frontend/src/components/landing/footer.tsx index c24f39b..bd81876 100644 --- a/frontend/src/components/landing/footer.tsx +++ b/frontend/src/components/landing/footer.tsx @@ -1,6 +1,6 @@ import Link from "next/link"; import { Twitter, Github, Linkedin, Zap, ExternalLink } from "lucide-react"; -import { FEEDBACK_URL } from "@/lib/constants"; +import { FEEDBACK_URL, EXPLORER_BASE, NETWORK_LABEL } from "@/lib/constants"; const TelegramIcon = () => ( - Testnet + {NETWORK_LABEL} ยท diff --git a/frontend/src/components/layout/sidebar.tsx b/frontend/src/components/layout/sidebar.tsx index f5042fe..dd9682f 100644 --- a/frontend/src/components/layout/sidebar.tsx +++ b/frontend/src/components/layout/sidebar.tsx @@ -6,7 +6,7 @@ import { useEffect } from "react"; import { cn } from "@/lib/utils"; import { useBlockHeight } from "@/hooks/use-block-height"; import { useAppStore } from "@/stores/app-store"; -import { FEEDBACK_URL } from "@/lib/constants"; +import { FEEDBACK_URL, NETWORK_LABEL } from "@/lib/constants"; import { LayoutDashboard, PlusCircle, @@ -158,7 +158,7 @@ export function Sidebar() { - Testnet + {NETWORK_LABEL} #{blockHeight.toLocaleString()} diff --git a/frontend/src/lib/constants.ts b/frontend/src/lib/constants.ts index 8710130..15e6fdf 100644 --- a/frontend/src/lib/constants.ts +++ b/frontend/src/lib/constants.ts @@ -33,6 +33,10 @@ export const EXPLORER_BASE = IS_MAINNET ? "https://explorer.hiro.so" : "https://explorer.hiro.so/?chain=testnet"; +// Human-readable network name for UI badges and labels. Derived from NETWORK +// so the site always reflects the chain it is actually running on. +export const NETWORK_LABEL = IS_MAINNET ? "Mainnet" : "Testnet"; + // ============================================================================ // Stream Status Codes (matching smart contract) // ============================================================================ diff --git a/frontend/src/lib/stacks.ts b/frontend/src/lib/stacks.ts index 81d9b95..f8ebe65 100644 --- a/frontend/src/lib/stacks.ts +++ b/frontend/src/lib/stacks.ts @@ -245,7 +245,11 @@ export function buildCreateStreamTx(params: { contractName: mgrName, functionName: "create-stream", functionArgs, - postConditionMode: PostConditionMode.Allow, + // Deny-by-default: the wallet rejects any token movement not covered by an + // explicit post-condition. create-stream moves the deposit out of the + // sender's wallet, so we bound that outflow at exactly depositAmount โ€” + // matching the deny-by-default exit paths (claim/cancel). + postConditionMode: PostConditionMode.Deny, postConditions: [ Pc.principal(params.senderAddress) .willSendLte(params.depositAmount) @@ -386,7 +390,10 @@ export function buildTopUpStreamTx(params: { principalCV(params.tokenContract), uintCV(params.amount), ], - postConditionMode: PostConditionMode.Allow, + // Deny-by-default: top-up moves additional tokens out of the sender's + // wallet, so we bound that outflow at exactly the top-up amount โ€” + // matching the deny-by-default exit paths (claim/cancel). + postConditionMode: PostConditionMode.Deny, postConditions: [ Pc.principal(params.senderAddress) .willSendLte(params.amount) diff --git a/tests/stream-manager.test.ts b/tests/stream-manager.test.ts index 9c84b58..364bd6a 100644 --- a/tests/stream-manager.test.ts +++ b/tests/stream-manager.test.ts @@ -1408,9 +1408,20 @@ describe("StackStream - Stream Manager Contract", () => { for (let cycle = 0; cycle < 5; cycle++) { simnet.mineEmptyBlocks(Math.max(2, Math.floor(Number(duration) * 0.04))); - simnet.callPublicFn(streamManagerContract, "pause-stream", [Cl.uint(streamId)], wallet1); + + // The contract intentionally rejects pause/resume once the stream has + // elapsed (ERR-STREAM-ENDED) to avoid a zombie ACTIVE state. The + // "returns to ACTIVE" invariant only applies while the stream is still + // live, so stop the cycle once a random walk pushes us past end-block โ€” + // otherwise a resume that lands on/after end-block legitimately leaves + // the stream PAUSED and is not a contract bug. + const pause = simnet.callPublicFn(streamManagerContract, "pause-stream", [Cl.uint(streamId)], wallet1); + if ((pause.result as any).type !== "ok") break; + simnet.mineEmptyBlocks(2); - simnet.callPublicFn(streamManagerContract, "resume-stream", [Cl.uint(streamId)], wallet1); + + const resume = simnet.callPublicFn(streamManagerContract, "resume-stream", [Cl.uint(streamId)], wallet1); + if ((resume.result as any).type !== "ok") break; const status = (simnet.callReadOnlyFn(streamManagerContract, "get-stream-status", [Cl.uint(streamId)], deployer).result as any).value.value as bigint; expect(status).toBe(0n); // STATUS-ACTIVE