diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ef4f751..8d4e3df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. @@ -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 ``` @@ -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. @@ -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)** @@ -63,11 +63,11 @@ 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 @@ -75,7 +75,7 @@ in the README for what's already covered. - 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 @@ -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. diff --git a/README.md b/README.md index 82d0a83..77f1f34 100644 --- a/README.md +++ b/README.md @@ -23,20 +23,20 @@ Sender and receiver stay public and verifiable on-chain. Only the amount moves i Watch the Lucent launch demo on YouTube -

Watch on YouTube — a short animated walkthrough of shielding USDC, confidential transfers, payroll, escrow, and selective disclosure.

+

Watch on YouTube, a short animated walkthrough of shielding USDC, confidential transfers, payroll, escrow, and selective disclosure.

Balances are Pedersen commitments on the Grumpkin curve. Every register, withdraw, and transfer carries an UltraHonk zero-knowledge proof, generated -in the browser and verified natively on-chain by Soroban — no off-chain +in the browser and verified natively on-chain by Soroban, no off-chain operators, no relayers, no custodians. On top of that primitive, Lucent ships a full payments product: shielded deposits and withdrawals, confidential transfers, confidential payroll, confidential escrow, an auditor console for compliance, and off-chain -selective disclosure — plus a complete Next.js front-end and a Freighter -wallet integration. +selective disclosure, plus a complete Next.js front-end with Stellar wallet +integration. > ⚠️ **Not production ready.** The UltraHonk verifier backend and the circuits @@ -70,16 +70,16 @@ pnpm dev # http://localhost:3000 `pnpm dev` and `pnpm build` both build `@lucent/sdk` first automatically (it's a workspace package the app imports from its compiled `dist/`, not from -source) — no separate build step needed. +source), no separate build step needed. -Install [Freighter](https://freighter.app/), switch it to **Testnet**, and -fund your account (Freighter's built-in friendbot). `next dev` already serves +Install a Stellar wallet that supports message signing, switch it to +**Testnet**, and fund your account with the friendbot. `next dev` already serves the cross-origin-isolation headers in-browser proving needs, so there's nothing extra to configure locally. Home, Shield, Send, Auditor, and Verify work immediately against the deployment in `deployments/testnet.json`. **Payroll and Escrow** show a "not configured" -notice until their contracts are deployed ([below](#deploying)) — after which +notice until their contracts are deployed ([below](#deploying)), after which they light up automatically, no code change needed. Want to see the compliance view without deploying anything yourself? The in-app **Docs** page's Auditor section has a demo key you can paste straight in. @@ -89,19 +89,19 @@ page's Auditor section has a demo key you can paste straight in. | | | |---|---| | **Shield** | Deposit public USDC into a confidential balance, merge receiving into spendable, withdraw back to public USDC. Registration binds a Grumpkin key set to the account, one time. | -| **Send** | Confidential transfers to any registered account. The amount never appears in plaintext anywhere on-chain — not in the transaction, not in an event, not in contract storage. | +| **Send** | Confidential transfers to any registered account. The amount never appears in plaintext anywhere on-chain, not in the transaction, not in an event and not in contract storage. | | **Payroll** | An employer distributes salaries to a set of employees in one atomic run. No employee can read another's amount; the vault itself never sees a plaintext salary either. | | **Escrow** | Two-party (optionally arbitrated) escrow whose locked amount stays confidential through creation, funding, delivery, dispute, and release. Each escrow is its own isolated confidential account. | | **Auditor console** | The party holding the registered Grumpkin auditor key decrypts every transfer amount and balance checkpoint across the deployment, for every account, without needing anyone's cooperation. | -| **Compliance** | An allowlist policy and per-account freezing, both enforced on-chain by the token contract itself and gated behind a single compliance admin. An account off the allowlist, or frozen, is rejected before any amount is touched — the primitive KYC-gated payroll needs. Managed from the Auditor screen. | +| **Compliance** | An allowlist policy and per-account freezing, both enforced on-chain by the token contract itself and gated behind a single compliance admin. An account off the allowlist, or frozen, is rejected before any amount is touched, the primitive KYC-gated payroll needs. Managed from the Auditor screen. | | **Selective disclosure** | A holder proves that one specific transfer paid exactly one amount to one counterparty, off-chain, revealing nothing else. The counterparty verifies the proof against the chain itself. | -| **Freighter wallet** | The only supported signer. Confidential keys are derived deterministically from a Freighter message signature and cached locally, so the signing prompt only appears once per account. | -| **Local state engine** | Every balance read goes through a client-side reconstruction layer that persists decrypted openings and re-verifies them against on-chain commitments — load-bearing, not a cache, since the Soroban RPC only serves ~7 days of event history. | +| **Wallet** | The only supported signer. Confidential keys are derived deterministically from a wallet message signature and cached locally, so the signing prompt only appears once per account. | +| **Local state engine** | Every balance read goes through a client-side reconstruction layer that persists decrypted openings and re-verifies them against on-chain commitments, so it is load-bearing, not a cache, since the Soroban RPC only serves ~7 days of event history. | | **Proof UX** | Every proof-carrying action shows a progress state immediately on tap; nothing ever looks frozen while a proof generates in-browser (typically a few seconds, up to tens of seconds for multi-proof flows like escrow funding). | ## How value moves -A balance is a commitment `C = v·G + r·H` — the chain only ever sees +A balance is a commitment `C = v·G + r·H`. The chain only ever sees commitments and proofs, never plaintext amounts. Every confidential account holds a **spendable** balance (what you can send or withdraw) and a **receiving** balance (where deposits and incoming transfers land); `merge` @@ -110,25 +110,25 @@ folds one into the other homomorphically, with no proof needed. | Operation | Proof? | Effect | |---|---|---| | `register` | ✔ | Bind a Grumpkin key set to the contract (one-time) | -| `deposit` | — | Public USDC → receiving balance | -| `merge` | — | Receiving → spendable | +| `deposit` | none | Public USDC → receiving balance | +| `merge` | none | Receiving → spendable | | `withdraw` | ✔ | Spendable → public USDC | | `confidential_transfer` | ✔ | Spendable → another account's receiving balance | `deposit` is the one public amount: a plaintext `i128` argument in USDC base -units (7 decimals, so 1 USDC = 10,000,000 base units) — deliberately, so the +units (7 decimals, so 1 USDC = 10,000,000 base units). This is deliberate, so the on-chain reserve backing the confidential supply stays auditable. Every other operation hides the amount behind a commitment and a proof. -Every transfer also emits **dual auditor ciphertexts** — one for the sender's -channel, one for the recipient's — encrypted to the registered auditor's +Every transfer also emits **dual auditor ciphertexts**, one for the sender's +channel, one for the recipient's, encrypted to the registered auditor's Grumpkin public key. That's the compliance channel Payroll and Auditor build on: register an employer as the auditor, and every salary transfer becomes decryptable to them alone. Because a Soroban contract cannot generate a ZK proof, confidential value can only move via a `confidential_transfer` proven by whoever holds the sender's -key — a contract can never silently move funds on someone's behalf. That +key, so a contract can never silently move funds on someone's behalf. That single constraint is what shapes Payroll and Escrow into the design below. ## Protocol contracts @@ -144,7 +144,7 @@ single constraint is what shapes Payroll and Escrow into the design below. **PayrollVault** is an orchestrator, not a custodian: an employer creates a template of employees and opens a run against it. Salaries are never written -to chain storage — at `execute_run` the employer's browser proves one +to chain storage. At `execute_run`, the employer's browser proves one `confidential_transfer` per employee, and the vault routes them atomically and records run state. The employer registers as the run's auditor, so every salary stays decryptable to them and opaque to everyone else. @@ -173,7 +173,7 @@ Settlement spends the fixed commitment without merging unsolicited later credits At settlement, the instance's own state machine (created → funded → completed → released / disputed / refunded / cancelled) submits exactly the -one pre-generated proof it selects — the instance self-authorizes its own +one pre-generated proof it selects. The instance self-authorizes its own outgoing `confidential_transfer` by invocation, so no one needs to hold its key at settlement time. See the in-app Docs page for the full funding-to-settlement flow. @@ -187,11 +187,11 @@ funding-to-settlement flow. **Compliance Policy** is a separate, admin-gated allowlist contract, wired onto the token via `ComplianceConfig` (from OpenZeppelin's confidential token compliance extension). When the gate is on, every register, deposit, -transfer, receive, and withdraw checks the policy first — an account off the +transfer, receive, and withdraw checks the policy first, and an account off the allowlist is rejected on-chain before any amount is touched. The same admin can also freeze a specific account outright, independent of the allowlist -and independent of whether the gate is on. All three — the gate itself, -allowlist membership, and freezing — are toggled live from the Auditor +and independent of whether the gate is on. All three (the gate itself, +allowlist membership, and freezing) are toggled live from the Auditor screen's Compliance panel by whoever holds the compliance-admin wallet set at deploy time; the contract enforces that itself, not the app. Turning the gate off is the practical default for demos: it makes every address free to @@ -199,8 +199,8 @@ register and transact immediately, with freeze still available as the exception mechanism, rather than requiring every tester to be allowlisted first. -Lucent's confidential-token layer — the commitment scheme, the UltraHonk -circuits, and the Poseidon2/Grumpkin crypto — is built on +Lucent's confidential-token layer (the commitment scheme, the UltraHonk +circuits, and the Poseidon2/Grumpkin crypto) is built on [OpenZeppelin `stellar-contracts`](https://github.com/OpenZeppelin/stellar-contracts) (`feat/confidential-verifier-ultrahonk`), consumed as a git dependency; the on-chain verifier backend is @@ -210,28 +210,45 @@ the entire product front-end are Lucent's own. ## Product -The front-end (`packages/app`) is a dark, gold-accented Next.js app: - -- **Landing, Docs, About** — the public marketing surface. -- **Home · Shield · Send · Payroll · Escrow · Auditor · Verify · Profile** — - the app, reachable from a sidebar (desktop) or bottom nav (mobile: five - primary tabs plus a "More" sheet for Escrow, Verify, Auditor, and Profile), - all one tap from each other — none of them redirect. Home is the default - landing screen after connecting: balances, quick actions, and recent - activity at a glance, including a "Prove" action on your own transfers - right where they are in the feed. Shield, Send, Payroll, Escrow, Home, and - Profile need a connected wallet and show an inline connect prompt in place - of their content until one's attached; Auditor and Verify need no wallet - at all — Verify is purely the verifier side of selective disclosure now - (mint a request, check a returned bundle), since the holder side (proving) - lives on Home, next to the transfer it's about. -- A shared component library (glass cards, proof-loading overlays, encrypted - badges, a numeric keypad, tx-status steppers) and a `ConfidentialWallet` - client class wrapping the SDK's crypto, proving, chain, and state layers. - -For a full walkthrough of what each flow actually does on-chain, see the -in-app **Docs** page (`/docs`) — it covers Shield, Send, Payroll, Escrow, the -auditor model, and selective disclosure in depth. +The front-end (`packages/app`) is a Next.js app with a dark and a light theme +(it follows your system setting, with a toggle in every header). It is built +for desktop and mobile alike. + +- **Landing, Docs, About, Stats**: the public site. The landing page opens + with a live ledger demo that flips between what a public explorer sees (an + address pair and an encrypted amount) and what the auditor key sees. +- **Home, Shield, Send, Payroll, Escrow, Auditor, Verify, Profile**: the app. + Desktop gets a fixed sidebar grouped into Money, Programs and Compliance; + mobile gets a bottom bar with the main tabs plus a "More" sheet. Pages never + redirect, so everything is one tap from everything else. + - **Home** is the overview: total, spendable and receiving balances, a merge + prompt when funds are waiting, and an activity table with a "Prove" action + on your own transfers. + - **Shield** and **Send** pair a form with a live summary. Desktop uses a + large amount field; phones get an on-screen keypad. + - **Payroll** and **Escrow** list templates, runs and escrows as tables, each + expandable into the full state machine. + - Shield, Send, Payroll, Escrow, Home and Profile need a connected wallet and + show an inline connect prompt until one is attached. Auditor and Verify + need no wallet. Verify is the verifier side of selective disclosure (mint + a request, check a returned bundle); the holder side lives on Home, next to + the transfer it is about. +- A shared component library (panels, segmented controls, amount field, + proof-loading overlays, encrypted badges, tx-status steppers) styled by CSS + variable design tokens in `app/globals.css`, and a `ConfidentialWallet` + client class wrapping the SDK's crypto, proving, chain and state layers. + +### Demo mode + +In development, add `?demo=1` to any URL (for example +`http://localhost:3000/home?demo=1`) to run the app against a fake connected +wallet with sample balances, activity, escrows and payroll runs. It is meant +for UI work and screenshots, needs no wallet or chain access, and is compiled +out of production builds. `?demo=0` turns it off. + +For a full walkthrough of what each flow does on-chain, see the in-app +**Docs** page (`/docs`). It covers Shield, Send, Payroll, Escrow, the auditor +model and selective disclosure in depth. ## Building and testing the contracts @@ -255,7 +272,7 @@ cargo test --manifest-path contracts/Cargo.toml ## Deploying -The confidential token wraps a SEP-41 asset — **USDC**. Point the deploy at +The confidential token wraps a SEP-41 asset, **USDC**. Point the deploy at the USDC Stellar Asset Contract via `UNDERLYING_TOKEN`: ```bash @@ -266,15 +283,15 @@ stellar contract id asset --asset USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4 UNDERLYING_TOKEN=CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA pnpm deploy:contracts ``` -Deploys the full stack — token, verifier, auditor, compliance policy, -PayrollVault, and the PrivateEscrow factory — under your `admin` stellar CLI +Deploys the full stack, token, verifier, auditor, compliance policy, +PayrollVault, and the PrivateEscrow factory, under your `admin` stellar CLI identity, and writes both `deployments/testnet.json` and `packages/app/lib/deployment.json`. The token deploys with the compliance hooks enabled and `admin` set as the compliance admin, and the script wires the policy contract onto it via `set_compliance_config` right after deploy -(with `sac_passthrough: false` — the allowlist is the only gate). The app +(with `sac_passthrough: false`, the allowlist is the only gate). The app reads the deployment file directly, so a redeploy takes effect with **no -code edit and no env var** — just rebuild and run. (`NEXT_PUBLIC_PAYROLL_ID` / +code edit and no env var**, just rebuild and run. (`NEXT_PUBLIC_PAYROLL_ID` / `NEXT_PUBLIC_ESCROW_FACTORY_ID` exist only to override those two ids ahead of a redeploy, e.g. to point at someone else's.) @@ -282,16 +299,16 @@ a redeploy, e.g. to point at someone else's.) A full end-to-end walkthrough, real proofs on testnet: -1. **Shield** — connect Freighter, register, deposit, merge. -2. **Send** — confidential transfer to a second registered account. -3. **Payroll** — create a template of employees, open + confirm a run, enter +1. **Shield**: connect your wallet, register, deposit, merge. +2. **Send**, confidential transfer to a second registered account. +3. **Payroll**, create a template of employees, open + confirm a run, enter salaries, execute; then **Auditor** decrypts every salary amount. -4. **Escrow** — deploy + fund an escrow (four funding confirmations); walk it +4. **Escrow**, deploy + fund an escrow (four funding confirmations); walk it through mark-completed → release (or dispute → resolve). -5. **Selective disclosure** — mint a request on the Verify screen, disclose +5. **Selective disclosure**, mint a request on the Verify screen, disclose the matching transfer from Home's activity feed, verify the returned bundle back on Verify. -6. **Compliance** — on the Auditor screen's Compliance panel (connected as +6. **Compliance**, on the Auditor screen's Compliance panel (connected as the compliance admin), allowlist an account, confirm Send/Shield reject it before allowlisting; freeze an account and confirm the same. @@ -309,7 +326,7 @@ contracts/ Rust/Soroban (separate Cargo workspace) packages/ sdk/ @lucent/sdk crypto · witness · proving · chain (incl. payroll/escrow) · state · auditor · disclosure disclosure/ @lucent/disclosure shared disclosure circuits + pinned verification keys - app/ @lucent/app Next.js product front-end (Freighter wallet) + app/ @lucent/app Next.js product front-end (Stellar wallet) indexer/ @lucent/indexer optional Goldsky indexer for full event history scripts/ deploy.ts · e2e.ts · e2e-disclosure.ts ``` @@ -324,15 +341,15 @@ Read from `deployments/testnet.json`, rewritten automatically by | Underlying | USDC SAC `CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA` (issuer `GBBD47IF…FLA5`) | | Confidential token | `CACKEFUKVPBGDJM6Z4NOTY3H44MUMJDYIRNBLH6MDFN3QUSFWENXAABF` | | Verifier | `CAL32C7RZ5AB4IIYJBAH6XHPFZPFMUGMJ4ZDYQPOA3X7LX6EOJCZSTYN` | -| Auditor | `CB6FOYTSTRCCBYZGOZJVNY5RGCT5LXEEPV5D2BZK7O3LMKSLLLQ7QRUQ` (id `0` — demo key in the Docs page) | +| Auditor | `CB6FOYTSTRCCBYZGOZJVNY5RGCT5LXEEPV5D2BZK7O3LMKSLLLQ7QRUQ` (id `0`, demo key in the Docs page) | | PayrollVault | `CCO6LMKLPJCWUQYDEHOY2EHYPITYOGT6JPKXIRHDXKEUYTXQEAJPPVVS` | | PrivateEscrow factory | `CCGJ5XEZDFSKJSLWEO7D22KZXOSPRNWE4R3RFO4SQBWPQ5RZ3WK7I2XS` | | PrivateEscrow instance wasm | `44c5852692374c24c5fe2cc6c9e7fcfa936418365ae352266182743794501306` | -| Compliance Policy | `CB6Z7YIWYMKU3QUALYBJZMJEF72QRKBPFOCDHECPD3V5DCIRMU674UME` (gate is toggleable live from Auditor → Compliance — see the Compliance Policy section) | +| Compliance Policy | `CB6Z7YIWYMKU3QUALYBJZMJEF72QRKBPFOCDHECPD3V5DCIRMU674UME` (gate is toggleable live from Auditor → Compliance, see the Compliance Policy section) | ## Contributing -Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) for dev +Contributions are welcome, see [CONTRIBUTING.md](CONTRIBUTING.md) for dev setup, the test/lint commands CI runs, and PR expectations. Please also read the [Code of Conduct](CODE_OF_CONDUCT.md). Found a security issue? Follow [SECURITY.md](SECURITY.md) instead of opening a public issue. diff --git a/packages/app/app/about/page.tsx b/packages/app/app/about/page.tsx index c2df4c0..253fdaf 100644 --- a/packages/app/app/about/page.tsx +++ b/packages/app/app/about/page.tsx @@ -1,282 +1,149 @@ import Link from "next/link"; -import { ArrowRight, Eye, EyeOff, Lock, Shield, Users, Briefcase, Globe } from "lucide-react"; -import { LucentLogoMark } from "@/components/icons/LucentLogoMark"; +import { ArrowRight, ArrowUpRight } from "lucide-react"; +import { MarketingFooter, MarketingNav } from "@/components/layout/MarketingNav"; -export default function AboutPage() { - return ( -
- +const TRUST = [ + { + title: "No off-chain operators", + desc: "Every proof is generated in your browser and verified natively by Stellar's Protocol 26 host functions. There is no separate privacy infrastructure to trust.", + }, + { + title: "No operator keys", + desc: "No private key exists that could be stolen to decrypt your balances. The encryption is enforced by mathematics, not by a trusted party.", + }, + { + title: "No bridge custody", + desc: "Your USDC stays in Soroban contracts on Stellar. There is no bridge, no wrapped token and no custodian holding your funds.", + }, +]; -
- {/* Hero */} -
-
-
- Private payments for public chains -
-

