Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,8 @@ MEM_SERVICE_SUB=svc:atlas # read-only service membership on the gatewa
# Shared limiter store (cross-instance); without it the limiter is per instance.
# UPSTASH_REDIS_REST_URL=
# UPSTASH_REDIS_REST_TOKEN=

# Testnet reset notice (r1007): the reset date shown in the developer-page callout,
# YYYY-MM-DD (UTC). Leave empty until the date is set: the callout then says
# "to be announced". Inlined at build, so changing it needs a redeploy.
NEXT_PUBLIC_TESTNET_RESET_DATE=
3 changes: 3 additions & 0 deletions components/doc-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { Icon } from "./icons";
import { findNavBySlug } from "@/lib/nav-util";
import { cn } from "@/lib/cn";
import { FreshnessBadge } from "./freshness-badge";
import { TestnetResetNotice } from "./testnet-reset-notice";
import { showsTestnetResetNotice } from "@/lib/testnet-reset";

/** The tier-aware document reader — renders every DocResponse state (DESIGN_BRIEF §7, §15). */
export function DocView({ slug }: { slug: string }) {
Expand Down Expand Up @@ -115,6 +117,7 @@ export function DocView({ slug }: { slug: string }) {
const confidential = doc.tier === "confidential";
return (
<Shell>
{showsTestnetResetNotice(slug) && <TestnetResetNotice />}
{confidential && (
<div className="mb-4 flex items-center gap-2 rounded-xl border border-[color-mix(in_oklab,var(--color-amber)_45%,transparent)] bg-[color-mix(in_oklab,var(--color-amber)_8%,transparent)] px-3 py-2 text-xs text-[var(--color-amber)]">
<Icon name="lock" size={13} /> Confidential · access-logged · acknowledged · served at request time (never in the build).
Expand Down
24 changes: 24 additions & 0 deletions components/testnet-reset-notice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Link from "next/link";
import { testnetResetCallout } from "@/lib/testnet-reset";
import { Icon } from "./icons";

/** Callout at the top of developer pages: the r1007 testnet reset (lib/testnet-reset.ts). */
export function TestnetResetNotice() {
const c = testnetResetCallout(process.env.NEXT_PUBLIC_TESTNET_RESET_DATE);
return (
<aside
aria-label="Testnet reset notice"
className="mb-5 flex items-start gap-2 rounded-xl border border-[color-mix(in_oklab,var(--color-amber)_45%,transparent)] bg-[color-mix(in_oklab,var(--color-amber)_8%,transparent)] px-3 py-2 text-sm text-[var(--color-fg)]"
>
<span className="mt-0.5 shrink-0 text-[var(--color-amber)]" aria-hidden="true">
<Icon name="clock" size={14} />
</span>
<p className="m-0">
<strong>{c.title}.</strong> {c.body} Reset date: <strong>{c.dateLabel}</strong>.{" "}
<Link href={c.href} className="font-medium underline underline-offset-2">
{c.linkLabel}
</Link>
</p>
</aside>
);
}
4 changes: 2 additions & 2 deletions content/_generated/content.ts

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions content/start/testnet-reset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
title: Notice for developers, the testnet will reset
codex_slug: /start/testnet-reset
tier: public
org_scope: ~
source_kind: authored
source: Owner-approved developer notice (r1007, 2026-11-07)
surfaces: [START-testnet-reset]
audited_against_sha: ~
status: Implemented
created: 2026-11-07T17:30:00Z
author: Citrate team
nav_order: 1
---

The current Citrate testnet (chain ID 40204) will be reset before mainnet. The reset starts a
new chain from a fresh genesis block. Chain ID 40204 stays the same.

## What this means for you

Treat everything on the current testnet as temporary. Contracts you deploy, balances, and
transaction history will not carry over. Keep your source code and deploy scripts, so you can
redeploy after the reset.

A stable release of the testnet on chain ID 40204 is coming. We will say when it is ready for
longer-lived deployments.

## Test SALT

To get test SALT, go to https://faucet.citrate.ai and enter your account address. The faucet
sends 10 test SALT per address every 24 hours. Test SALT is only for testing. After the reset,
request test SALT again from the same faucet.

## When it happens

We will post the reset date on this page and on https://x.com/citratenetwork before it
happens. Questions or problems from your hands-on testing are welcome at
https://github.com/CitrateNetwork.
51 changes: 51 additions & 0 deletions lib/testnet-reset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Testnet reset notice (r1007). Owner-approved developer notice, 2026-11-07.
*
* DATA SOURCE: the reset date is a deploy config value,
* NEXT_PUBLIC_TESTNET_RESET_DATE (YYYY-MM-DD, UTC), inlined at build. Unset or
* malformed means "to be announced". The full notice is the public page
* content/start/testnet-reset.md. Nothing here reads the chain.
*
* Copy rules (owner): no em-dashes, no overstatement, no rewards language.
* test/testnet-reset.test.ts enforces them.
*/

export const TESTNET_RESET_SLUG = "/start/testnet-reset";

/** Sections whose pages are for developers building on the testnet. */
const DEVELOPER_SECTIONS = new Set(["chain", "contracts", "sdks", "aa", "compute"]);

export function showsTestnetResetNotice(slug: string): boolean {
if (slug === TESTNET_RESET_SLUG) return false;
const parts = slug.replace(/^\//, "").split("/");
return DEVELOPER_SECTIONS.has(parts[0] ?? "") || parts.includes("tutorials");
}

function dateLabel(raw: string | undefined | null): string {
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec((raw ?? "").trim());
if (!m) return "to be announced";
const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];
const dt = new Date(Date.UTC(y, mo - 1, d));
if (dt.getUTCFullYear() !== y || dt.getUTCMonth() !== mo - 1 || dt.getUTCDate() !== d) return "to be announced";
return dt.toLocaleDateString("en-US", { timeZone: "UTC", year: "numeric", month: "long", day: "numeric" });
}

export interface TestnetResetCallout {
title: string;
body: string;
dateLabel: string;
href: string;
linkLabel: string;
}

export function testnetResetCallout(rawDate: string | undefined | null): TestnetResetCallout {
return {
title: "The testnet will reset",
body:
"The current testnet (chain ID 40204) will restart from a fresh genesis block before mainnet. The chain ID stays the same. " +
"Contracts, balances and transaction history will not carry over, so keep your source code and deploy scripts to redeploy.",
dateLabel: dateLabel(rawDate),
href: TESTNET_RESET_SLUG,
linkLabel: "Read the notice",
};
}
74 changes: 74 additions & 0 deletions test/testnet-reset.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* Testnet reset notice (r1007, owner-approved developer notice 2026-11-07).
* A callout at the top of the developer pages plus one public notice page.
* The reset date is a config value (NEXT_PUBLIC_TESTNET_RESET_DATE), "to be
* announced" until set. Owner copy rules: no em-dashes, no overstatement, no
* rewards language, never "dogfood".
*/
import { describe, it, expect } from "vitest";
import fs from "node:fs";
import path from "node:path";
import {
showsTestnetResetNotice,
testnetResetCallout,
TESTNET_RESET_SLUG,
} from "@/lib/testnet-reset";
import { CONTENT_DOCS } from "@/content/_generated/content";

describe("which pages carry the callout", () => {
it("shows on developer sections", () => {
for (const s of ["/chain/rpc", "/contracts/reference", "/sdks/js", "/aa/paymaster", "/compute/gateway"]) {
expect(showsTestnetResetNotice(s)).toBe(true);
}
});
it("shows on every tutorial", () => {
expect(showsTestnetResetNotice("/start/tutorials/first-transaction")).toBe(true);
expect(showsTestnetResetNotice("/apps/tutorials/anything")).toBe(true);
});
it("does not show on non-developer pages or on the notice page itself", () => {
for (const s of ["/start/primer", "/research/nat", "/enterprise/overview", TESTNET_RESET_SLUG, "/chainlink"]) {
expect(showsTestnetResetNotice(s)).toBe(false);
}
});
});

describe("callout copy and date", () => {
it("says 'to be announced' without a configured date", () => {
expect(testnetResetCallout(undefined).dateLabel).toBe("to be announced");
expect(testnetResetCallout("not-a-date").dateLabel).toBe("to be announced");
});
it("shows a configured YYYY-MM-DD date in UTC", () => {
expect(testnetResetCallout("2026-12-01").dateLabel).toBe("December 1, 2026");
});
it("keeps the approved facts and links the notice page", () => {
const c = testnetResetCallout(undefined);
expect(c.body).toContain("40204");
expect(c.body).toMatch(/fresh genesis/i);
expect(c.body).toMatch(/will not carry over/i);
expect(c.href).toBe(TESTNET_RESET_SLUG);
});
it("follows the owner copy rules", () => {
const c = testnetResetCallout("2026-12-01");
const all = `${c.title} ${c.body} ${c.linkLabel}`;
expect(all).not.toMatch(/[—–!]/);
expect(all).not.toMatch(/\b(reward|rewards|earn|earnings|airdrop|dogfood|guarantee)\b/i);
});
});

describe("the notice page", () => {
const file = path.resolve(__dirname, "../content/start/testnet-reset.md");
it("exists, is public and is in the generated content", () => {
expect(fs.existsSync(file)).toBe(true);
const doc = CONTENT_DOCS[TESTNET_RESET_SLUG];
expect(doc).toBeTruthy();
expect(doc.tier).toBe("public");
});
it("carries the approved facts and follows the copy rules", () => {
const md = fs.readFileSync(file, "utf8");
for (const s of ["40204", "fresh genesis", "will not carry over", "https://faucet.citrate.ai", "10 test SALT", "https://x.com/citratenetwork", "https://github.com/CitrateNetwork"]) {
expect(md).toContain(s);
}
expect(md).not.toMatch(/[—–!]/);
expect(md).not.toMatch(/\b(reward|rewards|earn|earnings|airdrop|dogfood|guarantee)\b/i);
});
});
Loading