Add serde support for Vec<(Signed)Amount>
What changed, and why it matters
This commit adds new Rust code that lets programs serialize and deserialize lists (vectors) of Bitcoin amounts when using the serde data-format library. It is a feature addition with matching tests. There is no indication it fixes a security bug or introduces a vulnerability.
No security action required; review as normal feature code.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change extends the existing amount::serde helper modules (as_sat, as_btc, as_str) with new vec submodules that serialize and deserialize Vec<Amount> and Vec<SignedAmount> by delegating each element to the existing single-value helpers. It also adds unit tests using serde_test tokens. The implementation is straightforward and gated behind the alloc feature.
Changed components
units/src/amount/serde.rsunits/tests/serde.rsInspect captured patch +307 / −0
diff --git a/units/src/amount/serde.rs b/units/src/amount/serde.rs
index cdb952da..f8b0f4db 100644
--- a/units/src/amount/serde.rs
+++ b/units/src/amount/serde.rs
@@ -145,6 +145,73 @@ pub mod as_sat {
d.deserialize_option(VisitOptAmt::<A>(PhantomData))
}
}
+
+ #[cfg(feature = "alloc")]
+ pub mod vec {
+ //! Serialize and deserialize `Vec<Amount>` and `Vec<SignedAmount>` as real numbers
+ //! denominated in satoshi.
+ //!
+ //! Use with `#[serde(with = "amount::serde::as_sat::vec")]`.
+
+ use alloc::vec::Vec;
+ use core::fmt;
+ use core::marker::PhantomData;
+
+ use serde::de::{self, SeqAccess};
+ use serde::ser::SerializeSeq;
+ use serde::{Deserialize, Deserializer, Serializer};
+
+ use crate::SignedAmount;
+
+ pub fn serialize<A, S: Serializer>(a: &[A], s: S) -> Result<S::Ok, S::Error>
+ where
+ A: Into<SignedAmount> + Copy,
+ {
+ let mut seq = s.serialize_seq(Some(a.len()))?;
+ for amount in a {
+ let signed_amount: SignedAmount = (*amount).into();
+ seq.serialize_element(&signed_amount.to_sat())?;
+ }
+ seq.end()
+ }
+
+ pub fn deserialize<'d, A, D: Deserializer<'d>>(d: D) -> Result<Vec<A>, D::Error>
+ where
+ A: TryFrom<SignedAmount>,
+ <A as TryFrom<SignedAmount>>::Error: core::fmt::Display,
+ {
+ struct VisitVec<X>(PhantomData<X>);
+
+ impl<'de, X> de::Visitor<'de> for VisitVec<X>
+ where
+ X: TryFrom<SignedAmount>,
+ <X as TryFrom<SignedAmount>>::Error: core::fmt::Display,
+ {
+ type Value = Vec<X>;
+
+ fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "a sequence of i64")
+ }
+
+ fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
+ where
+ A: SeqAccess<'de>,
+ {
+ #[derive(Deserialize)]
+ #[serde(transparent)]
+ struct Wrapper(#[serde(with = "super")] SignedAmount);
+
+ let mut out = Vec::with_capacity(seq.size_hint().unwrap_or(0));
+ while let Some(wrapped) = seq.next_element::<Wrapper>()? {
+ out.push(X::try_from(wrapped.0).map_err(de::Error::custom)?);
+ }
+ Ok(out)
+ }
+ }
+
+ d.deserialize_seq(VisitVec::<A>(PhantomData))
+ }
+ }
}
#[cfg(feature = "alloc")]
@@ -244,6 +311,72 @@ pub mod as_btc {
d.deserialize_option(VisitOptAmt::<A>(PhantomData))
}
}
+
+ pub mod vec {
+ //! Serialize and deserialize `Vec<Amount>` and `Vec<SignedAmount>` as JSON numbers
+ //! denominated in BTC.
+ //!
+ //! Use with `#[serde(with = "amount::serde::as_btc::vec")]`.
+
+ use alloc::vec::Vec;
+ use core::fmt;
+ use core::marker::PhantomData;
+
+ use serde::de::{self, SeqAccess};
+ use serde::ser::SerializeSeq;
+ use serde::{Deserialize, Deserializer, Serializer};
+
+ use crate::amount::{Denomination, SignedAmount};
+
+ pub fn serialize<A, S: Serializer>(a: &[A], s: S) -> Result<S::Ok, S::Error>
+ where
+ A: Into<SignedAmount> + Copy,
+ {
+ let mut seq = s.serialize_seq(Some(a.len()))?;
+ for amount in a {
+ let signed_amount: SignedAmount = (*amount).into();
+ seq.serialize_element(&signed_amount.to_float_in(Denomination::Bitcoin))?;
+ }
+ seq.end()
+ }
+
+ pub fn deserialize<'d, A, D: Deserializer<'d>>(d: D) -> Result<Vec<A>, D::Error>
+ where
+ A: TryFrom<SignedAmount>,
+ <A as TryFrom<SignedAmount>>::Error: core::fmt::Display,
+ {
+ struct VisitVec<X>(PhantomData<X>);
+
+ impl<'de, X> de::Visitor<'de> for VisitVec<X>
+ where
+ X: TryFrom<SignedAmount>,
+ <X as TryFrom<SignedAmount>>::Error: core::fmt::Display,
+ {
+ type Value = Vec<X>;
+
+ fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "a sequence of f64")
+ }
+
+ fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
+ where
+ A: SeqAccess<'de>,
+ {
+ #[derive(Deserialize)]
+ #[serde(transparent)]
+ struct Wrapper(#[serde(with = "super")] SignedAmount);
+
+ let mut out = Vec::with_capacity(seq.size_hint().unwrap_or(0));
+ while let Some(wrapped) = seq.next_element::<Wrapper>()? {
+ out.push(X::try_from(wrapped.0).map_err(de::Error::custom)?);
+ }
+ Ok(out)
+ }
+ }
+
+ d.deserialize_seq(VisitVec::<A>(PhantomData))
+ }
+ }
}
#[cfg(feature = "alloc")]
@@ -345,6 +478,72 @@ pub mod as_str {
d.deserialize_option(VisitOptAmt::<A>(PhantomData))
}
}
+
+ pub mod vec {
+ //! Serialize and deserialize `Vec<Amount>` and `Vec<SignedAmount>` as JSON strings
+ //! denominated in BTC.
+ //!
+ //! Use with `#[serde(with = "amount::serde::as_str::vec")]`.
+
+ use alloc::vec::Vec;
+ use core::fmt;
+ use core::marker::PhantomData;
+
+ use serde::de::{self, SeqAccess};
+ use serde::ser::SerializeSeq;
+ use serde::{Deserialize, Deserializer, Serializer};
+
+ use crate::amount::{Denomination, SignedAmount};
+
+ pub fn serialize<A, S: Serializer>(a: &[A], s: S) -> Result<S::Ok, S::Error>
+ where
+ A: Into<SignedAmount> + Copy,
+ {
+ let mut seq = s.serialize_seq(Some(a.len()))?;
+ for amount in a {
+ let signed_amount: SignedAmount = (*amount).into();
+ seq.serialize_element(&signed_amount.to_string_in(Denomination::Bitcoin))?;
+ }
+ seq.end()
+ }
+
+ pub fn deserialize<'d, A, D: Deserializer<'d>>(d: D) -> Result<Vec<A>, D::Error>
+ where
+ A: TryFrom<SignedAmount>,
+ <A as TryFrom<SignedAmount>>::Error: core::fmt::Display,
+ {
+ struct VisitVec<X>(PhantomData<X>);
+
+ impl<'de, X> de::Visitor<'de> for VisitVec<X>
+ where
+ X: TryFrom<SignedAmount>,
+ <X as TryFrom<SignedAmount>>::Error: core::fmt::Display,
+ {
+ type Value = Vec<X>;
+
+ fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "a sequence of String")
+ }
+
+ fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
+ where
+ A: SeqAccess<'de>,
+ {
+ #[derive(Deserialize)]
+ #[serde(transparent)]
+ struct Wrapper(#[serde(with = "super")] SignedAmount);
+
+ let mut out = Vec::with_capacity(seq.size_hint().unwrap_or(0));
+ while let Some(wrapped) = seq.next_element::<Wrapper>()? {
+ out.push(X::try_from(wrapped.0).map_err(de::Error::custom)?);
+ }
+ Ok(out)
+ }
+ }
+
+ d.deserialize_seq(VisitVec::<A>(PhantomData))
+ }
+ }
}
#[cfg(test)]
diff --git a/units/tests/serde.rs b/units/tests/serde.rs
index 13162370..7ebb493b 100644
--- a/units/tests/serde.rs
+++ b/units/tests/serde.rs
@@ -133,6 +133,42 @@ fn serde_amount_as_sat() {
);
}
+#[test]
+#[cfg(feature = "serde")]
+#[cfg(feature = "alloc")]
+fn serde_amount_as_sat_vec() {
+ #[derive(Serialize, Deserialize, PartialEq, Debug)]
+ struct T {
+ #[serde(with = "crate::amount::serde::as_sat::vec")]
+ pub amt: Vec<Amount>,
+ #[serde(with = "crate::amount::serde::as_sat::vec")]
+ pub samt: Vec<SignedAmount>,
+ }
+
+ serde_test::assert_tokens(
+ &T {
+ amt: vec![sat(123), sat(456), sat(789)],
+ samt: vec![ssat(-123), ssat(-456), ssat(-789)],
+ },
+ &[
+ serde_test::Token::Struct { name: "T", len: 2 },
+ serde_test::Token::Str("amt"),
+ serde_test::Token::Seq { len: Some(3) },
+ serde_test::Token::I64(123),
+ serde_test::Token::I64(456),
+ serde_test::Token::I64(789),
+ serde_test::Token::SeqEnd,
+ serde_test::Token::Str("samt"),
+ serde_test::Token::Seq { len: Some(3) },
+ serde_test::Token::I64(-123),
+ serde_test::Token::I64(-456),
+ serde_test::Token::I64(-789),
+ serde_test::Token::SeqEnd,
+ serde_test::Token::StructEnd,
+ ],
+ );
+}
+
#[test]
#[cfg(feature = "serde")]
#[cfg(feature = "alloc")]
@@ -159,6 +195,42 @@ fn serde_amount_as_btc() {
assert_eq!(t, serde_json::from_value(value).unwrap());
}
+#[test]
+#[cfg(feature = "serde")]
+#[cfg(feature = "alloc")]
+fn serde_amount_as_btc_vec() {
+ #[derive(Serialize, Deserialize, PartialEq, Debug)]
+ struct T {
+ #[serde(with = "crate::amount::serde::as_btc::vec")]
+ pub amt: Vec<Amount>,
+ #[serde(with = "crate::amount::serde::as_btc::vec")]
+ pub samt: Vec<SignedAmount>,
+ }
+
+ serde_test::assert_tokens(
+ &T {
+ amt: vec![sat(123), sat(456), sat(789)],
+ samt: vec![ssat(-123), ssat(-456), ssat(-789)],
+ },
+ &[
+ serde_test::Token::Struct { name: "T", len: 2 },
+ serde_test::Token::Str("amt"),
+ serde_test::Token::Seq { len: Some(3) },
+ serde_test::Token::F64(0.000_001_23),
+ serde_test::Token::F64(0.000_004_56),
+ serde_test::Token::F64(0.000_007_89),
+ serde_test::Token::SeqEnd,
+ serde_test::Token::Str("samt"),
+ serde_test::Token::Seq { len: Some(3) },
+ serde_test::Token::F64(-0.000_001_23),
+ serde_test::Token::F64(-0.000_004_56),
+ serde_test::Token::F64(-0.000_007_89),
+ serde_test::Token::SeqEnd,
+ serde_test::Token::StructEnd,
+ ],
+ );
+}
+
#[test]
#[cfg(feature = "serde")]
#[cfg(feature = "alloc")]
@@ -184,6 +256,42 @@ fn serde_amount_as_str() {
);
}
+#[test]
+#[cfg(feature = "serde")]
+#[cfg(feature = "alloc")]
+fn serde_amount_as_str_vec() {
+ #[derive(Serialize, Deserialize, PartialEq, Debug)]
+ struct T {
+ #[serde(with = "crate::amount::serde::as_str::vec")]
+ pub amt: Vec<Amount>,
+ #[serde(with = "crate::amount::serde::as_str::vec")]
+ pub samt: Vec<SignedAmount>,
+ }
+
+ serde_test::assert_tokens(
+ &T {
+ amt: vec![sat(123), sat(456), sat(789)],
+ samt: vec![ssat(-123), ssat(-456), ssat(-789)],
+ },
+ &[
+ serde_test::Token::Struct { name: "T", len: 2 },
+ serde_test::Token::String("amt"),
+ serde_test::Token::Seq { len: Some(3) },
+ serde_test::Token::String("0.00000123"),
+ serde_test::Token::String("0.00000456"),
+ serde_test::Token::String("0.00000789"),
+ serde_test::Token::SeqEnd,
+ serde_test::Token::String("samt"),
+ serde_test::Token::Seq { len: Some(3) },
+ serde_test::Token::String("-0.00000123"),
+ serde_test::Token::String("-0.00000456"),
+ serde_test::Token::String("-0.00000789"),
+ serde_test::Token::SeqEnd,
+ serde_test::Token::StructEnd,
+ ],
+ );
+}
+
#[test]
#[cfg(feature = "serde")]
#[cfg(feature = "alloc")]
Why this scored 16/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.