Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7a98779
Migrate nym shim binding generation from zingolib
dorianvp Jul 31, 2026
a3f8090
feat: host the mixnet proxy shim in this repository
zancas Aug 10, 2026
58102b0
chore: draft changes for nym integration from zingolib
dorianvp Aug 11, 2026
9ecf785
chore: ui fixes
dorianvp Aug 2, 2026
39de9b9
chore: ios perf fixes
dorianvp Aug 4, 2026
e0deded
chore: update Orchard mentions to Ironwood
dorianvp Aug 4, 2026
5f286d4
chore: nym toggle feedback
dorianvp Aug 4, 2026
2b3a1eb
fix: animation status bar continuity
dorianvp Aug 4, 2026
d12f988
chore: fix NYM edge cases
dorianvp Aug 6, 2026
c55e264
fix: animations, network states, and icons
dorianvp Aug 6, 2026
42ab31f
fix: iOS podfile min 16
juanky201271 Aug 7, 2026
f73a81f
fix: Android quickstart & script improvement
juanky201271 Aug 7, 2026
fdcf2f9
fix: update receiver types and onload nym config
dorianvp Aug 10, 2026
98fb689
fix: some ADR and eslint config issues
dorianvp Aug 10, 2026
351de5a
fix: move nym to network section
dorianvp Aug 10, 2026
333de33
fix: value transfer types when sending
dorianvp Aug 10, 2026
ac1035e
chore: re-enable private migration
dorianvp Aug 10, 2026
2bbbad4
chore: route the private path through nym too
dorianvp Aug 10, 2026
7d55333
chore: use latest zingolib
dorianvp Aug 10, 2026
a25e071
ci: host-side verdict for the excluded mixnet proxy shim
zancas Aug 11, 2026
01e7b6f
fix: adapt the replayed nym UI to the dev theme tokens
zancas Aug 11, 2026
68c49aa
refactor: the main workspace names zingo-netutils exactly once
zancas Aug 11, 2026
7229477
feat: the shim's TLS verifies against Android's platform verifier
zancas Aug 11, 2026
62afaaa
docs: every unmerged doc-comment is one sentence
zancas Aug 11, 2026
30e1d8d
ci: the shim lanes cover the tls-init member
zancas Aug 11, 2026
90cd731
chore: fix binding gen
dorianvp Aug 12, 2026
9036bad
chore: integrate remaining zingolib changes
dorianvp Aug 13, 2026
e8bc3bb
refactor: split `LoadedApp` context & migrate to `jotai`
dorianvp Aug 14, 2026
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
11 changes: 11 additions & 0 deletions .agents/claude.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Agent instructions: documentation

## The one-sentence documentation rule (ratified 2026-08-10)

Every item doc-comment — Rust `///`, and the KDoc or doc-comment on a
Kotlin or Swift item — is exactly one sentence. That sentence must not
reference ADRs, issues, or any other document. Module headers (Rust
`//!`, a file- or class-level header block) are exempt, and test
doc-comments that follow a ratified convention (for example HYPOTHESIS
falsifiers) keep that convention's shape. Apply the rule to every
unmerged doc-comment before merge.
1 change: 1 addition & 0 deletions .github/workflows/android-apk-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ jobs:
run: |
set -eu
test -f "android/app/src/main/jniLibs/${{ env.ABI }}/libuniffi_zingo.so"
test -f "android/app/src/main/jniLibs/${{ env.ABI }}/libzingo_nym_proxy_ffi.so"
test -f "android/app/build/generated/source/uniffi/release/java/uniffi/zingo/zingo.kt"

- name: Remove unnecessary directories to free up space
Expand Down
30 changes: 29 additions & 1 deletion .github/workflows/android-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -222,11 +222,39 @@ jobs:
working-directory: ./rust/target/${{ env.TARGET }}/release
run: mv ./libzingo.so ./libuniffi_zingo.so

- name: Cargo build nym proxy shim
working-directory: ./rust/nym-proxy-ffi
run: |
cargo ndk \
--target ${{ env.TARGET }} build \
--release
env:
AR: llvm-ar
LD: ld
RANLIB: llvm-ranlib
CC: ${{ env.CC }}26-clang
CARGO_FEATURE_STD: ${{ env.CARGO_FEATURE_STD }}
LIBCLANG_PATH: /usr/lib/llvm-18/lib
CARGO_NDK_PLATFORM: 26
CARGO_NDK_ANDROID_PLATFORM: 26

