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
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ execution are disabled and unauthorized.**
Three surfaces form one workflow:

1. **RFQ** — collateralized, bucket-only private USDC↔STRK requests and atomic localnet Take
2. **Mailbox** — encrypted correspondence, structured evidence, invoice completion, and backup recovery
3. **Counterparties** — a device-encrypted directory with RFQ and Mail handoffs
2. **Chat** — encrypted correspondence one counterparty at a time: letters, structured evidence, invoice completion, and backup recovery
3. **Counterparties** — a device-encrypted directory with RFQ and Chat handoffs

Multi-maker requests stay in the RFQ workspace; Chat holds the correspondence
around them and is the only mail surface. The former Mailbox route redirects to it.

Users connect once in the header. The Ready Wallet API path does not expose a
viewing key to APP20; the optional Privy browser-owned SDK derives/holds its
Expand All @@ -34,15 +37,15 @@ origin is live. This is not a Mainnet value-moving release.
| Surface | Route | Current scope |
| --------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Private RFQ | `/rfq` | Two localnet fixture makers, collateralized quotes, browser selection, and atomic Take. No production maker network. |
| Mailbox | `/mail/inbox` | Encrypted correspondence, payment handoffs, and self-backups. Evidence and recovery transport, not settlement authority. |
| Counterparties | `/contacts` | Device-encrypted labels and addresses with RFQ/Mail handoffs and optional encrypted backup. |
| Chat | `/chat` (`/mail/inbox` redirects here) | Encrypted correspondence one counterparty at a time: letters, offers, invoices, escrows, payment handoffs, and self-backups, beside that contact's open RFQs. Evidence and recovery transport, not settlement authority. |
| Counterparties | `/contacts` | Device-encrypted labels and addresses with RFQ/Chat handoffs and optional encrypted backup. |
| Separate tools | `/funding`, `/send`, `/cross-chain-review`, `/recovery/privy`, `/pay` | Funding readiness, unavailable public send, dry cross-chain review, Privy recovery, and unsigned payment links. |
| Read-only views | `/rfq/operations`, `/rfq/markets/:tokenA/:tokenB/proposal`, `/swap/:tokenA/:tokenB` | Operations status, proposal-only market planning, and non-executable pair handoffs. |

`/pay` is a Mail helper, not a separate settlement product. It only creates an unsigned
payment-request link. Nothing is sent until the payer confirms in Mail.
payment-request link. Nothing is sent until the payer confirms in Chat.

### RFQ, Mailbox, and Counterparties
### RFQ, Chat, and Counterparties

Within the localnet fixture, only Cairo plus pool-applied chain state can confirm
an RFQ lifecycle. Same-devnet confirmation is not production configured-chain
Expand Down Expand Up @@ -77,7 +80,7 @@ Wallet policy is enforced below the UI:
- **Sepolia** — Ready, plus an optional Privy browser signer
- **Localnet** — build-gated development wallet

In the product, mail is **Mailbox** / **Mail**. This pre-release namespace
In the product, encrypted mail is **Chat** / **Mail**. This pre-release namespace
reset renames the active contracts, storage keys, cryptographic domains,
environment variables, and localnet paths to APP20. Pre-reset browser data,
Mail ciphertext, signed payment links, and contract artifacts are not compatible
Expand All @@ -104,7 +107,7 @@ flowchart TD
M --> V[Funding utilities, public send unavailable]
S --> V
LD --> D[Private RFQ]
W -. UI view only, live Mail action denied .-> MB[Mailbox and Counterparties, localnet actions only]
W -. UI view only, live Mail action denied .-> MB[Chat and Counterparties, localnet actions only]
L --> MB

D -. no automatic fallback .-> X[Public venue]
Expand Down
2 changes: 1 addition & 1 deletion STRK20_INTEGRATION_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ Historical status at that checkpoint: code-complete locally, with no helper depl

| File | Landed behavior |
| --- | --- |
| `src/app/routes.ts` | Defines the current canonical `/mail/inbox` route and legacy redirects |
| `src/app/routes.ts` | Defines the canonical `/chat` route and the legacy redirects, `/mail/inbox` included |
| `src/app/inbox/page.tsx` | Network-aware onboarding, compose, public-event scan, and newest-first local plaintext list |
| `src/components/mail/Onboard.tsx` | Locally persisted random device seed, one-time backup display, and public `register_pubkey` transaction |
| `src/components/mail/Compose.tsx` | Recipient directory lookup, local encryption, optional private STRK attachment, and STRK20 submission |
Expand Down
21 changes: 17 additions & 4 deletions docs/APP20_PRIVATE_DESK.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,21 @@
APP20 joins three surfaces into one workflow:

1. **RFQ** — inventory-backed private USDC↔STRK quote and settlement.
2. **Mailbox** — encrypted correspondence, authenticated backup, and non-authoritative evidence.
3. **Counterparties** — a device-encrypted address book with RFQ and Mail handoffs.
2. **Chat** — encrypted correspondence one counterparty at a time, authenticated backup, and non-authoritative evidence.
3. **Counterparties** — a device-encrypted address book with RFQ and Chat handoffs.

**Chat** (`/chat`) is the mailbox: every encrypted record this wallet holds, grouped by
counterparty, with letters, offers, invoices, and escrows beside that contact's open RFQs,
pending payments, and escrows. Accept, receipt, pay, and escrow actions sit on the cards
themselves; key setup, chain scans, drafts, self-backups, and device safety live in its
mailbox tools. Multi-maker requests stay in the RFQ workspace. The former `/mail/inbox`
route redirects to `/chat`.

MessagePosted carries no sender, so a decrypted record joins a counterparty's
conversation only through evidence this device holds: a name assigned on this device,
the offerer, requester or maker a payload claims, the thread a single-recipient Sent copy
opened, or the deal, request or escrow id a memo answers. Anything else shows as a
sealed sender until it is named; the name is a local label, never authentication.

APP20 is a bookless invited-maker venue using the existing STRK20 privacy pool. It does not create a pool, AMM, public order book, or automatic public fallback. See [`GAPS.md`](GAPS.md) for open engineering work.

Expand Down Expand Up @@ -71,7 +84,7 @@ The localnet desk renders the maturity estimate and verified maker-mid context.

Mail accepts a registry-resolved localnet USDC payment request as well as STRK. For USDC, **Pay privately with STRK** stores an account/chain-scoped, five-minute invoice handoff and opens `/rfq`. The v3 invoice model estimates a STRK bucket from the verified maker median with a 2% buffer, then finds the minimum selected schedule allocations whose USDC output reaches the invoice amount.

The localnet desk consumes the scoped handoff, requires a fresh verified maker median for preliminary sizing, minimizes the sell amount against verified schedules, and enforces the exact invoice amount as its floor. After a confirmed Take, it binds the request to the Take hash, Take block, USDC token, and exact invoice amount in state `awaiting-note-maturity`, then returns to Mail. At `takeBlock + 10`, Mail can reserve and submit the existing private memo-transfer path in USDC, then move through submitted to confirmed.
The localnet desk consumes the scoped handoff, requires a fresh verified maker median for preliminary sizing, minimizes the sell amount against verified schedules, and enforces the exact invoice amount as its floor. After a confirmed Take, it binds the request to the Take hash, Take block, USDC token, and exact invoice amount in state `awaiting-note-maturity`, then returns to Chat. At `takeBlock + 10`, Chat can reserve and submit the existing private memo-transfer path in USDC, then move through submitted to confirmed.

## Chain-anchored encrypted backup

Expand All @@ -89,7 +102,7 @@ Restore authenticates every candidate before sequence ranking. Authenticated sam

RFQ history payload v2 carries bounded authenticated portable tombstones as well as records. Forget wins over any included record for the same lifecycle id, including on a fresh database, and the tombstone is persisted before the row is removed. Export recursively strips signing-key fields. Every restored v3 row is durably marked `restoredFromBackup`, local, and non-authoritative; it has no `takerSigningKey`, cannot submit Take, and remains verify-only through reload, re-export, and re-import. A fresh device that receives only an old snapshot which predates a deletion cannot infer the missing newer tombstone, so operators must preserve the newest authenticated self-backup.

