Skip to content

feat: Add Serialize to response types - #20

Open
evanlinjin wants to merge 1 commit into
masterfrom
feat/serialize-response-types
Open

feat: Add Serialize to response types#20
evanlinjin wants to merge 1 commit into
masterfrom
feat/serialize-response-types

Conversation

@evanlinjin

Copy link
Copy Markdown
Member

Description

Every type in src/response.rs derived serde::Deserialize only. That makes the responses decodable but not persistable.

The concrete motivation is downstream: bdk_electrum_streaming caches response::Tx values (a script's Electrum history) and now wants to persist that cache. Because response::Tx and friends are deserialize-only, it had to define a private mirror enum plus hand-written conversions in both directions purely to get the data back out. Deriving Serialize upstream removes that workaround entirely.

What this does

Adds serde::Serialize to every type in src/response.rs, plus PartialEq/Eq on the ones that lacked them so round trips can be asserted.

This is not just a derive change. Several fields decode through helpers in src/custom_serde.rs, and each one needed a matching serializer so the output is still valid Electrum wire data rather than the Rust type's own serde representation:

deserializer new serializer notes
from_consensus_hex to_consensus_hex
from_cancat_consensus_hex to_cancat_consensus_hex re-concatenates before hex-encoding
feerate_opt_from_btc_per_kb feerate_opt_to_btc_per_kb None is written back as -1.0
feerate_from_sat_per_byte feerate_to_sat_per_byte
weight_from_vb weight_to_vb
amount_from_btc amount_to_btc
amount_from_sats amount_to_sats integer sats, not Amount's own serde
amount_from_maybe_negative_sats amount_to_maybe_negative_sats
all_inputs_confirmed_bool_from_height all_inputs_confirmed_bool_to_height writes the height field as 0 / -1, never a bool

The existing #[serde(rename = ...)] attributes (hex, tx_hash) already apply to both directions, so field names are unchanged.

Tests

  • round_trip_responses: for every type, value → serde_json::to_value → deserialize → assert_eq! against the original.
  • round_trip_tx_preserves_variant: Tx is #[serde(untagged)], so this checks that both variants land back on themselves. They do — MempoolTx is tried first and only matches when fee is present, and ConfirmedTx catches the rest.
  • mempool_tx_serializes_height_not_bool: asserts the exact JSON shape, pinning height: 0 / height: -1. This is the case most likely to regress.

cargo test, cargo clippy --all-targets and cargo fmt --check all pass.

For the reviewer to decide

  • amount_from_maybe_negative_sats is lossy in a way that cannot be undone. GetBalanceResp::unconfirmed is documented as possibly negative, but the deserializer calls .unsigned_abs(), so the sign is discarded at decode time and Amount has no way to represent it. amount_to_maybe_negative_sats therefore always writes a non-negative number. A Rust-value round trip is lossless; a JSON→JSON round trip of a negative balance is not. Fixing that properly means changing unconfirmed to SignedAmount, which is a breaking change, so I left it alone and documented the asymmetry on the helper.
  • Float-backed conversions are approximate in both directions. EstimateFeeResp and FeePair go through f32; the tests use values that are exactly representable (0.001 BTC/kvB, 1 sat/vB), but arbitrary server-supplied rates may not survive a round trip bit-for-bit. This is pre-existing on the decode side; serializing just makes it visible in both directions.
  • Scope. I only touched src/response.rs as requested. The Deserialize-only types in src/protocol.rs and src/notification.rs are untouched — happy to extend if you want them too.

Checklists

All Submissions:

  • I've signed all my commits
  • I followed the contribution guidelines
  • I ran cargo test, cargo clippy --all-targets and cargo fmt --check before pushing

🤖 Generated with Claude Code

Every type in `response` derived `Deserialize` only, so a downstream
crate that wanted to persist a cached response (e.g. a script's history
of `response::Tx`) had to define a mirror type and hand-write the
conversions just to get the data back out.

Several fields decode through `custom_serde` helpers, so each one gains
a symmetrically-named serializer that writes the Electrum wire
representation back out rather than the Rust type's own serde:

- `to_consensus_hex` / `to_cancat_consensus_hex`
- `feerate_opt_to_btc_per_kb` (writes `-1.0` for `None`)
- `feerate_to_sat_per_byte`
- `weight_to_vb`
- `amount_to_btc` / `amount_to_sats` / `amount_to_maybe_negative_sats`
- `all_inputs_confirmed_bool_to_height` (writes `0` / `-1`, not a bool)

`PartialEq`/`Eq` are derived on the types that lacked them so the round
trip can be asserted in tests.

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

@noahjoeris noahjoeris left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Have a look at PR #17 which removes the need for those helpers:

  • amount_from_btc / amount_to_btc
  • amount_from_sats / amount_to_sats
  • amount_from_maybe_negative_sats / amount_to_maybe_negative_sats

Comment thread src/custom_serde.rs
Ok(items)
}

pub fn to_cancat_consensus_hex<T, S>(values: &[T], serializer: S) -> Result<S::Ok, S::Error>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: this should be _concat_ right?

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.

2 participants