- name: LLVM Strip nym proxy shim
working-directory: ./rust/nym-proxy-ffi/target
run: |
llvm-strip --strip-all ./${{ env.TARGET }}/release/libzingo_nym_proxy_ffi.so
llvm-objcopy --remove-section .comment ./${{ env.TARGET }}/release/libzingo_nym_proxy_ffi.so

- name: Stage native libraries
run: |
mkdir -p rust/native-out
cp rust/target/${{ env.TARGET }}/release/libuniffi_zingo.so rust/native-out/
cp rust/nym-proxy-ffi/target/${{ env.TARGET }}/release/libzingo_nym_proxy_ffi.so rust/native-out/

- name: Upload native rust
uses: actions/upload-artifact@v7
with:
name: native-android-uniffi-${{ env.ARCH }}-${{ env.CACHE-KEY }}
path: rust/target/${{ env.TARGET }}/release/libuniffi_zingo.so
path: rust/native-out/
retention-days: 3

- name: Cancel the run on failure (fail-all)
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ jobs:
with:
fail-all: true

nym-proxy-ffi-check:
uses: ./.github/workflows/nym-proxy-ffi-check.yaml
with:
fail-all: true

js-depcheck:
uses: ./.github/workflows/js-depcheck.yaml
with:
Expand Down
66 changes: 66 additions & 0 deletions .github/workflows/nym-proxy-ffi-check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Mixnet proxy shim checks

on:
workflow_call:
inputs:
# Fail-all (see CONTEXT.md): when true, a failure here cancels the
# whole run. PR CI opts in; ci-nightly does not.
fail-all:
required: false
type: boolean
default: false

jobs:
nym-proxy-ffi-check:
name: shim check, clippy, fmt, tests
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v7

# The shim is excluded from the main workspace (own lockfile, see
# rust/Cargo.toml), so workspace-wide jobs never touch it. This job
# is its only host-side verdict.
- name: Cache the shim build
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
rust/nym-proxy-ffi/target
key: nym-proxy-ffi-${{ runner.os }}-${{ hashFiles('rust/nym-proxy-ffi/Cargo.lock') }}

- name: Install cargo-nextest
uses: taiki-e/install-action@v2
with:
tool: cargo-nextest@0.9.98

- name: Format check
working-directory: ./rust/nym-proxy-ffi
run: cargo fmt --check

# --workspace covers the tls-init member; its JNI body is
# Android-gated, so the host lanes prove the workspace shape and the
# release cross-build proves the JNI code.
- name: Check
working-directory: ./rust/nym-proxy-ffi
run: cargo check --locked --workspace --all-targets

- name: Clippy
working-directory: ./rust/nym-proxy-ffi
run: cargo clippy --locked --workspace --all-targets -- -D warnings

- name: Tests
working-directory: ./rust/nym-proxy-ffi
run: cargo nextest run --locked --workspace

- name: Cancel the run on failure (fail-all)
if: ${{ failure() && inputs.fail-all }}
uses: actions/github-script@v7
with:
script: |
await github.rest.actions.cancelWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.runId,
});
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,13 @@ android/app/beta/release/
*.aab
*.apk
ios/Zingolib.xcframework/
ios/ZingoNymProxyFFI.xcframework/
ios/NymHost.xcframework/
ios/zingo.swift
ios/zingo_nym_proxy_ffi.swift
rust/lib/Generated/zingo.swift
rust/lib/Generated/zingoFFI.h
rust/nym-proxy-ffi/Generated/
rust/lib/Generated/zingoFFI.modulemap

# Android/IntelliJ
Expand Down Expand Up @@ -99,5 +103,12 @@ buck-out/

# Claude Code
.claude/

# Wayfinder Agent Skill
.wayfinder

# Jest coverage output
coverage/

# Local env
.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# 01 — RESEARCH: Header reads the jotai default store inside LoadingApp

**Type:** Research (investigate and decide, do not implement yet)

