feat(results): disclose which tier answered a wallet balance read - #5
Merged
Conversation
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>
This was referenced Aug 6, 2026
Merged
MichaelTaylor3d
commented
Aug 6, 2026
MichaelTaylor3d
left a comment
Contributor
Author
There was a problem hiding this comment.
VERDICT: PASS — reviewed at dbabab5008c3a5a4cfa561b29469467800394bbf.
What I verified rather than accepted:
- The tri-state is real and compile-enforced.
source: Option<WalletReadSource>plusassert_eq!(parsed.source, None, ...)ina_pre_disclosure_nodes_payload_still_parses_with_the_tier_unknownmeans 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 suppliedsourceeverywhere 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'sOptionfield defaults toNoneon 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 onsourcenow 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 intact —
node_balance_superset_is_readable_by_dig_apps_balance_structstill passes with the new field present, so dig-app is unaffected until it opts in. - SPEC coherence: the method-catalog row and the
WalletBalanceResultwire-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. --shortstatmatches--ignore-cr-at-eol(98/12).Refs, not a qualifiedCloses.
Two non-gating notes are attached inline; I am resolving both myself so neither blocks merge.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Contract half of dig_ecosystem#2233 (S1 of the #2232 sync plan). The dig-node half is DIG-Network/dig-node#189.
What changed
WalletBalanceResultgainssource: Option<WalletReadSource>—"db"(the node's own chain replica) or"fallback"(a third-party coinset HTTP oracle).WalletReadSourceenum, lowercase wire tokens, pinned by a KAT.synced/peak_heightare documented as properties of the ANSWER, not the node: a"fallback"answer reportssynced: false/peak_height: nullregardless of the node's own replica state.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
sourcefield 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-3204xwallet error codes. Defining it once, here, is the whole reason this crate exists.Additive in both directions
sourceisOption, and serde treats a missingOptionfield asNone, 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 anOption, 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 --checkclean,cargo clippy --all-targets -- -D warningsclean, 60 unit tests + 1 doctest green.Mutation battery (each mutation read back off disk before running):
#[serde(default)]fromsourceOption. Attribute removed as dead noise rather than kept as a false guard.Option<WalletReadSource>→ requiredWalletReadSource(the naive additive field)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 thesourcekey. 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 --shortstatand--ignore-cr-at-eolagree (98/12), so no CRLF flip.Refs dig_ecosystem#2233