The localnet IPFS emulator binds only to `127.0.0.1:5054` and stores blocks in an in-memory `Map`; restart loses them. On a configured production deployment, the IPFS RPC service would learn the source network metadata, upload timing, CID, and padded ciphertext size; gateways would learn fetch timing and requested CID. They receive encrypted padded blob bytes, not snapshot plaintext. The APP20 relay only permits reviewed origins in CSP and does not proxy or pin blobs itself. Production blob storage fails closed unless both browser IPFS origin settings and matching relay `IPFS_ORIGINS` are configured. A settled Take queues the opt-in RFQ auto-backup for the next unlocked Mailbox session.
The localnet IPFS emulator binds only to `127.0.0.1:5054` and stores blocks in an in-memory `Map`; restart loses them. On a configured production deployment, the IPFS RPC service would learn the source network metadata, upload timing, CID, and padded ciphertext size; gateways would learn fetch timing and requested CID. They receive encrypted padded blob bytes, not snapshot plaintext. The APP20 relay only permits reviewed origins in CSP and does not proxy or pin blobs itself. Production blob storage fails closed unless both browser IPFS origin settings and matching relay `IPFS_ORIGINS` are configured. A settled Take queues the opt-in RFQ auto-backup for the next unlocked mailbox session in Chat.

## Contact storage and recovery

Expand Down
2 changes: 1 addition & 1 deletion docs/APP20_PROCESS_DIAGRAMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ flowchart TD
RS --> V
PS --> PR[Privy Sepolia recovery vault]
LD --> D[Private RFQ v3 localnet desk plus legacy recovery]
RW -. UI view only, live Mail action denied .-> M[Mailbox and Counterparties, localnet actions only]
RW -. UI view only, live Mail action denied .-> M[Chat and Counterparties, localnet actions only]
LW --> M

D -. no automatic fallback .-> X[Public venue]
Expand Down
2 changes: 1 addition & 1 deletion docs/APP20_RFQ_V3_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ export type MakerIndicativeMidV1 = Readonly<{
- `src/lib/backup-snapshot.ts`: `BackupSnapshotV1 { version: 1, kind: "contacts" | "rfq-resume", seq, owner, chainId, helperAddress, mailboxFingerprint, createdAt, payload, digest, mac }` using the same HKDF/HMAC discipline as `contact-backup.ts` (domain `app20/backup/v1`); `contact_snapshot` v1 remains readable.
- `src/lib/backup-blob.ts`: `sealBackupBlob({ mailboxSeed, owner, chainId, kind, seq, bytes })` → AES-256-GCM (key = HKDF(seed, salt `app20/backup-blob/v1/salt`, info `app20/backup-blob/v1:owner:chainId:kind:seq`), 12-byte nonce, AAD = info), padded to 4096-byte buckets, header `[version=1][kind][seq u32]`; `openBackupBlob`.
- `src/lib/blob-store.ts`: `BlobStore { put(bytes): Promise<{ cid }>; get(cid): Promise<Uint8Array> }`; `computeCidV1Raw(bytes)` (multihash sha2-256 → base32 `bafkrei…`); `createIpfsBlobStore({ rpcOrigin, gatewayOrigins })` (upload via `/api/v0/add`, fetch via `/ipfs/{cid}?format=raw` on each gateway until one verifies; hash mismatch → reject); `createUnavailableBlobStore(reason)`. Configuration: localnet → `ipfsProxyPath` from `/config`; production → `VITE_IPFS_RPC_ORIGIN`/`VITE_IPFS_GATEWAY_ORIGINS` (default empty → unavailable, fail closed).
- Backup flow (inbox): "Back up RFQ history" and "Back up contacts" buttons create a snapshot; if its encoded envelope fits one Mail (≤ 4,293 bytes), post `backup_snapshot` inline; otherwise seal/upload a blob and post `backup_pointer { kind, seq, cid, bucketBytes, blobDigest }`. Candidate snapshots/pointers are authenticated before sequence ranking; same-sequence disagreement is rejected and RFQ restore never falls back past an unavailable/invalid newest authenticated candidate. Import enforces scope, a durable sequence high-water, strict bounded v2 record/tombstone shape, and an explicit keep-newer merge. Every backup is a public helper transaction; settled Take only queues the opt-in request for the next unlocked Mailbox session.
- Backup flow (Chat mailbox tools): "Back up RFQ history" and "Back up contacts to this mailbox" buttons create a snapshot; if its encoded envelope fits one Mail (≤ 4,293 bytes), post `backup_snapshot` inline; otherwise seal/upload a blob and post `backup_pointer { kind, seq, cid, bucketBytes, blobDigest }`. Candidate snapshots/pointers are authenticated before sequence ranking; same-sequence disagreement is rejected and RFQ restore never falls back past an unavailable/invalid newest authenticated candidate. Import enforces scope, a durable sequence high-water, strict bounded v2 record/tombstone shape, and an explicit keep-newer merge. Every backup is a public helper transaction; settled Take only queues the opt-in request for the next unlocked Mailbox session.
- Invoices: `PaymentRequestPayload.token` may be localnet USDC (registry-resolved) in addition to STRK. `InvoiceCard` shows "Pay privately with STRK" when the invoice token is USDC, which navigates to the desk in invoice mode. Inbox shows "Complete payment" for `awaiting-note-maturity` records once `takeBlock + 10 ≤ head` (uses `note-maturity.ts`), which runs the existing memo-transfer pay path with the USDC token and the recorded amount; states `awaiting-note-maturity → reserved → submitted → confirmed`.
- Relay CSP: `headers.ts` adds `IPFS_ORIGINS` env (comma-separated HTTPS origins, same validation as Privy origins) appended to `connect-src`; default unset → unchanged CSP. `verify-production-csp.mjs` needs no change while unset.

Expand Down
127 changes: 127 additions & 0 deletions src/app/chat/ChatComposer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import type { FormEvent, KeyboardEvent } from "react";
import { ProvingProgress } from "@/components/mail/OperationProgress";
import {
CHAT_LETTER_MAX_CHARS,
type ChatLetterBudget,
type ChatSendBlocker,
} from "./chat-send";
import styles from "./chat.module.css";

export type ChatComposerStatus = Readonly<{
kind: "sending" | "ok" | "error";
message: string;
startedAt?: number;
}> | null;

type ChatComposerProps = {
contactName: string;
value: string;
onChange: (value: string) => void;
blocker: ChatSendBlocker | null;
sending: boolean;
status: ChatComposerStatus;
budget: ChatLetterBudget;
onSend: () => void;
/** Opens the document composer for this counterparty: terms, invoices, escrow. */
onAttach: () => void;
attachDisabled?: boolean;
};

export default function ChatComposer({
contactName,
value,
onChange,
blocker,
sending,
status,
budget,
onSend,
onAttach,
attachDisabled = false,
}: ChatComposerProps) {
const disabled = Boolean(blocker) || sending;
const canSend = !disabled && value.trim().length > 0 && budget.fits;

function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (canSend) onSend();
}

function onKeyDown(event: KeyboardEvent<HTMLTextAreaElement>) {
if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
if (canSend) onSend();
}
}

return (
<form
className={styles.composer}
aria-label={`Write to ${contactName}`}
onSubmit={submit}
>
<label className={styles.srOnly} htmlFor="chat-composer">
Message to {contactName}
</label>
<textarea
id="chat-composer"
className={styles.composerInput}
value={value}
rows={3}
maxLength={CHAT_LETTER_MAX_CHARS}
placeholder={`Write an encrypted letter to ${contactName}…`}
disabled={sending}
onChange={(event) => onChange(event.target.value)}
onKeyDown={onKeyDown}
/>
<div className={styles.composerRow}>
<span className={styles.composerMeta}>
{blocker ? (
<>
{blocker.message}
{blocker.kind === "key" ? (
<>
{" "}
<a href="#mailbox-key-setup">Set up a mailbox key</a>
</>
) : null}
</>
) : (
<>
{budget.plaintextBytes} / {budget.maxPlaintextBytes} bytes ·
sealed on this device · 1 wallet approval · recipient count and
timing are public
</>
)}
</span>
<span className={styles.composerActions}>
<button
type="button"
className={styles.attachButton}
disabled={attachDisabled || sending}
onClick={onAttach}
>
Attach terms
</button>
<button type="submit" className={styles.sendButton} disabled={!canSend}>
{sending ? "Sending…" : "Send encrypted"}
</button>
</span>
</div>
{status ? (
<p
className={styles.composerStatus}
data-kind={status.kind}
role={status.kind === "error" ? "alert" : "status"}
>
{status.message}
</p>
) : null}
<ProvingProgress
active={sending}
startedAt={status?.startedAt}
label="Sealing and submitting the letter"
/>
</form>
);
}
Loading
Loading