**What to build:** A decision on how `Header` should resolve its jotai atoms when it renders under `LoadingApp`. Today `Header` reads `syncStatusAtom` and `usePrice()` (`priceViewAtom`). Under `LoadedApp` it reads the per-instance `controllerStore` through `<Provider>`. Under `LoadingApp` (import-UFVK and new-seed screens) there is no jotai `<Provider>`, so per the jotai docs those reads fall to the process-global default store ("provider-less mode"). Same component, two stores, chosen by tree position. The finding is latent, not firing: nothing in production writes the default store, so it stays at atom defaults (blank sync, unpriced), which is the correct onboarding display. The research decides whether to close the isolation hole and how.

**Blocked by:** None — can start immediately.

**Status:** ready-for-agent

## Questions to answer

- [ ] Confirm the two render paths: `Header` under `LoadingApp` (via the import-UFVK and new-seed screens) reads the global default store, and under `LoadedApp` reads `controllerStore`. Cite the exact call sites.
- [ ] Confirm nothing in production writes the default store: no `getDefaultStore`, no provider-less `useSetAtom`, every container write targets `controllerStore`. State whether that invariant is enforced or merely current.
- [ ] Decide the fix shape. Options to weigh: (a) wrap the `LoadingApp` subtree in a `<Provider>` (bare or with a store field) so onboarding gets an isolated, remount-clean store; (b) give `Header` an explicit onboarding variant prop so its sync/price widgets are inert by intent, not by a missing Provider; (c) accept and document the current behaviour with a guard against future default-store writes.
- [ ] Check for the same defect elsewhere: any other jotai consumer that can mount outside a `<Provider>`. The known-clean set inside `LoadedApp` is `HomeStackBody`, `AddTagModalSlice`, and the options-panel host, all within the Provider subtree.
- [ ] Recommend one option and record why. Output is a decision plus, if warranted, a follow-up build ticket — not a code change in this ticket.

## Notes

- Jotai docs, provider-less mode: "If an atom is used in a tree without a Provider, it will use the default state." Verified against `/pmndrs/jotai`.
- Severity as found: latent robustness gap, not a live bug. It breaks the per-instance reset guarantee and is fragile against any future default-store write.
22 changes: 13 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

## MUST DO ALWAYS

- Use ASD-STE100 for concision.
- If you need a paragraph-long comment to justify why a workaround is OK, the code is wrong: Fix the code!
- Call the user "friend", in a similar fashion to Mr Robot.
- When reporting information to me, be extremely concise and sacrifice grammar for the sake of concision.
- When in doubt, use context7 to check for accurate documentation.

## Writing & Code Style

Goal: produce prose and code that reads as if written by a specific, competent human, not by a model. The point is naturalness and fit, not looking exhaustive or safe. When in doubt, commit to a choice and keep it short.
Goal: naturalness and fit, not looking exhaustive or safe. When in doubt, commit to a choice and keep it short.

### Prose

Expand All @@ -24,6 +26,7 @@ Goal: produce prose and code that reads as if written by a specific, competent h
- Use the active voice. Prefer "the parser rejects malformed input" over "malformed input is rejected by the parser". Passive voice only when the actor is unknown or irrelevant.
- Put statements in positive form. Say what something is, not what it isn't. "The cache is stale" beats "the cache is not up to date". No "not un-" constructions.
- Use definite, specific, concrete language. "Sync stalls after 40k blocks" beats "performance degrades under certain conditions". If you have a number, a name, or a mechanism, state it.
- Leave no room for two readings. If a word or phrase is ambiguous in context, replace it with one that isn't. One term, one meaning throughout.
- One paragraph, one topic. Don't braid two ideas together and rely on connectors to hold them.
- Place emphatic words at the end of the sentence. Don't bury the key claim mid-sentence and trail off with qualifiers.
- Don't overstate. Cut intensifiers ("very", "extremely", "incredibly"). When uncertainty is real, hedge precisely ("untested on mainnet"), not vaguely ("may or may not work").
Expand All @@ -40,22 +43,23 @@ Goal: produce prose and code that reads as if written by a specific, competent h
- Defaulting to groups of three (adjectives, clauses, list items). Vary the count.
- "From X to Y" fake-comprehensive sweeps.
- "Whether you're X or Y" catch-all wrap-ups.
- Forced analogies ("think of it like a...").
- No metaphors or analogies. Describe the mechanism directly, not by comparison ("think of it like a...").

#### Avoid manufactured logical connectors generally

