Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
b8fbf63
fix(rust): advance the zingolib pin past the 2509 merge and adapt to it
zancas Jul 24, 2026
b7b2c71
refactor!: retire the migration cadence surface end to end
zancas Jul 24, 2026
c0f8339
docs: ratify the Mixnet Mode and silent-alpha vocabulary
zancas Jul 25, 2026
e52a002
feat: add the Always On flavor that builds the silent alpha APK
zancas Jul 25, 2026
7fd1eab
fix(ffi): carry the Mixnet error variant to the Swift and TS bridges
zancas Jul 25, 2026
5af2179
feat: add the alwaysontest flavor so the alpha ships per network
zancas Jul 25, 2026
708c9b5
feat: fail-close both covered surfaces in the always-on flavors
zancas Jul 25, 2026
062caca
feat(price)!: type the price-fetch outcome and consume it exhaustively
zancas Jul 25, 2026
8d909fc
docs: ratify ADR 0004, exhaustive handler records for typed outcomes
zancas Jul 25, 2026
db0aeed
fix(price): carry full error chains across the FFI and take the oracl…
zancas Jul 25, 2026
72581ea
feat(price)!: attest the mixnet route on every successful fetch
zancas Jul 25, 2026
4055af6
docs: audit every null in the TypeScript sources
zancas Jul 25, 2026
42f8f0f
fix: replace four ambiguous nulls with named discriminated unions
zancas Jul 25, 2026
44c59d5
refactor: define the selection and FFI-decode patterns once, apply ev…
zancas Jul 25, 2026
580617c
refactor(send)!: name updateToField's writes as a discriminated union
zancas Jul 25, 2026
55acda4
refactor: remove dead code agreed on by three audit tools
zancas Jul 25, 2026
9a2c684
feat(price)!: bound every price fetch at 25 seconds with a typed timeout
zancas Jul 27, 2026
c713363
fix(ffi)!: adapt to dev's immediate-migration vocabulary
zancas Jul 27, 2026
478ef11
fix: drop the cadence-era exports the dev merge resurrected
zancas Jul 27, 2026
bb42888
test: render every price failure as one copyable support report
zancas Jul 27, 2026
3ff88fe
docs: plan the Connection Doctor and the pin collaboration protocol
zancas Jul 27, 2026
8b1d013
build: bump zingolib to nym_mobile_adoption head 44ec7e07
zancas Jul 27, 2026
0d786e4
feat: surface the full price failure report on a manual fetch
zancas Jul 27, 2026
4caa9a4
build: repoint the zingolib pin to dev after #2527 merged
zancas Jul 27, 2026
a0e320a
feat(ffi): expose the paired connectivity probe as structured data
zancas Jul 27, 2026
a5f9a16
feat(doctor): the Connection Doctor screen and its copyable report
zancas Jul 27, 2026
31c5797
fix(doctor): keep the probe interpreter free of the native surface
zancas Jul 27, 2026
9213f11
build: pin zingolib to net_diag_taxonomy for the fielded taxonomy
zancas Jul 27, 2026
ba1514a
feat(doctor)!: fielded probes as exhaustive enums, and the staged syn…
zancas Jul 27, 2026
f1399a1
fix(android): gate the recents-card TaskDescription on API 33
zancas Jul 28, 2026
e3675bc
fix(doctor): gate the Connection Doctor on the debuggable bit
zancas Jul 28, 2026
c7b43ba
feat(workbench): tie the staged shim to the zingolib pin by provenance
zancas Jul 28, 2026
eb06963
build: repoint the zingolib pin to socks5_typed_timeout
zancas Jul 28, 2026
13d8d2c
feat(price): name the transport on every fetch and free the report
zancas Jul 28, 2026
8eaec8b
feat(doctor): head the staged section with its clearnet transport
zancas Jul 28, 2026
ed652ad
feat(ffi): carry the mixnet death detail across the bridge typed
zancas Jul 28, 2026
5ccd40d
feat(price)!: never hold the wallet lock in the price-fetch flow
zancas Jul 28, 2026
428fda1
docs(context): add the Price surface and Indexers glossary sections
zancas Jul 28, 2026
aa3db17
build: repoint the zingolib pin to attach_retune
zancas Jul 28, 2026
c78bde7
feat(ffi): carry the mixnet timing and the death report across the br…
zancas Jul 28, 2026
b97b898
feat(price): carry buffered observations on the piggyback write
zancas Jul 28, 2026
a68c38c
build: repoint the zingolib pin to price_sources
zancas Jul 28, 2026
00a30b9
feat(price): consume the three-source race and name the winner
zancas Jul 28, 2026
8892fcd
fix(price): reserve the store's retry for natively settled failures
zancas Jul 28, 2026
5190241
test(ffi): unify the price fixtures onto lock_discipline_tests
zancas Jul 28, 2026
d9875d7
chore: reconcile the linearized tree with feat/nym
zancas Jul 28, 2026
d4a67d5
build: repoint the zingolib pin to indexer_census
zancas Jul 28, 2026
013feae
feat(app): project the server list from the wallet's indexer census
zancas Jul 28, 2026
0305628
feat(app)!: retire the live hosh registry; selection consults only th…
zancas Jul 28, 2026
5273eb8
fix: re-export the coordinator poll cadences for the dev-era test
zancas Jul 29, 2026
41881fe
build: settle the lockfile and regenerate the Kotlin bindings post-re…
zancas Jul 29, 2026
3490da9
test: retake the History snapshots against the rebased tree
zancas Jul 29, 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
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,10 @@ without losing meaning, the "so" was decorative. Delete it.
### 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.