- Finance has always needed -
- selective privacy. -

-

- You wouldn't hand your bank statement to a stranger on the street. Yet every payment - on a public blockchain is permanently, irrevocably visible to anyone who cares to look. - Lucent fixes that — without hiding who you paid, only how much. -

-
-
+const AUDIENCE = [ + { title: "Stellar teams", desc: "Pay contributors and vendors without publishing your treasury's payment history to competitors." }, + { title: "DAOs", desc: "Run payroll with per-person confidential salaries. No member can see another's compensation." }, + { title: "Freelancers", desc: "Invoice and get paid in USDC without your client list and rate card becoming public record." }, + { title: "Protocols", desc: "Need confidential escrow in your settlement layer? Integrate PrivateEscrow into your Soroban contracts." }, +]; - {/* The problem — two columns */} -
-
-

- The problem -

-
-
-
- -
-
-

The transparency trap

-

- Public blockchains expose every transaction to everyone, forever. Your salary, your - vendor payments, your treasury moves — all permanently readable by competitors, data - harvesters, and anyone with a block explorer. No traditional financial system would - accept this level of exposure if it fully understood what it was publishing. -

-
-
+function Eyebrow({ children }: { children: React.ReactNode }) { + return

{children}

; +} -
-
- -
-
-

The opacity trap