Don't use "so," "which means," "therefore," or "as a result" unless the
second clause is a genuine, non-obvious consequence of the first. Don't
use "not just X, but Y" or "it's not merely A, it's B" as a rhetorical
crutch. When two clauses are just parallel facts, write them as
parallel facts — don't dress them up as an inference.
parallel facts. Don't dress them up as an inference.

Rule of thumb: if you can delete "so" and replace it with a period
without losing meaning, the "so" was decorative. Delete it.

#### Openers and closers
- No throat-clearing: "It's important to note", "It's worth noting", restating the question before answering.
- No grandiose closers or zoom-outs: "In conclusion", "Ultimately", "At the end of the day", "in an ever-evolving world". Stop when the point is made.
- No epigram closers. Don't end a section or paragraph with a line that restates its point as a quotable maxim ("a convention the compiler cannot see is a convention every call site can violate"). The tell is an unlabeled aphorism that adds cadence, not information. Stop at the last concrete point.
- Don't chain connectives: "Moreover", "Furthermore", "Additionally", "That said".

#### Tone
Expand All @@ -64,14 +68,14 @@ without losing meaning, the "so" was decorative. Delete it.
- Assert plainly. Cut reflexive hedging and over-qualification.

#### Vocabulary to avoid
- delve, tapestry, realm, landscape, navigate/navigating, leverage, robust, seamless, crucial, vital, pivotal, testament, boasts, nestled, foster, harness, unlock, elevate, embark, showcase, underscore, spearhead, treasure trove, game-changer, cheap, liveness, gap, shape, correctness, alive, honest, simple, probe, contact, stay, stranger.
- Flat ban: delve, tapestry, realm, landscape, navigate/navigating, leverage, robust, seamless, crucial, vital, pivotal, testament, boasts, nestled, foster, harness, unlock, elevate, embark, showcase, underscore, spearhead, treasure trove, game-changer.
- Avoid as filler, keep when technically precise: cheap, liveness, gap, shape, correctness, alive, honest, simple, probe, contact, stay, stranger.

#### Formatting
- Don't bold the lead phrase of every bullet.
- Don't bullet what should be prose.
- No headers on two-sentence sections.
- No emoji as section markers.
- Vary sentence length deliberately.

#### Articles and determiners

Expand All @@ -94,7 +98,7 @@ without losing meaning, the "so" was decorative. Delete it.
- No docstrings that just restate the signature.
- Names: concise and domain-specific. Avoid generic placeholders (`data`, `result`, `output`, `item`, `value`, `temp`, `handleData`, a helper named `helper`) and avoid over-long descriptive names where a short one is idiomatic.
- No completeness theater: no unrequested demo/usage blocks, no logs narrating execution ("Starting...", "Done!"), no emoji in output, no unprompted complexity analysis in comments.
- Don't add guards for conditions that can't occur. Don't wrap non-throwing code in try/catch. Don't swallow-and-log errors; let them propagate.
- Don't add guards for conditions that can't occur. Don't wrap non-throwing code in try/catch. Don't swallow-and-log errors. Let them propagate.
- Match the surrounding codebase's idioms and conventions over textbook-uniform formatting.

### Rust
Expand Down Expand Up @@ -129,9 +133,9 @@ without losing meaning, the "so" was decorative. Delete it.
### For agents

- Before finishing a task, scan what you wrote against this file. Focus on the high-signal tells, not a full re-audit: antithesis flips and narrating comments in prose, `.clone()`/`.unwrap()` spam and explicit trailing `return` in Rust, `useEffect` for derived state and `any` in TS.
- Verify your *new* output fits these rules and the surrounding code's style. The question is "does what I added fit", not "does this whole file now obey CLAUDE.md".
- Verify your *new* output fits these rules and the surrounding code's style. The question is "does what I added fit", not "does this whole file now obey AGENTS.md".
- Don't reformat, re-comment, or otherwise "correct" existing code you were only asked to touch lightly. Match what's there. Keep diffs scoped to the task.

### Tooling

- No heredocs (<< EOF, << 'EOF') in bash or other shell scripts. They're hard to read, break on escaping, and bury content that should be its own file. Use a real file, a templating step, or printf with explicit lines instead.
- No heredocs (<< EOF, << 'EOF') in bash or other shell scripts. They're hard to read, break on escaping, and bury content that should be its own file. Use a real file, a templating step, or printf with explicit lines instead.
83 changes: 83 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,86 @@ part and a zingo-mobile part (`zl_…-zm_…`), each derived from git
describe against that repo's release tags. The commit count and
5-character hash fields are elided when a part sits exactly on its
release tag, and `_dirty` marks a part built from an uncommitted tree.