## Pending plans

Coordinated work ready for implementation lives in `docs/agents/`.
Current: `docs/agents/nym-diagnostics-plan.md`, the Connection Doctor
debug APK for Nym connectivity triage (issue #1221) and the pin
collaboration protocol for agents updating the zingolib pin.
2 changes: 1 addition & 1 deletion App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export const basicTheme: ThemeType = {

const Stack = createStackNavigator<AppStackParamList>();

export const navigationRef = createNavigationContainerRef();
const navigationRef = createNavigationContainerRef();

const App: React.FunctionComponent = () => {
const [theme, setTheme] = useState<ThemeType>(advancedTheme);
Expand Down
130 changes: 117 additions & 13 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ The group of parts broadcast together in one window. UI-facing term.
_Avoid_: bucket (API-internal), shipment

**Window**:
One 144-block (3h) anchor-height bucket during which a batch may be sent.
UI-facing term.
One bucket-modulus span of anchor heights (144 blocks, ~3h provisionally)
during which a batch may be sent. UI-facing term.
_Avoid_: bucket, epoch

**Boundary**:
Expand All @@ -65,10 +65,10 @@ anchors to. Sharing one anchor is what keeps a send free of per-wallet timing
information. A window can only carry notes that already existed at its
boundary, so a batch of freshly split notes waits for the next one.

**Cadence**:
How many parts share each window (`per_bucket`). Chosen once, after splitting
completes and before any part is signed; only re-buckets existing parts, never
re-cuts notes. UI frame: "How many batches?"
**Cadence** (retired):
Formerly the user-chosen parts-per-window count (`per_bucket`). The ZIP 318
Poisson schedule draws every broadcast delay itself, so there is no cadence
to choose and no chooser screen; the term survives only in history.

**Wake**:
An OS-scheduled re-entry into the app for a window. Two kinds per window: a
Expand All @@ -88,17 +88,114 @@ round's inputs nor its outputs, so planning over it is wrong.
A part whose window became unwitnessable and which moved itself to a coming
window. Rendered as a plain explanatory sentence, never an error.

**Stranded**:
Value left out of the plan because moving it would cost more than it carries.
Disclosed at consent.

**Residual**:
What remains when the migration completes; disclosed on completion.
Value left unmigrated because moving it would cost more than it carries —
projected and disclosed at consent, reported again on completion. A plan
never loses value silently: migrated + fees + residual equals the starting
balance.
_Avoid_: stranded (retired 2026-07-26 when zingolib unified the plan-time
and completion-time vocabulary on "residual")

**Consent**:
The single user approval of an exact plan hash before anything is signed or
sent. Covers the whole migration, both phases.

## Mixnet Mode

**Mixnet Mode**:
Routing the send (transaction broadcast) and price-fetch surfaces over the
Nym mixnet. Synchronization is never covered; the IP-correlation disclaimer
(ZIP-0318) states that boundary. Modes: `off`, `bootstrapping`, `ready`,
`died`.

**Fail-closed**:
The policy that when Mixnet Mode is anything but `off`, a covered surface
that cannot reach the mixnet refuses rather than falling back to clearnet.
A refusal is not a server error and is never retried.

**Silent alpha APK**:
An alpha build of the app that routes the covered surfaces over Nym with
the stock (pre-Mixnet-Mode) UX/UI — no toggle, no banners, no disclaimer
screen. Its purpose is isolating transport behavior from UI work.
_Avoid_: silent mode (it is a build, not a runtime mode)

**Always On** (build flavor):
The build flavors that produce the silent alpha APKs: Mixnet Mode is enabled
unconditionally at wallet initialization and cannot be disabled at runtime.
Two network variants exist — `alwayson` first-runs on mainnet, and
`alwaysontest` first-runs on testnet — installable side by side.

## Indexers

**Indexer**:
A lightwalletd or zaino gRPC endpoint the wallet talks to. In UI copy:
"server".

**Operator**:
The party running one or more indexer endpoints. Regional DNS variants of
one operator (`eu.zec.rocks`, `zec.rocks`) are the same operator. Trust,
diversity, and exclusion are all reckoned per operator, never per DNS name.

**Sync Indexer**:
The indexer the wallet synchronizes against. It necessarily learns the
wallet's address set.

**Broadcast Indexer**:
An indexer drawn to receive one transaction broadcast. Also called a
**witness**. A Broadcast Indexer is never the Sync Indexer: the operator
that holds the address set must not also receive the broadcast. Exclusion
is by operator.
_Avoid_: relay, send server

**Witness Rotation**:
The policy of drawing each broadcast's witness uniformly from an
operator-diverse pool, so accumulating knowledge of the wallet's sends
spreads across parties instead of concentrating in one.

**Census** (of indexers):
The one authoritative list of indexer endpoints, held in zingolib and
consumed everywhere an indexer is defaulted, selected, probed, or
health-checked. Refreshes on release cadence, never over the network at
runtime. Server selection consults only the census.
_Avoid_: registry (that names the retired live fetch), server list (ambiguous)

**Operator diversity**:
The requirement that indexer selection — for sync and for broadcast —
pursue distinct operators, not merely distinct hostnames. A selection whose
candidates collapse to one operator fails this even if it lists many URIs.

## Price surface

**Price observation**:
One fetched ZEC/USD price point, held in memory awaiting a piggyback
write. A price fetch produces an observation and a payload for the
caller; it never promises durability.

**Observation window**:
The bounded in-memory buffer of price observations, capacity 1000.
When the 1001st observation arrives the oldest is dropped.

**Piggyback write**:
Persistence that rides along with another operation already holding the
wallet write lock. The price surface's only persistence path: if no such
operation runs while an observation is buffered, the observation is lost,
by contract.
_Avoid_: drain (already means the immediate migration path)

**Route snapshot**:
The price surface's view of the mixnet state, as last observed by a call
that held the wallet for its own reasons. May lag the live state by one
poll interval; the fail-closed policy applies to the snapshot.

**Price source**:
An operator the wallet asks for the ZEC/USD price: Gemini and Kraken
(medians of recent trades) or CoinGecko (an aggregator's spot value).

**Price race**:
One fetch asking every price source concurrently and reporting the first
success; the losing requests are cancelled. Only a race in which every
source fails is a fetch failure, and that failure names each source.

## CI

**Blocking check** — a PR CI job whose failure fails the pull request.
Expand All @@ -116,8 +213,15 @@ the verdict path.

**Bucket** — a group of Android integration tests that share one CI job,
so runner setup and emulator boot amortize across the group instead of
being paid once per test. Unrelated to the migration API's `per_bucket`
windowing sense, which UI copy avoids entirely.
being paid once per test. Unrelated to the migration schedule's windowing
sense of the word, which UI copy avoids entirely.

**Mobileclient scenario contract** — the agreement that the
`*_mobileclient` regtest scenarios zingo-mobile consumes from zingolib
keep pre-ironwood semantics: the scenario chain never activates NU6.3,
no funds can land in the Ironwood pool, and the on-device ledgers assert
the pre-ironwood distribution. An ironwood-era ledger is valid only
after a coordinated change on both sides of the contract.

**Fail-all** — the policy that the first failure of any blocking check
cancels the entire run at once, rather than letting the surviving checks
Expand Down
84 changes: 0 additions & 84 deletions __tests__/BlockTime.unit.test.ts

This file was deleted.

56 changes: 56 additions & 0 deletions __tests__/CheckAddressVerdict.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* @format
*/

import { interpretCheckAddressResult } from '../components/Receive/components/checkAddressVerdict';
import { FfiResult } from '../app/walletBackend/ffi';

const ok = (value: string): FfiResult<string> => ({ ok: true, value });
const rejected = (): FfiResult<string> => ({
ok: false,
error: { code: 'InvalidInput', message: 'bad address' },
});

describe('interpretCheckAddressResult', () => {
test('is_wallet_address true is a positive verdict', () => {
const raw = JSON.stringify({ is_wallet_address: true, account_id: 0 });
expect(interpretCheckAddressResult(ok(raw))).toEqual({ kind: 'mine' });
});

test('is_wallet_address false is a negative verdict', () => {
const raw = JSON.stringify({ is_wallet_address: false, account_id: 0 });
expect(interpretCheckAddressResult(ok(raw))).toEqual({ kind: 'notMine' });
});

test('a typed FFI rejection is named, and carries its code', () => {
expect(interpretCheckAddressResult(rejected())).toEqual({
kind: 'ffiRejection',
code: 'InvalidInput',
message: 'bad address',
});
});

// EVIDENCE of the misinterpretation this replaces: the screen stored
// `is_wallet_address` straight off JSON.parse behind a `verifyOK !== null`
// render gate, so a well-formed payload lacking the field stored
// `undefined`, passed the gate, and rendered the definitive "this address
// does not belong to you" — a confident false negative produced by a
// check that never returned a verdict.
test('a payload without is_wallet_address is malformed, not "not your address"', () => {
const raw = JSON.stringify({ encoded_address: 'u1aaa' });
expect(interpretCheckAddressResult(ok(raw)).kind).toBe('malformed');
});

// EVIDENCE, same gate: a truthy non-boolean must not read as "yours".
test('a non-boolean is_wallet_address is malformed, not a verdict', () => {
const raw = JSON.stringify({ is_wallet_address: 'yes' });
expect(interpretCheckAddressResult(ok(raw)).kind).toBe('malformed');
});

// EVIDENCE: a parse failure used to be swallowed by a bare catch, so the
// user tapped Verify and nothing happened at all.
test('an unparseable or empty payload is malformed, never silent', () => {
expect(interpretCheckAddressResult(ok('not json')).kind).toBe('malformed');
expect(interpretCheckAddressResult(ok('')).kind).toBe('malformed');
});
});
Loading
Loading