-

- Existing privacy tools — mixers, shielded pools, fully opaque coins — solve - transparency by hiding everything. You lose the auditability that makes blockchains - useful in the first place. Counterparties can't verify who they paid. Compliance - becomes impossible. You swap one problem for another. -

-
-
-
-
-
+export default function AboutPage() { + return ( +
+ - {/* The Lucent approach */} -
-
-

- The approach -

-
-
- -
-

- Encrypt the amount. -
- Keep everything else public. -

-

- Lucent takes a surgical approach: addresses stay fully visible on-chain, but amounts are - held as Pedersen commitments and moved with zero-knowledge proofs. You can verify that a - payment was made from wallet A to wallet B — but you cannot read how much was sent. - Counterparties remain auditable. Amounts stay confidential. -

-

- The result: payroll can be distributed without exposing individual salaries, escrow can - be settled without revealing the locked sum to third parties, and payments can be made - without your transaction history becoming a public business-intelligence dataset. -

-
-
-
+ {/* Hero */} +
+ About +

+ Finance has always needed selective privacy. +

+

+ You would not hand your bank statement to a stranger. Yet every payment on a public + blockchain is permanently visible to anyone who looks. Lucent fixes that without hiding who + you paid, only how much. +

+
- {/* Trust model */} -
-
-

- Trust model -

-
-
- -

The math is the trust.

-
-
- {[ - { - title: "No off-chain operators", - desc: "Every proof is generated in your browser and verified natively by Stellar's Protocol 26 host functions — no separate privacy infrastructure to trust.", - }, - { - title: "No operator keys", - desc: "There is no private key that could be stolen to decrypt your balances. The encryption is enforced by mathematics, not by a trusted party.", - }, - { - title: "No bridge custody", - desc: "Your USDC stays in Soroban contracts on Stellar. There is no bridge, no wrapped token, and no custodian holding your funds.", - }, - ].map(({ title, desc }) => ( -
-
-

{title}

-

{desc}

-
- ))} -
-

- Lucent inherits its security guarantees entirely from Stellar's native UltraHonk - verifier — a zero-knowledge proof system verified inside Soroban itself. Encryption is - enforced at the protocol level. No configuration, no trust assumptions, no operator. -

+ {/* Problem / approach */} +
+ {[ + { + label: "The problem", + title: "Everything is exposed, or nothing is auditable.", + body: [ + "Public blockchains expose every transaction to everyone, forever. Salaries, vendor payments and treasury moves are readable by competitors, data harvesters and anyone with a block explorer.", + "Existing privacy tools such as mixers, shielded pools and opaque coins hide everything. You lose the auditability that makes a blockchain useful, and compliance becomes impossible.", + ], + }, + { + label: "The approach", + title: "Encrypt the amount. Keep the rest public.", + body: [ + "Addresses stay visible on-chain. Amounts are held as Pedersen commitments and moved with zero-knowledge proofs. You can verify that wallet A paid wallet B, but not how much.", + "Payroll goes out without exposing salaries. Escrow settles without revealing the locked sum. Your history never becomes a public business-intelligence dataset.", + ], + }, + ].map((c) => ( +
+ {c.label} +

{c.title}

+
+ {c.body.map((p) => ( +

{p}

+ ))}
-
+ ))} +
- {/* Who it is for */} -
-
-

- Who it's for -

-
- {[ - { - icon: Briefcase, - title: "Stellar teams", - desc: "Pay contributors and vendors without publishing your treasury's payment history to competitors.", - }, - { - icon: Users, - title: "DAOs", - desc: "Run payroll with per-person confidential salaries. No member can see another's compensation on-chain.", - }, - { - icon: Globe, - title: "Freelancers", - desc: "Invoice and get paid in USDC without your entire client list and rate card becoming public record.", - }, - { - icon: Shield, - title: "Protocols", - desc: "Need confidential escrow in your settlement layer? Integrate PrivateEscrow directly into your Soroban contracts.", - }, - ].map(({ icon: Icon, title, desc }) => ( -
-
- -
-
-

{title}

-

{desc}

-
-
- ))} -
+ {/* Trust model */} +
+
+
+ Trust model +

Nothing to trust but the math.

-
+
    + {TRUST.map((t, i) => ( +
  1. + {String(i + 1).padStart(2, "0")} +

    {t.title}

    +

    {t.desc}

    +
  2. + ))} +
+
+
- {/* Built on Stellar */} -
-
-

Built on

-
-
- - × -
- USDC -
-
-
-

Lucent is powered by Stellar Soroban

-

- Soroban is Stellar's smart contract platform, and Protocol 26 shipped native ZK - host functions that verify UltraHonk proofs on-chain without a coprocessor. Lucent - builds on OpenZeppelin's confidential-token module — Pedersen commitments, - Poseidon2 hashing, and a Rust contract stack — to bring amount-hidden payments to - Stellar. -

- - Learn about Stellar - -
+ {/* Audience */} +
+ Who it is for +
+ {AUDIENCE.map((a, i) => ( +
0 ? "border-t border-border sm:border-t-0 sm:border-l" : ""} ${i === 2 ? "sm:max-lg:border-t sm:max-lg:border-l-0" : ""}`}> +

{a.title}

+

{a.desc}

-
-
+ ))} +
+
- {/* Footer CTA */} -
-
-

Ready to pay privately?

-

Connect Freighter and deposit your first USDC.

+ {/* Built on */} +
+
+ Built on +

Soroban and Protocol 26.

+
+
+

+ Protocol 26 shipped native ZK host functions that verify UltraHonk proofs on-chain without + a coprocessor. Lucent builds on OpenZeppelin's confidential-token module, Pedersen + commitments, Poseidon2 hashing and a Rust contract stack to bring amount-hidden payments + to Stellar. +

+
- Launch App + Launch app + + OpenZeppelin stellar-contracts + +
-
+
+
- {/* Footer */} - -
+
); } diff --git a/packages/app/app/auditor/page.tsx b/packages/app/app/auditor/page.tsx index 64db819..a97d54a 100644 --- a/packages/app/app/auditor/page.tsx +++ b/packages/app/app/auditor/page.tsx @@ -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 @@ -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"; @@ -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, @@ -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, @@ -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; @@ -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 }) @@ -290,20 +291,15 @@ export default function AuditorPage() {
{hasCompliance && ( -
- {(["decrypt", "compliance"] as Tab[]).map((t) => ( - - ))} -
+ )} {tab === "compliance" && hasCompliance ? ( @@ -312,7 +308,7 @@ export default function AuditorPage() { <>

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.

{auditorSk === null && ( @@ -326,8 +322,8 @@ export default function AuditorPage() { Unlock Console

- Paste your auditor secret key (the k whose - public point K_aud = k·H was + Paste your auditor secret key (the k{" "}whose + public point K_aud = k·H{" "}was registered on-chain) to decrypt with it.

@@ -352,7 +348,7 @@ export default function AuditorPage() { {savedSecrets.map((s) => (

- Console unlocked with the key you pasted in. Only K_aud — - the public point — is shown below; the secret itself is never redisplayed. + Console unlocked with the key you pasted in. Only K_aud, + the public point, is shown below; the secret itself is never redisplayed.

{kAud && (
@@ -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."}

{accounts.length === 0 && !busy && (

No accounts in the retention window.

@@ -462,7 +458,7 @@ export default function AuditorPage() {

Decrypted activity

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.

setError(null)} className="mb-3" size="sm" /> @@ -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. @@ -539,21 +535,21 @@ function CompliancePanel() {

{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.

{!wallet ? ( ) : (
setError(null)} size="sm" /> -
+
Allowlist gate

@@ -649,7 +645,7 @@ function CompliancePanel() { function AuditRowView({ row }: { row: AuditRow }) { const { ev } = row; return ( -

  • +
  • {ev.type} {ev.type === "register" || ev.type === "merge" ? ( diff --git a/packages/app/app/docs/page.tsx b/packages/app/app/docs/page.tsx index 646173d..116dcc3 100644 --- a/packages/app/app/docs/page.tsx +++ b/packages/app/app/docs/page.tsx @@ -4,6 +4,7 @@ import { useState, useEffect } from "react"; import Link from "next/link"; import { ArrowRight, ChevronDown, ChevronRight } from "lucide-react"; import { LucentLogoMark } from "@/components/icons/LucentLogoMark"; +import { MarketingNav } from "@/components/layout/MarketingNav"; import { Callout } from "@/components/ui/Callout"; import { DEPLOYMENT } from "@/lib/deployment"; @@ -23,7 +24,7 @@ const SECTIONS = [ function Code({ children }: { children: string }) { return ( - + {children} ); @@ -31,7 +32,7 @@ function Code({ children }: { children: string }) { function Pre({ children }: { children: string }) { return ( -
    +    
           {children}
         
    ); @@ -39,7 +40,7 @@ function Pre({ children }: { children: string }) { function H2({ id, children }: { id: string; children: React.ReactNode }) { return ( -

    +

    {children}

    ); @@ -87,8 +88,8 @@ export default function DocsPage() { ); // Classic scrollspy: whichever heading's top has most recently scrolled // past a fixed line near the top of the viewport is "current," and it - // stays current through that entire section's body — regardless of how - // long the section is — until the next heading also passes the line. + // stays current through that entire section's body, regardless of how + // long the section is, until the next heading also passes the line. // (An IntersectionObserver "visible band" can't express this: once a // heading's own line scrolls out of a thin band, the observer drops it, // so long sections go dark for most of their own body.) @@ -100,7 +101,7 @@ export default function DocsPage() { let current = headings[0]?.id; for (const el of headings) { if (el.getBoundingClientRect().top <= THRESHOLD) current = el.id; - else break; // headings are in document order — none after this have passed either + else break; // headings are in document order, none after this have passed either } if (current) setActiveId(current); } @@ -130,25 +131,10 @@ export default function DocsPage() { return (
    - + -
    -

    Withdraw

    - Withdrawing is a two-step, in-browser flow: a withdraw proof is generated locally (this - takes a few seconds — you're proving "I can open this commitment to a value at + Withdrawing is a two-step, in-browser flow: a withdraw proof is generated locally (this takes a few seconds; you're proving "I can open this commitment to a value at most my balance" without revealing the balance itself), then submitted on-chain. The Soroban verifier checks the proof and converts your spendable balance back into public USDC.

    @@ -279,35 +260,31 @@ transfer proof Spendable -> another account's receiving balance`}

    Private Payments

    Go to Send, choose a registered recipient, and enter an amount. A transfer - proof is generated in your browser and submitted with the transaction — the recipient - address is visible on-chain; the amount is not. + proof is generated in your browser and submitted with the transaction. The recipient address is visible on-chain; the amount is not.

    Why only registered recipients

    Building the transfer proof requires the recipient's public viewing key ( PVK), which only exists on-chain once they've completed the one-time - register step themselves. Send only lists addresses the app has already seen register — an - address with no PVK has nowhere for the proof to encrypt an amount into. + register step themselves. Send only lists addresses the app has already seen register, because an address with no PVK{" "}has nowhere for the proof to encrypt an amount into.

    What the proof actually proves

    - confidential_transfer proves, without revealing any of the three numbers + confidential_transfer{" "}proves, without revealing any of the three numbers involved: "I know the opening of my spendable commitment, it covers at least this - amount, and here are two new commitments — one for what I'm sending and one for what + amount, and here are two new commitments, one for what I'm sending and one for what stays in my own balance." The proof also seals the amount into two ciphertexts, one per auditor channel (sender's and recipient's), so a registered auditor can decrypt it later even though no one else can.

    - The sent amount lands in the recipient's receiving balance, not spendable — same as a - deposit, they merge it in whenever they like. + The sent amount lands in the recipient's receiving balance, not spendable, same as a deposit. They merge it in whenever they like.

    Confidential Payroll

    - PayrollVault is an orchestrator, not a custodian: an employer creates a - template of employees and opens a run. Salaries never touch chain storage — at execution - the employer's browser proves one confidential transfer per employee, and the vault + PayrollVault{" "}is an orchestrator, not a custodian: an employer creates a + template of employees and opens a run. Salaries never touch chain storage. At execution, the employer's browser proves one confidential transfer per employee, and the vault routes them atomically. No employee can read another's salary, and the vault itself never sees a plaintext amount either.

    @@ -319,41 +296,34 @@ cancel_run(run_id) claim() // employee folds salary into spendable`}

    Employer flow

    - Create a template once with the employee list — templates carry no - amounts, just who gets paid. Create a run against it to open a{" "} - Scheduled run. Confirm the run (fund_run on-chain - — the app calls it "Confirm" since it doesn't actually move anything) records that the - employer's confidential spendable balance covers the total — a state marker, not a - transfer, since the vault can't read an encrypted balance to lock anything against it. + Create a template{" "}once with the employee list. Templates carry no amounts, just who gets paid. Create a run{" "}against it to open a{" "} + Scheduled{" "}run. Confirm the run{" "}(fund_run{" "}on-chain; the app calls it "Confirm" since it doesn't actually move anything) records that the + employer's confidential spendable balance covers the total. It is a state marker, not a transfer, since the vault can't read an encrypted balance to lock anything against it.

    - Execute the run is where the money actually moves: the employer enters each - salary, and the browser builds one transfer proof per employee, chained — employee 2's - proof spends the balance opening left over after employee 1's, and so on, so the whole + Execute the run{" "}is where the money actually moves: the employer enters each + salary, and the browser builds one transfer proof per employee, chained: employee 2's proof spends the balance opening left over after employee 1's, and so on, so the whole batch is internally consistent. All proofs submit in one transaction; the vault runs every - transfer and only marks the run Executed if all of them succeed — no partial - payroll runs. A Scheduled run can be cancelled any time before execution. + transfer and only marks the run Executed{" "}if all of them succeed, so there are no partial payroll runs. A Scheduled{" "}run can be cancelled any time before execution.

    Employee flow

    A paid employee sees the salary appear in their receiving balance and merges it into - spendable with Claim, same merge as anywhere else — no proof - needed. + spendable with Claim. It is the same merge{" "}as anywhere else and needs no proof.

    Compliance

    Every salary transfer is an ordinary confidential_transfer, so it carries the same dual auditor ciphertexts as a Send. Register the employer as the deployment's auditor and every salary they've ever paid becomes decryptable to them on the Auditor - screen — while each employee still only ever sees their own. + screen, while each employee still only ever sees their own.

    Encrypted Escrow

    - PrivateEscrow is custodial, unlike Payroll: the funds actually need to sit + PrivateEscrow{" "}is custodial, unlike Payroll: the funds actually need to sit somewhere confidential between funding and release, and a confidential balance only exists - at a contract address, so each escrow deploys its own tiny instance contract just to hold - it — its own confidential account, isolated from every other escrow's. + at a contract address, so each escrow deploys its own tiny instance contract just to hold it. Each instance is its own confidential account, isolated from every other escrow's.

    {`Depositor creates -> funds escrow      (CREATED -> FUNDED)
     Recipient delivers -> mark_completed   (FUNDED -> COMPLETED)
    @@ -366,30 +336,29 @@ If depositor stalls past the release window:
     No delivery -> depositor waits for timeout -> reclaims (REFUNDED)`}

    The two clocks

    - Two independent, non-overlapping timers gate the escrow — one per failure mode: + Two independent, non-overlapping timers gate the escrow, one per failure mode:

    - Timeout protects the depositor if the recipient never engages at all. Chosen - per-escrow at creation — 1h, 24h, 7d, or{" "} - 30d — and only live while still Funded: once the recipient marks + Timeout{" "}protects the depositor if the recipient never engages at all. Chosen + per-escrow at creation, 1h, 24h, 7d, or{" "} + 30d, and only live while still Funded: once the recipient marks delivery, the timeout path is permanently blocked, so a stale timeout can never undercut a delivery that's already been confirmed.

    - Release window protects the recipient if the depositor stalls after delivery - is confirmed. Fixed at 10 minutes from mark_completed, not - configurable per-escrow. Both deadlines are enforced by the contract itself, not the app — - calling an action before its deadline simply reverts on-chain. The Escrow screen shows a live + Release window{" "}protects the recipient if the depositor stalls after delivery + is confirmed. Fixed at 10 minutes{" "}from mark_completed, not + configurable per-escrow. Both deadlines are enforced by the contract itself, not the app: calling an action before its deadline simply reverts on-chain. The Escrow screen shows a live countdown to whichever deadline currently applies, and disables the corresponding action (Timeout Refund / Claim / Dispute) until it actually opens, so there's no guessing at when a call will succeed.

    Funding and payout validation

    - Funding is the expensive step, proof-wise — the depositor derives a one-time Grumpkin + Funding is the expensive step, proof-wise. The depositor derives a one-time Grumpkin identity for the instance itself and builds four proofs: a register proof for that identity, - a transfer-in proof (depositor → instance), and — because the instance can never generate a - proof on its own later — both possible payout proofs up front, instance → recipient and + a transfer-in proof (depositor → instance), and, because the instance can never generate a + proof on its own later, both possible payout proofs up front, instance → recipient and instance → depositor. At roughly 14KB each, all four together exceed what fits in one Soroban transaction. Four wallet confirmations prepare the inputs, verify each payout, and fund:

    @@ -398,79 +367,67 @@ store_payout_proof(true, release_proof) // verifies recipient payout store_payout_proof(false, refund_proof) // verifies depositor refund fund() // atomically registers, transfers and merges`}

    - fund requires both payout proofs to have passed verification against the exact + fund{" "}requires both payout proofs to have passed verification against the exact prepared funding commitment. Replacing the funding inputs clears both approvals. Funds - move only in the last call; a rejected proof leaves the escrow unfunded. Once funding completes, the depositor discards the instance's one-time secret — - see the trust caveat below. + move only in the last call; a rejected proof leaves the escrow unfunded. Once funding completes, the depositor discards the instance's one-time secret, as described in the trust caveat below.

    Settlement

    Every payout proof was generated once, at funding time, against a fixed opening. That's what lets the instance submit one of them unattended, whenever the state machine says so. Funding merges once, atomically; settlement never merges later deposits, so unsolicited - credits cannot change that fixed opening. No - one needs to be online or hold a key at settlement time. On the happy path the recipient + credits cannot change that fixed opening. No one needs to be online or hold a key at settlement time. On the happy path the recipient marks delivery, a 10-minute release window opens, and the depositor releases. If the - depositor stalls, the recipient either self-serves after the window (no arbiter) or escalates - to a dispute (arbiter configured) — the arbiter resolves by state transition alone and never + depositor stalls, the recipient either self-serves after the window (no arbiter) or escalates to a dispute (arbiter configured). The arbiter resolves by state transition alone and never sees the amount.

    Trust caveat

    To pre-generate the two payout proofs, the depositor must derive the instance's Grumpkin - secret at fund time. They're expected to discard it immediately afterward — a depositor - who keeps it could re-spend the escrowed balance and invalidate both stored proofs. + secret at fund time. They're expected to discard it immediately afterward. A depositor who keeps it could re-spend the escrowed balance and invalidate both stored proofs. Acceptable for a testnet demo; not a production-grade custody model.

    Selective Disclosure

    - The Auditor screen gives one party standing access to everything under an auditor id — the - right tool for a compliance relationship, wrong for a one-off. Selective disclosure answers - a narrower need: proving one specific payment to someone who shouldn't get a - decrypt key to your whole history — an accountant who needs one receipt, a landlord who - wants proof of one rent payment. It's a proof, generated and verified entirely - off-chain — there's no on-chain disclosure verifier, the chain is only ever read from, - never written to, for this flow. + The Auditor screen gives one party standing access to everything under an auditor id. That is the right tool for a compliance relationship and the wrong one for a one-off. Selective disclosure answers + a narrower need: proving one specific payment{" "}to someone who shouldn't get a decrypt key to your whole history (an accountant who needs one receipt, a landlord who wants proof of one rent payment). It's a proof, generated and verified entirely + off-chain. There is no on-chain disclosure verifier, and the chain is only ever read from, never written to, for this flow.

    Two claims

    - D-recipient — "this on-chain transfer paid me exactly this + D-recipient:{" "}"this on-chain transfer paid me exactly this amount." Available for anything you received; you can already decrypt it with your own viewing key.

    - D-sender — "I sent this on-chain transfer for exactly this + D-sender:{" "}"I sent this on-chain transfer for exactly this amount." This one needs more: you re-derive the one-time ephemeral scalar your wallet used at send time from data still on the event itself, then prove you can reconstruct what the recipient decrypted. If that scalar isn't recoverable (old local state, a different - device), the transfer isn't disclosable as a sender — your Home activity feed marks it - accordingly. + device), the transfer isn't disclosable as a sender, and your Home activity feed marks it accordingly.

    How it works

    - On the Verify screen (no wallet required — this can be anyone, even someone - with no Stellar account at all), click "Create request": a fresh public key and + On the Verify{" "}screen (no wallet required, so this can be anyone, even someone with no Stellar account at all), click "Create request": a fresh public key and nonce, generated locally, that binds whatever proof comes back to this request specifically so it can't be replayed against someone else. Copy the resulting JSON and send it to the holder however you'd normally share a file.

    The holder pastes that JSON against the relevant transfer right in their{" "} - Home activity feed — a "Prove" action sits next to any transfer - that's disclosable. The browser generates a zero-knowledge proof bound to it — a few - seconds of in-browser proving — and produces a bundle to copy back. Paste the bundle into + Home{" "}activity feed, where a "Prove" action sits next to any transfer that's disclosable. The browser generates a zero-knowledge proof bound to it (a few seconds of in-browser proving) and produces a bundle to copy back. Paste the bundle into the Verify screen to check it.

    Why the verifier doesn't have to trust the bundle

    The verifier never takes the holder's word for anything except the proof and one sealed ciphertext. It doesn't trust the bundle for who sent the transfer, who received it, or - what the transaction even was — it re-reads the actual event from the chain by the + what the transaction even was. It re-reads the actual event from the chain by the bundle's event reference, re-reads the relevant account's public viewing key from the token contract, and re-derives every other public input from that independently-fetched chain state. Only then does it check the proof, and only after the proof checks out does it - decrypt the sealed amount with the verifier's own secret key — never the holder's. + decrypt the sealed amount with the verifier's own secret key, never the holder's. The circuit's range and binding constraints mean a holder can't get a false amount past the proof by construction.

    @@ -478,9 +435,8 @@ fund() // atomically registers, transfers and

    Auditor View Key

    Every account registers under an auditor id. The holder of that Grumpkin secret key can - decrypt every transfer amount and balance checkpoint on the Auditor screen — - the institutional compliance primitive. Nobody else can. The screen needs no wallet: paste - the secret into the unlock field, and it stays in page state only — never written to local + decrypt every transfer amount and balance checkpoint on the Auditor{" "}screen. This is the institutional compliance primitive, and nobody else can. The screen needs no wallet: paste + the secret into the unlock field, and it stays in page state only, never written to local storage, never sent anywhere, gone the moment you navigate away or reload. The app's shipped bundle does not contain a working default; that's deliberate, so no one gets auditor access just by loading the page. @@ -491,10 +447,10 @@ fund() // atomically registers, transfers and from the sender-channel checkpoints, receiving balance as the sum of decrypted inbound transfers plus public deposits, folded on merge.

    -

    Try it — demo key (testnet only)

    +

    Try it with the demo key (testnet only)

    - This deployment's registered auditor id 0 key, so you can try the console - without deploying your own stack. Paste the secret value into the Auditor + This deployment's registered auditor id 0{" "}key, so you can try the console + without deploying your own stack. Paste the secret{" "}value into the Auditor screen's unlock field.
    {`secret:  0x002db306c1ef9309a478294fc1f14b2e9b112052d3d5b8ce7e5c0f1c69bb2804
    @@ -502,8 +458,7 @@ K_aud.x: 0x2414ea8f68ff9c6ab9c49a5113cbee002c5646fcea36757e8108b1edcf82d4d0
     K_aud.y: 0x0ad237715304822d5c532c31a1afbd83b690f34c8e5af01bd9b47b3271e5ac4d`}

    This key decrypts every confidential balance and transfer registered under auditor id 0 on - this specific testnet deployment. There's nothing sensitive behind it — testnet, no real - value, and the whole point of this screen is that this is what an auditor is meant to see — + this specific testnet deployment. There's nothing sensitive behind it: it is testnet with no real value, and the whole point of this screen is that this is what an auditor is meant to see, but it is real key material for a live deployment, not a placeholder. It stops applying the moment this stack is redeployed (a fresh random key is generated each time); check the Contracts section below for which deployment is current. @@ -514,25 +469,23 @@ K_aud.y: 0x0ad237715304822d5c532c31a1afbd83b690f34c8e5af01bd9b47b3271e5ac4d`}

    allowlist policy{" "} and per-account freezing. Both are gated behind a single compliance admin - address set at deploy time — nobody else can call either, and the contract enforces that + address set at deploy time. Nobody else can call either, and the contract enforces that itself rather than relying on the app to hide the buttons.

    - Allowlist. A separate policy contract holds a simple allow/deny registry. - When wired onto the token, every deposit, transfer, receive, and withdraw checks the policy - first — an account not on the list is rejected on-chain, before any amount is touched. This + Allowlist.{" "}A separate policy contract holds a simple allow/deny registry. + When wired onto the token, every deposit, transfer, receive, and withdraw checks the policy first, and an account not on the list is rejected on-chain before any amount is touched. This is the primitive an institution needs for KYC-gated payroll: onboard employees onto the allowlist once, and every payroll run after that is automatically restricted to verified accounts, with no per-transaction compliance check outside the contract itself.

    - Freezing. Independent of the allowlist, the compliance admin can freeze a - specific account outright — it can no longer deposit, transfer, receive, or withdraw until + Freezing.{" "}Independent of the allowlist, the compliance admin can freeze a specific account outright. It can then no longer deposit, transfer, receive, or withdraw until unfrozen, regardless of allowlist status. Useful for responding to a flagged account without having to touch the broader allowlist.

    - Both are managed from the Auditor screen's Compliance panel: connect + Both are managed from the Auditor{" "}screen's Compliance panel: connect the compliance-admin's wallet, paste an address, and allow/remove or freeze/unfreeze it. The panel also shows the live allowlist/frozen status for any address you check.

    @@ -549,7 +502,7 @@ K_aud.y: 0x0ad237715304822d5c532c31a1afbd83b690f34c8e5af01bd9b47b3271e5ac4d`}

    OpenZeppelin/stellar-contracts
    .

    -
    +
    {Object.entries(addresses).map(([name, addr]) => ( ))} @@ -559,7 +512,7 @@ K_aud.y: 0x0ad237715304822d5c532c31a1afbd83b690f34c8e5af01bd9b47b3271e5ac4d`}

    Can anyone see my balance?

    No. Your balance is a Pedersen commitment on-chain. The plaintext openings live only in - events, reconstructed and persisted locally by your browser's state engine — never + events, reconstructed and persisted locally by your browser's state engine, never broadcast in the clear.

    Does the escrow arbiter see the locked amount?

    @@ -567,15 +520,14 @@ K_aud.y: 0x0ad237715304822d5c532c31a1afbd83b690f34c8e5af01bd9b47b3271e5ac4d`}

    -

    Auditor console vs. selective disclosure — which one do I want?

    +

    Auditor console or selective disclosure: which do I want?

    - Auditor if you need standing, ongoing visibility into every amount under an auditor id — the - compliance relationship Payroll is built around. Selective disclosure if you need to prove - just one payment to one party who shouldn't get broader access — see{" "} + Auditor if you need standing, ongoing visibility into every amount under an auditor id, which is the compliance relationship Payroll is built around. Selective disclosure if you need to prove + just one payment to one party who shouldn't get broader access (see{" "} Selective Disclosure {" "} - above. + above).

    Is Lucent audited?

    @@ -584,7 +536,7 @@ K_aud.y: 0x0ad237715304822d5c532c31a1afbd83b690f34c8e5af01bd9b47b3271e5ac4d`}

    What happens if I lose my wallet?

    - Confidential keys are derived deterministically from a Freighter signature, so they can be + Confidential keys are derived deterministically from a wallet signature, so they can be re-derived from the same wallet. Local balance history persisted in your browser is not recoverable if lost, but re-syncing from chain events reconstructs it within the RPC's ~7-day retention window. diff --git a/packages/app/app/escrow/page.tsx b/packages/app/app/escrow/page.tsx index 279f558..8c24e57 100644 --- a/packages/app/app/escrow/page.tsx +++ b/packages/app/app/escrow/page.tsx @@ -24,6 +24,7 @@ import { DEPLOYMENT } from "@/lib/deployment"; import { ESCROW_STATE_LABEL, formatCountdown } from "@/lib/format"; import { cn } from "@/lib/cn"; import { ErrorBanner } from "@/components/ui/ErrorBanner"; +import { Segmented } from "@/components/ui/Segmented"; type Row = { id: bigint; address: string; info: EscrowInfo }; type Filter = "all" | "depositor" | "recipient" | "arbiter"; @@ -55,7 +56,7 @@ const TIMEOUT_OPTIONS = [ /** * Time the depositor has to `release()` after the recipient marks delivery, * before the recipient can claim (no arbiter) or dispute (arbiter set). - * Matches `RELEASE_WINDOW` in contracts/escrow-instance/src/lib.rs — fixed by + * Matches `RELEASE_WINDOW` in contracts/escrow-instance/src/lib.rs, fixed by * the contract, not configurable per-escrow the way the timeout is. */ const RELEASE_WINDOW_SECONDS = 600; @@ -171,6 +172,7 @@ export default function EscrowPage() { setShowCreate(true)}> @@ -184,25 +186,22 @@ export default function EscrowPage() { {!registered && ( - You need to register your confidential account before you can create an escrow — head to{" "} - Home to register, a one-time proof. + You need to register your confidential account before you can create an escrow, head to{" "} + Home{" "}to register, a one-time proof. )} -

    - {(["all", "depositor", "recipient", "arbiter"] as Filter[]).map((f) => ( - - ))} -
    + {rows === null ? (

    Loading…

    @@ -240,21 +239,21 @@ export default function EscrowPage() {

    - Without an arbiter the recipient has no on-chain recourse if you withhold payment. + Without an arbiter the recipient has no on-chain recourse{" "}if you withhold payment.

    )}
    - Timeout — depositor reclaims if no delivery + Timeout, depositor reclaims if no delivery
    {TIMEOUT_OPTIONS.map((o, i) => ( @@ -278,7 +277,7 @@ export default function EscrowPage() { />

    Funding generates a register proof for the escrow's confidential account, plus the - transfer-in and two payout proofs — all in your browser. Four wallet confirmations + transfer-in and two payout proofs, all in your browser. Four wallet confirmations prepare funding, verify both payouts, and finally move the funds.

    @@ -397,13 +400,13 @@ function EscrowRow({ ) : (
    - No arbiter — disputes not possible + No arbiter, disputes not possible
    )}
    {stateNum === EscrowState.Funded && ( -
    +
    Depositor can self-refund in {timeoutReached ? "Available now" : formatCountdown(timeoutAt)} @@ -411,7 +414,7 @@ function EscrowRow({
    )} {stateNum === EscrowState.Completed && ( -
    +
    {hasArbiter ? "Dispute opens in" : "Auto-claim opens in"} diff --git a/packages/app/app/globals.css b/packages/app/app/globals.css index 0536ca5..20797e8 100644 --- a/packages/app/app/globals.css +++ b/packages/app/app/globals.css @@ -1,88 +1,149 @@ -@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@400;500;600&family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap'); @tailwind base; @tailwind components; @tailwind utilities; +/* ───────────────────────── Design tokens ───────────────────────── + RGB triplets so Tailwind can apply alpha. Dark is the default; the + `data-theme` attribute (set before paint by the inline script in + layout.tsx) switches to light. */ +:root, +:root[data-theme='dark'] { + color-scheme: dark; + --bg: 8 8 10; + --surface: 13 13 16; + --surface-2: 19 19 23; + --card: 24 24 29; + --card-hover: 34 34 40; + --panel: 17 17 21; + --fg: 255 255 255; + --text-1: 250 250 250; + --text-2: 170 170 180; + --text-3: 128 128 140; + --accent: 251 191 36; + --accent-hover: 252 211 77; + --accent-solid: 251 191 36; + --accent-solid-hover: 252 211 77; + --on-accent: 20 14 0; + --encrypted: 167 139 250; + --success: 52 211 153; + --error: 248 113 113; + --warning: 245 158 11; + --info: 56 189 248; + --shadow-card: 0 1px 0 rgb(255 255 255 / 0.04) inset, 0 8px 28px rgb(0 0 0 / 0.45); + --shadow-pop: 0 24px 64px rgb(0 0 0 / 0.65); + --shadow-accent: 0 6px 24px rgb(251 191 36 / 0.22); + --scrim: 0 0 0 / 0.65; +} + +:root[data-theme='light'] { + color-scheme: light; + --bg: 246 245 241; + --surface: 255 255 255; + --surface-2: 250 249 246; + --card: 241 240 235; + --card-hover: 232 231 225; + --panel: 255 255 255; + --fg: 24 21 14; + --text-1: 24 21 14; + --text-2: 78 76 70; + --text-3: 112 109 100; + --accent: 146 84 0; + --accent-hover: 115 66 0; + --accent-solid: 245 175 20; + --accent-solid-hover: 232 160 0; + --on-accent: 28 18 0; + --encrypted: 104 70 222; + --success: 4 120 87; + --error: 211 47 47; + --warning: 180 83 9; + --info: 2 118 177; + --shadow-card: 0 1px 2px rgb(40 30 0 / 0.05), 0 8px 24px rgb(40 30 0 / 0.06); + --shadow-pop: 0 24px 64px rgb(40 30 0 / 0.2); + --shadow-accent: 0 6px 20px rgb(245 175 20 / 0.35); + --scrim: 24 21 14 / 0.4; +} + @layer base { * { box-sizing: border-box; margin: 0; padding: 0; } + html { background: rgb(var(--bg)); } + body { - background: #080808; - color: #FFFFFF; - font-family: 'Space Grotesk', system-ui, sans-serif; + background: rgb(var(--bg)); + color: rgb(var(--text-1)); + font-family: 'Inter', system-ui, sans-serif; + font-feature-settings: 'cv11', 'ss01'; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; min-height: 100dvh; overscroll-behavior-y: contain; -webkit-tap-highlight-color: transparent; + transition: background-color 0.25s ease, color 0.25s ease; } - /* Grain overlay */ - body::before { - content: ''; - position: fixed; - inset: -200%; - width: 400%; height: 400%; - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); - background-size: 128px 128px; - pointer-events: none; - z-index: 9999; - opacity: 0.032; - animation: grain 8s steps(10) infinite; + ::selection { background: rgb(var(--accent-solid) / 0.35); } + ::-webkit-scrollbar { width: 8px; height: 8px; } + ::-webkit-scrollbar-track { background: transparent; } + ::-webkit-scrollbar-thumb { background: rgb(var(--fg) / 0.14); border-radius: 8px; } + ::-webkit-scrollbar-thumb:hover { background: rgb(var(--fg) / 0.25); } + + :focus-visible { + outline: 2px solid rgb(var(--accent-solid)); + outline-offset: 2px; + border-radius: 8px; } - ::selection { background: rgba(251,187,36,0.3); color: #fff; } - ::-webkit-scrollbar { width: 4px; height: 4px; } - ::-webkit-scrollbar-track { background: transparent; } - ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 4px; } + @media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } + } } @layer utilities { .tabular-nums { font-variant-numeric: tabular-nums; } .text-balance { text-wrap: balance; } - .scrollbar-hide { - -ms-overflow-style: none; - scrollbar-width: none; - } - .scrollbar-hide::-webkit-scrollbar { - display: none; - } + .scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; } + .scrollbar-hide::-webkit-scrollbar { display: none; } - .pb-safe { - padding-bottom: max(1rem, env(safe-area-inset-bottom)); - } + .pb-safe { padding-bottom: max(0.75rem, env(safe-area-inset-bottom)); } .encrypted-value { filter: blur(6px); transition: filter 0.7s cubic-bezier(0.4,0,0.2,1); user-select: none; - color: #A78BFA; + color: rgb(var(--encrypted)); letter-spacing: 0.1em; } .encrypted-value.revealed { filter: blur(0); - color: #FFFFFF; + color: rgb(var(--text-1)); letter-spacing: -0.02em; } + /* Surface used for every panel in the app. Name kept for compatibility. */ .glass-card { - background: rgba(24,24,27,0.80); - backdrop-filter: blur(24px); - -webkit-backdrop-filter: blur(24px); - border: 1px solid rgba(255,255,255,0.08); - border-radius: 20px; - box-shadow: 0 4px 24px rgba(0,0,0,0.6), 0 1px 0 rgba(255,255,255,0.04) inset; - transition: border-color 0.2s ease, box-shadow 0.2s ease; - } - .glass-card:hover { - border-color: rgba(255,255,255,0.14); - box-shadow: 0 8px 32px rgba(0,0,0,0.7), 0 1px 0 rgba(255,255,255,0.06) inset; + background: rgb(var(--panel)); + border: 1px solid rgb(var(--fg) / 0.09); + border-radius: 14px; + box-shadow: var(--shadow-card); + transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.25s ease; } .glow-divider { height: 1px; - background: linear-gradient(90deg, transparent, rgba(251,187,36,0.5), transparent); + background: linear-gradient(90deg, transparent, rgb(var(--accent-solid) / 0.5), transparent); + } + + /* Soft brand wash behind heroes / page tops. */ + .brand-wash { + background: + radial-gradient(60% 50% at 15% 0%, rgb(var(--accent-solid) / 0.12), transparent 70%), + radial-gradient(50% 45% at 90% 10%, rgb(var(--encrypted) / 0.12), transparent 70%); } } diff --git a/packages/app/app/home/disclose-flow.tsx b/packages/app/app/home/disclose-flow.tsx index 0ba4f01..96336f0 100644 --- a/packages/app/app/home/disclose-flow.tsx +++ b/packages/app/app/home/disclose-flow.tsx @@ -5,7 +5,7 @@ * inline to a transfer's activity row: paste a verifier's request (P_R, ν), * generate a D-recipient or D-sender proof in-browser, copy the bundle back. * No amount ever goes on-chain. Moved here from a standalone /prove tab so - * disclosing lives right next to the transfer it's about — /verify is now + * disclosing lives right next to the transfer it's about, /verify is now * the verifier-only side of this exchange. */ @@ -69,7 +69,7 @@ export function DiscloseFlow({ {direction === "received" ? "Prove this transfer paid you its exact amount." : "Prove you sent this transfer and what it paid the recipient."}{" "} - Paste the verifier's request (their pR key and one-time nu). + Paste the verifier's request (their pR{" "}key and one-time nu).