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
20 changes: 10 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Contributing to Lucent

Thanks for taking an interest in Lucent. This is a testnet-only, unaudited
confidential-payments protocol on Stellar — see the
confidential-payments protocol on Stellar, see the
["Not production ready"](README.md#not-production-ready) note before you dig
in. Contributions of all sizes are welcome: bug reports, docs fixes, tests,
and features.
Expand All @@ -11,18 +11,18 @@ and features.
- Be respectful and constructive. See the [Code of Conduct](CODE_OF_CONDUCT.md).
- Found a potential security issue (a bug that could move or reveal funds in
a way the protocol isn't supposed to allow)? Do **not** open a public
issue — follow [SECURITY.md](SECURITY.md) instead.
issue, follow [SECURITY.md](SECURITY.md) instead.
- Open an issue before starting any large or architectural change, so we can
agree on the approach before you invest the time.

## Project layout

```
contracts/ Rust/Soroban contracts — a separate Cargo workspace
contracts/ Rust/Soroban contracts, a separate Cargo workspace
packages/
sdk/ @lucent/sdk crypto · witness · proving · chain · state · auditor · disclosure
disclosure/ @lucent/disclosure shared disclosure circuits + pinned verification keys
app/ @lucent/app Next.js front-end (Freighter wallet)
app/ @lucent/app Next.js front-end (Stellar wallet)
indexer/ @lucent/indexer optional Goldsky indexer
scripts/ build/deploy/e2e scripts shared across packages
```
Expand All @@ -33,7 +33,7 @@ how these fit together.

## Getting set up

Requires **Node ≥ 20**, **pnpm 10**, and — for contract work — Rust via
Requires **Node ≥ 20**, **pnpm 10**, and, for contract work, Rust via
[`rustup`](https://rustup.rs) (the pinned toolchain in
`contracts/rust-toolchain.toml` installs itself automatically) plus
`stellar` CLI ≥ 25.2.
Expand All @@ -54,7 +54,7 @@ pnpm build:sdk # @lucent/sdk must be built before the app runs aga
pnpm --filter @lucent/app typecheck
pnpm --filter @lucent/sdk typecheck
pnpm test:sdk # full suite, including real proof generation (slow, needs network for the CRS on first run)
pnpm --filter @lucent/sdk test:fast # everything except live proving — use this for quick iteration
pnpm --filter @lucent/sdk test:fast # everything except live proving, use this for quick iteration
```

**Contracts (Rust/Soroban)**
Expand All @@ -63,19 +63,19 @@ pnpm --filter @lucent/sdk test:fast # everything except live proving — use t
cargo test --manifest-path contracts/Cargo.toml
cargo clippy --manifest-path contracts/Cargo.toml --all-targets
cargo fmt --manifest-path contracts/Cargo.toml -- --check
pnpm build:contracts # stellar contract build — required for wasm output, see contracts/Cargo.toml
pnpm build:contracts # stellar contract build, required for wasm output, see contracts/Cargo.toml
```

If you touch a contract's public interface, update or add tests in that
crate — see the [test coverage table](README.md#building-and-testing-the-contracts)
crate, see the [test coverage table](README.md#building-and-testing-the-contracts)
in the README for what's already covered.

## Commits and pull requests

- Keep commits focused; a commit message should explain *why*, not just
*what* (the diff already shows what).
- Rebase on top of `main` before opening a PR if it's fallen behind.
- Fill out the PR template — at minimum, state what changed, why, and how you
- Fill out the PR template, at minimum, state what changed, why, and how you
tested it (unit tests, `e2e`/`e2e:disclosure` scripts, or manual testnet
walkthrough).
- CI must pass (typecheck, SDK tests, contract tests/clippy/fmt) before
Expand All @@ -85,5 +85,5 @@ in the README for what's already covered.
## Reporting bugs / requesting features

Use the issue templates. For bugs, include: what you did, what you expected,
what happened, and — if it's chain-related — the network (testnet), the
what happened, and, if it's chain-related, the network (testnet), the
contract IDs involved, and the transaction hash if you have one.
143 changes: 80 additions & 63 deletions README.md

Large diffs are not rendered by default.

385 changes: 126 additions & 259 deletions packages/app/app/about/page.tsx

Large diffs are not rendered by default.

66 changes: 31 additions & 35 deletions packages/app/app/auditor/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
* Auditor console (DESIGN.md §8). Whoever holds the auditor's Grumpkin secret
* decrypts the dual-channel ciphertexts that each transfer and withdraw event
* carries. Pure key-and-events work: no wallet, no proving, no holder
* cooperation — just the secret, pasted in.
* cooperation, just the secret, pasted in.
*
* The secret never ships in the client bundle — `deploy.ts` redacts it from
* The secret never ships in the client bundle, `deploy.ts` redacts it from
* the app's deployment file for exactly this reason (see `lib/deployment.ts`).
* Once pasted in, it's remembered in this browser's localStorage (never sent
* anywhere) so an auditor doesn't have to keep re-pasting it every visit; any
Expand Down Expand Up @@ -47,6 +47,7 @@ import { timeAgo } from "@/lib/format";
import { DEPLOYMENT } from "@/lib/deployment";
import { errMsg } from "@/lib/err";
import { ErrorBanner } from "@/components/ui/ErrorBanner";
import { Segmented } from "@/components/ui/Segmented";
import { useWallet } from "@/lib/wallet-context";
import { useAction } from "@/lib/use-action";
import { cn } from "@/lib/cn";
Expand Down Expand Up @@ -136,7 +137,7 @@ function replay(events: ConfidentialEvent[], auditorSk: bigint): { rows: AuditRo
a.spendable = senderBalance;
rows.push({
ev,
text: "withdrawal (public amount) — checkpoint decrypted",
text: "withdrawal (public amount), checkpoint decrypted",
amount: ev.amount,
senderBalance,
channelsAgree: true,
Expand All @@ -154,7 +155,7 @@ function replay(events: ConfidentialEvent[], auditorSk: bigint): { rows: AuditRo
rows.push({
ev,
text: d.channelsAgree
? "confidential transfer — both channels decrypted"
? "confidential transfer, both channels decrypted"
: "transfer did NOT decrypt under this key",
amount: d.channelsAgree ? d.amount : null,
senderBalance: d.channelsAgree ? d.senderBalance : null,
Expand All @@ -171,7 +172,7 @@ function replay(events: ConfidentialEvent[], auditorSk: bigint): { rows: AuditRo
};
}

/** Remembered auditor secrets, most-recent-first — see the module doc comment. */
/** Remembered auditor secrets, most-recent-first, see the module doc comment. */
const SAVED_SECRETS_KEY = "lucent:auditor:secrets";
const MAX_SAVED_SECRETS = 6;

Expand Down Expand Up @@ -255,7 +256,7 @@ export default function AuditorPage() {
contracts: DEPLOYMENT.contracts,
});
// Hybrid source: the indexer (when configured) backfills the full history
// below the RPC's ~7-day window — exactly what an auditor needs. The RPC
// below the RPC's ~7-day window, exactly what an auditor needs. The RPC
// leg is clamped to the retention boundary internally.
const indexer = DEPLOYMENT.indexerUrl
? new IndexerClient({ baseUrl: DEPLOYMENT.indexerUrl })
Expand Down Expand Up @@ -290,20 +291,15 @@ export default function AuditorPage() {

<div className="flex flex-col gap-5 px-4 pb-8 md:mx-auto md:max-w-2xl md:px-8">
{hasCompliance && (
<div className="flex gap-2 rounded-2xl border border-border bg-card p-1">
{(["decrypt", "compliance"] as Tab[]).map((t) => (
<button
key={t}
onClick={() => setTab(t)}
className={cn(
"flex-1 rounded-xl py-2.5 text-sm font-medium capitalize transition-all duration-200",
tab === t ? "bg-accent text-black" : "text-text-muted hover:text-text-secondary",
)}
>
{t === "decrypt" ? "Decrypt" : "Compliance"}
</button>
))}
</div>
<Segmented
value={tab}
onChange={setTab}
ariaLabel="Auditor view"
options={[
{ value: "decrypt", label: "Decrypt" },
{ value: "compliance", label: "Compliance" },
]}
/>
)}

{tab === "compliance" && hasCompliance ? (
Expand All @@ -312,7 +308,7 @@ export default function AuditorPage() {
<>
<p className="text-sm leading-relaxed text-text-secondary">
Whoever holds a registered auditor's Grumpkin secret decrypts every transfer and withdrawal
addressed to that key — no wallet, no proofs, no account cooperation required.
addressed to that key, no wallet, no proofs, no account cooperation required.
</p>

{auditorSk === null && (
Expand All @@ -326,8 +322,8 @@ export default function AuditorPage() {
<GlassCard padding="md" className="border-accent/25">
<SectionLabel>Unlock Console</SectionLabel>
<p className="mb-3 mt-3 text-xs text-text-muted">
Paste your auditor secret key (the <code className="rounded bg-white/[0.07] px-1 py-0.5 font-mono text-[0.85em] text-accent/90">k</code> whose
public point <code className="rounded bg-white/[0.07] px-1 py-0.5 font-mono text-[0.85em] text-accent/90">K_aud = k·H</code> was
Paste your auditor secret key (the <code className="rounded bg-fg/[0.07] px-1 py-0.5 font-mono text-[0.85em] text-accent/90">k</code>{" "}whose
public point <code className="rounded bg-fg/[0.07] px-1 py-0.5 font-mono text-[0.85em] text-accent/90">K_aud = k·H</code>{" "}was
registered on-chain) to decrypt with it.
</p>
<div className="flex flex-col gap-3 sm:flex-row sm:items-end">
Expand All @@ -352,7 +348,7 @@ export default function AuditorPage() {
{savedSecrets.map((s) => (
<div
key={s}
className="flex items-center justify-between gap-2 rounded-lg border border-border bg-white/[0.02] px-3 py-2"
className="flex items-center justify-between gap-2 rounded-lg border border-border bg-fg/[0.02] px-3 py-2"
>
<button
onClick={() => unlockWith(s)}
Expand Down Expand Up @@ -382,8 +378,8 @@ export default function AuditorPage() {
</Button>
</div>
<p className="mb-3 mt-3 text-xs text-text-muted">
Console unlocked with the key you pasted in. Only <code className="rounded bg-white/[0.07] px-1 py-0.5 font-mono text-[0.85em] text-accent/90">K_aud</code> —
the public point — is shown below; the secret itself is never redisplayed.
Console unlocked with the key you pasted in. Only <code className="rounded bg-fg/[0.07] px-1 py-0.5 font-mono text-[0.85em] text-accent/90">K_aud</code>,
the public point, is shown below; the secret itself is never redisplayed.
</p>
{kAud && (
<dl className="space-y-1 break-all font-mono text-xs text-text-secondary">
Expand Down Expand Up @@ -416,7 +412,7 @@ export default function AuditorPage() {
Reconstructed from sender-channel balance checkpoints and decrypted inbound credits.{" "}
{hasIndexer
? "Backed by the Goldsky indexer, so the full deployment history is decrypted."
: "Only events inside the RPC's ~7-day retention window are available — accounts with older history may be incomplete."}
: "Only events inside the RPC's ~7-day retention window are available, accounts with older history may be incomplete."}
</p>
{accounts.length === 0 && !busy && (
<p className="text-sm text-text-muted">No accounts in the retention window.</p>
Expand Down Expand Up @@ -462,7 +458,7 @@ export default function AuditorPage() {
<h3 className="mb-1 font-semibold text-text-primary">Decrypted activity</h3>
<p className="mb-3 text-xs text-text-muted">
Every token-contract event {hasIndexer ? "since deployment" : "in the retention window"},
newest first. Amounts every other screen shows encrypted appear here in cleartext —
newest first. Amounts every other screen shows encrypted appear here in cleartext,
decrypted with your key alone.
</p>
<ErrorBanner error={error} onDismiss={() => setError(null)} className="mb-3" size="sm" />
Expand Down Expand Up @@ -494,7 +490,7 @@ export default function AuditorPage() {
* Freeze/unfreeze + allowlist management. Separate from the key-paste
* decryption flow above: those are Grumpkin viewing keys, but compliance
* actions are real on-chain transactions the token's `#[only_admin]` check
* gates — they need a connected, signing Freighter wallet, not the auditor
* gates, they need a connected, signing wallet, not the auditor
* secret. The app doesn't duplicate that authorization check; a connected
* wallet that isn't the admin will just have its transaction rejected
* on-chain, surfaced here as an error like anywhere else in the app.
Expand Down Expand Up @@ -539,21 +535,21 @@ function CompliancePanel() {
</div>
<p className="mb-3 mt-3 text-xs leading-relaxed text-text-muted">
{gateEnabled
? "The allowlist gate is ON — only allowlisted accounts can register, deposit, transfer, receive, or withdraw."
: "The allowlist gate is OFF — every account can register and transact freely. Freeze still works independently, for a specific bad-actor account."}{" "}
Both are enforced on-chain by the token contract, not by this page — only the compliance
? "The allowlist gate is ON, only allowlisted accounts can register, deposit, transfer, receive, or withdraw."
: "The allowlist gate is OFF, every account can register and transact freely. Freeze still works independently, for a specific bad-actor account."}{" "}
Both are enforced on-chain by the token contract, not by this page, only the compliance
admin set at deploy time can act here.
</p>

{!wallet ? (
<Button fullWidth isLoading={connecting} onClick={connect}>
Connect Freighter to manage compliance
Connect your wallet to manage compliance
</Button>
) : (
<div className="flex flex-col gap-3">
<ErrorBanner error={error} onDismiss={() => setError(null)} size="sm" />

<div className="flex items-center justify-between rounded-xl border border-border bg-white/[0.02] px-3 py-2.5">
<div className="flex items-center justify-between rounded-xl border border-border bg-fg/[0.02] px-3 py-2.5">
<div>
<span className="text-xs font-medium text-text-primary">Allowlist gate</span>
<p className="text-[11px] text-text-muted">
Expand Down Expand Up @@ -649,7 +645,7 @@ function CompliancePanel() {
function AuditRowView({ row }: { row: AuditRow }) {
const { ev } = row;
return (
<li className="rounded-xl border border-border bg-white/[0.02] p-3">
<li className="rounded-xl border border-border bg-fg/[0.02] p-3">
<div className="flex flex-wrap items-center gap-2">
<Pill tone={badgeTone(ev.type)}>{ev.type}</Pill>
{ev.type === "register" || ev.type === "merge" ? (
Expand Down
Loading
Loading