## State machine

Vocabulary of the frontend state model (`app/LoadedApp`, `app/LoadingApp`, and the
Jotai controller that replaces their scattered state).

**Native runtime**:
The zingolib worker behind `RPCModule`, the authoritative holder of sync, wallet, and
send state. JS observes it by polling and drives it by command; it does not take orders
to stop.
_Avoid_: the backend (ambiguous), the RPC.

**Fake native runtime**:
A dev-only stand-in for the Native runtime behind the `RPCModule` seam. It holds an authored
in-memory state the app observes by polling, exactly as it observes the real worker. Selected
by `__DEV__ && FAKE_RUNTIME`, dead-stripped from release. Driven by `set(patch)`, by hand
through a dev panel or by a scripted timeline. Reproduces the real wire shapes by hand until
the FFI-to-TS typing effort types the boundary.
_Avoid_: fake backend (the glossary bans "backend"), mock (it holds state, not a call log).

**Controller machine**:
The JS-side state machine, Jotai-held, that models intent plus the last observed native
state plus the derived views. A reflection-and-command layer over the native runtime,
never the owner of native work.
_Avoid_: the store (it is a machine, not a bag), app state.

**Cancellability class**:
Which of four behaviors a native command has when JS stops wanting its result:
*JS-ignorable* (runs to completion, JS discards the result by epoch), *native-pausable*
(`pauseSyncProcess` interrupts it), *must-complete* (never interrupted, e.g. the wallet
save), and *fire-and-forget-until-poll* (no handle, state learned only by polling). Every
async fix names the class it acts on.

**Epoch**:
A generation token the controller stamps on a command and its observations. A result
whose epoch is stale is discarded, not applied. The cure for JS-ignorable staleness: the
after-unmount write and the torn server-switch read.
_Avoid_: token (overloaded), version.

**Reconciliation**:
Updating the controller from a poll result, closing the window where JS intent and native
reality disagreed.

**WalletView**:
The derived render state — spinner, receive-only, full with Send, full without Send — as a
total function of the controller's source union. One value at a time.
_Avoid_: screen (that is navigation), mode.

**Faulted**:
The lifecycle state the controller enters when the native runtime returns an error that makes
it unusable: a corrupt wallet on load, or an invariant-break rejection mid-session. Recovered
only by a reset or a restore, never by a retry. Distinct from a sync failure, where the wallet
is intact and a poll retries it, and from Stalled, where the bridge is unreachable, not broken.
_Avoid_: crash (that is the RNRestart phase), corrupt (only one of its causes).

**Stalled**:
The observed condition where an issued command stays outstanding past a threshold, so the
native bridge is unreachable. Names the reality that one lock-holding command with no timeout
freezes every caller. Derived from how long the command has been in flight, never stored. It
does not cancel the native call.
_Avoid_: frozen, hung, timeout (nothing times out natively).

**Command scheduler**:
The single JS-side caller of the native runtime. It runs one native call at a time, poll and command
alike, mirroring the one write lock the native side serializes on. A self-rescheduling loop, not a
fixed interval, so a second tick cannot start while one runs. Stopping it clears its one timer and
bumps the epoch.
_Avoid_: sync loop (it owns commands too), poll loop.

**In-flight command**:
The one native command the scheduler has in flight, its slice of the single-flight lane. At most one
exists at a time, which serializes commands on the JS side. Carries the epoch the command was issued
under, so a stale result is discarded on return, and clears when the command's ack resolves. A native
call that never returns reads as Stalled, not a stuck in-flight command. Named `inFlight` in the model
(ADR 0005).
_Avoid_: overlay (a UI word, names nothing it holds), queue (there is at most one), pending state,
awaiting-confirmation (every command confirms on its ack).

**Configured server / Bound server**:
The configured server is the one the next sync launch will use. The bound server is the one the
running sync captured at its launch. A server switch changes the configured server at once,
while the running sync keeps the bound server until it finishes its catch-up and relaunches.
_Avoid_: current server (ambiguous between the two).
Loading
Loading