Skip to content

feat(results): disclose which tier answered a wallet balance read - #5

Merged
MichaelTaylor3d merged 1 commit into
mainfrom
feat/2233-source-field
Aug 6, 2026
Merged

feat(results): disclose which tier answered a wallet balance read#5
MichaelTaylor3d merged 1 commit into
mainfrom
feat/2233-source-field

Conversation

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor

Contract half of dig_ecosystem#2233 (S1 of the #2232 sync plan). The dig-node half is DIG-Network/dig-node#189.

What changed

  • WalletBalanceResult gains source: Option<WalletReadSource>"db" (the node's own chain replica) or "fallback" (a third-party coinset HTTP oracle).
  • New WalletReadSource enum, lowercase wire tokens, pinned by a KAT.
  • synced / peak_height are documented as properties of the ANSWER, not the node: a "fallback" answer reports synced: false / peak_height: null regardless of the node's own replica state.
  • SPEC.md wire-shape section + the method catalog row updated in the same commit.
  • 0.4.0 → 0.5.0 (minor: additive field + additive public type, no existing field removed or repurposed).

Why here and not in dig-node

dig-node does not currently depend on this crate — it hand-rolls the balance JSON. dig-app is the consumer. A source field defined only in dig-node would become a second literal for dig-app to mirror by hand, which is precisely the drift #2214 documents for the -3204x wallet error codes. Defining it once, here, is the whole reason this crate exists.

Additive in both directions

source is Option, and serde treats a missing Option field as None, so a payload from a node predating tier disclosure parses with the tier UNKNOWN — a third state, never a defaulted tier. Consumers reading only {balance} are unaffected (the existing superset KAT still passes).

Note #[serde(default)] is deliberately NOT written: it is a no-op on an Option, and a redundant attribute implying the compatibility comes from the attribute rather than the type is misleading. This was verified, not assumed — see MUT5 below.

Blast radius checked

WalletBalanceResult — grepped every construction and consumption site in the crate: src/results.rs (definition), src/kats.rs:230 (superset KAT), src/kats.rs:469 (mock handler), src/params.rs:230 (control_call! binding). No other repo in the ecosystem consumes this type today (dig-node hand-rolls the JSON; dig-app reads only {balance}), so the blast radius is this crate plus the two documented readers, neither of which breaks. gitnexus is disabled per the standing §2.0 override; radius established by grep + direct read.

Evidence

cargo fmt --check clean, cargo clippy --all-targets -- -D warnings clean, 60 unit tests + 1 doctest green.

Mutation battery (each mutation read back off disk before running):

# Mutation Result
MUT5 Remove #[serde(default)] from source SURVIVED — correctly, it is a no-op on Option. Attribute removed as dead noise rather than kept as a false guard.
MUT6 Option<WalletReadSource> → required WalletReadSource (the naive additive field) KILLED at compile time in a_pre_disclosure_nodes_payload_still_parses_with_the_tier_unknown — the test pins the tri-state at the type level.

The load-bearing fixture is a_pre_disclosure_nodes_payload_still_parses_with_the_tier_unknown, whose JSON deliberately OMITS the source key. A round-trip test that supplies the field in every fixture cannot see a required-field regression at all — the break would surface only against a real older node.

git diff --shortstat and --ignore-cr-at-eol agree (98/12), so no CRLF flip.

Refs dig_ecosystem#2233

Adds `source: "db" | "fallback"` to `WalletBalanceResult`, and tightens
`synced`/`peak_height` to describe the tier that answered rather than the node.

A wallet read is served either from the node's own chain replica or from a
third-party coinset HTTP oracle, and the two are not interchangeable: the oracle
path discloses the queried address off-node. Until now nothing on the wire said
which one answered, so "the node answered from its own chain state" was not a
falsifiable claim -- only a sync-progress flag was observable, and a flag can flip
while the oracle keeps answering.

Defining it here rather than in dig-node is the point: dig-app is the consumer, and
a field defined ad hoc in the node would become a second literal for dig-app to
mirror by hand -- the exact drift #2214 documents for the -3204x error codes.

Additive in both directions. `source` is `Option`, so a payload from a node
predating tier disclosure parses with the tier UNKNOWN rather than being rejected or
silently assigned a tier. Consumers reading only `{balance}` are unaffected.

Refs dig_ecosystem#2233

Co-Authored-By: Claude <noreply@anthropic.com>

@MichaelTaylor3d MichaelTaylor3d left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: PASS — reviewed at dbabab5008c3a5a4cfa561b29469467800394bbf.

What I verified rather than accepted:

  • The tri-state is real and compile-enforced. source: Option<WalletReadSource> plus assert_eq!(parsed.source, None, ...) in a_pre_disclosure_nodes_payload_still_parses_with_the_tier_unknown means MUT6 (a required field) cannot compile and cannot deserialize the legacy fixture — two independent failures. Absent is distinguishable from both "db" and "fallback" at the consumer, exactly as claimed. The fixture that carries the property is the one that OMITS the key, which is the right shape: a suite that supplied source everywhere would be blind to the regression.
  • MUT5's survival is diagnosed correctly, and removing the attribute was the right response. The compatibility genuinely lives in Option — serde's Option field defaults to None on absence with no attribute — so #[serde(default)] is a no-op whose removal survives by construction. Keeping it would have been a guard that cannot fail, misdirecting the next reader about where the property lives. The doc comment on source now says so explicitly, which is better than the attribute was.
  • Wire spelling placement. Confirmed independently on the dig-node side: git grep dig-node-control-interface -- '*.toml' at dig-node#189's head is empty, so dig-node takes no dependency on this crate and no release-first ordering binds. Defining the token here rather than as a dig-node literal is correct and is the #2214 lesson applied.
  • Superset guarantee intactnode_balance_superset_is_readable_by_dig_apps_balance_struct still passes with the new field present, so dig-app is unaffected until it opts in.
  • SPEC coherence: the method-catalog row and the WalletBalanceResult wire-shape section were both updated, and the old rule's phrasing ({balance, pending, synced, peak_height}) does not survive anywhere in the crate — I swept for it.
  • --shortstat matches --ignore-cr-at-eol (98/12). Refs, not a qualified Closes.

Two non-gating notes are attached inline; I am resolving both myself so neither blocks merge.

Comment thread src/results.rs
Comment thread src/results.rs
@MichaelTaylor3d
MichaelTaylor3d merged commit 9dd1fbe into main Aug 6, 2026
8 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the feat/2233-source-field branch August 6, 2026 07:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant