1use std::fmt::Display;
7use std::ops::Rem;
8use std::str::FromStr;
9
10use anyhow::Context;
11use fake::Dummy;
12use pathfinder_crypto::hash::HashChain;
13use pathfinder_crypto::Felt;
14use primitive_types::H160;
15use serde::{Deserialize, Serialize};
16
17pub mod casm_class;
18pub mod class_definition;
19pub mod consensus_info;
20pub mod consts;
21pub mod event;
22pub mod hash;
23mod header;
24pub mod integration_testing;
25mod l1;
26mod l2;
27mod macros;
28pub mod prelude;
29pub mod receipt;
30pub mod signature;
31pub mod state_update;
32pub mod test_utils;
33pub mod transaction;
34pub mod trie;
35
36pub use header::{BlockHeader, BlockHeaderBuilder, L1DataAvailabilityMode, SignedBlockHeader};
37pub use l1::{L1BlockHash, L1BlockNumber, L1TransactionHash};
38pub use l2::{
39 ConsensusFinalizedBlockHeader,
40 ConsensusFinalizedL2Block,
41 DecidedBlock,
42 DecidedBlocks,
43 DeclaredClass,
44 L2Block,
45 L2BlockToCommit,
46};
47pub use signature::BlockCommitmentSignature;
48pub use state_update::{FoundStorageValue, StateUpdate};
49
50impl ContractAddress {
51 pub const ONE: ContractAddress = contract_address!("0x1");
57 pub const TWO: ContractAddress = contract_address!("0x2");
66 pub const SYSTEM: [ContractAddress; 2] = [ContractAddress::ONE, ContractAddress::TWO];
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72pub struct ContractClass {
73 pub program: String,
75 pub entry_points_by_type: serde_json::Value,
80}
81
82impl EntryPoint {
83 pub fn hashed(input: &[u8]) -> Self {
88 use sha3::Digest;
89 EntryPoint(truncated_keccak(<[u8; 32]>::from(sha3::Keccak256::digest(
90 input,
91 ))))
92 }
93
94 pub const CONSTRUCTOR: Self =
97 entry_point!("0x028FFE4FF0F226A9107253E17A904099AA4F63A02A5621DE0576E5AA71BC5194");
98}
99
100impl StateCommitment {
101 pub fn calculate(
113 storage_commitment: StorageCommitment,
114 class_commitment: ClassCommitment,
115 version: StarknetVersion,
116 ) -> Self {
117 if class_commitment == ClassCommitment::ZERO
118 && storage_commitment == StorageCommitment::ZERO
119 {
120 return StateCommitment::ZERO;
121 }
122
123 if class_commitment == ClassCommitment::ZERO && version < StarknetVersion::V_0_14_0 {
124 return Self(storage_commitment.0);
125 }
126
127 const GLOBAL_STATE_VERSION: Felt = felt_bytes!(b"STARKNET_STATE_V0");
128
129 StateCommitment(
130 pathfinder_crypto::hash::poseidon::poseidon_hash_many(&[
131 GLOBAL_STATE_VERSION.into(),
132 storage_commitment.0.into(),
133 class_commitment.0.into(),
134 ])
135 .into(),
136 )
137 }
138}
139
140impl StorageAddress {
141 pub fn from_name(input: &[u8]) -> Self {
142 use sha3::Digest;
143 Self(truncated_keccak(<[u8; 32]>::from(sha3::Keccak256::digest(
144 input,
145 ))))
146 }
147
148 pub fn from_map_name_and_key(name: &[u8], key: Felt) -> Self {
149 use sha3::Digest;
150
151 let intermediate = truncated_keccak(<[u8; 32]>::from(sha3::Keccak256::digest(name)));
152 let value = pathfinder_crypto::hash::pedersen_hash(intermediate, key);
153
154 let value = primitive_types::U256::from_big_endian(value.as_be_bytes());
155 let max_address = primitive_types::U256::from_str_radix(
156 "0x7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00",
157 16,
158 )
159 .unwrap();
160
161 let value = value.rem(max_address);
162 let mut b = [0u8; 32];
163 value.to_big_endian(&mut b);
164 Self(Felt::from_be_slice(&b).expect("Truncated value should fit into a felt"))
165 }
166}
167
168#[derive(Copy, Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
170pub struct BlockNumber(u64);
171
172macros::i64_backed_u64::new_get_partialeq!(BlockNumber);
173macros::i64_backed_u64::serdes!(BlockNumber);
174
175impl From<BlockNumber> for Felt {
176 fn from(x: BlockNumber) -> Self {
177 Felt::from(x.0)
178 }
179}
180
181impl std::iter::Iterator for BlockNumber {
182 type Item = BlockNumber;
183
184 fn next(&mut self) -> Option<Self::Item> {
185 Some(*self + 1)
186 }
187}
188
189#[derive(Copy, Debug, Clone, PartialEq, Eq, Default)]
191pub struct BlockTimestamp(u64);
192
193macros::i64_backed_u64::new_get_partialeq!(BlockTimestamp);
194macros::i64_backed_u64::serdes!(BlockTimestamp);
195
196#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
198pub struct TransactionIndex(u64);
199
200macros::i64_backed_u64::new_get_partialeq!(TransactionIndex);
201macros::i64_backed_u64::serdes!(TransactionIndex);
202
203#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
205pub struct GasPrice(pub u128);
206
207#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
209pub struct GasPriceHex(pub GasPrice);
210
211#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
213pub struct ResourceAmount(pub u64);
214
215#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
218pub struct Tip(pub u64);
219
220#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Dummy)]
222pub struct TipHex(pub Tip);
223
224#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
226pub struct ResourcePricePerUnit(pub u128);
227
228#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, Default, Dummy)]
230pub struct TransactionVersion(pub Felt);
231
232impl TransactionVersion {
233 pub fn is_zero(&self) -> bool {
235 self.without_query_version() == 0
236 }
237
238 pub fn without_query_version(&self) -> u128 {
244 let lower = &self.0.as_be_bytes()[16..];
245 u128::from_be_bytes(lower.try_into().expect("slice should be the right length"))
246 }
247
248 pub const fn with_query_version(self) -> Self {
249 let mut bytes = self.0.to_be_bytes();
250 bytes[15] |= 0b0000_0001;
251
252 let felt = match Felt::from_be_bytes(bytes) {
253 Ok(x) => x,
254 Err(_) => panic!("Adding query bit to transaction version failed."),
255 };
256 Self(felt)
257 }
258
259 pub const fn has_query_version(&self) -> bool {
260 self.0.as_be_bytes()[15] & 0b0000_0001 != 0
261 }
262
263 pub fn with_query_only(self, query_only: bool) -> Self {
264 if query_only {
265 self.with_query_version()
266 } else {
267 Self(self.without_query_version().into())
268 }
269 }
270
271 pub const ZERO: Self = Self(Felt::ZERO);
272 pub const ONE: Self = Self(Felt::from_u64(1));
273 pub const TWO: Self = Self(Felt::from_u64(2));
274 pub const THREE: Self = Self(Felt::from_u64(3));
275 pub const ZERO_WITH_QUERY_VERSION: Self = Self::ZERO.with_query_version();
276 pub const ONE_WITH_QUERY_VERSION: Self = Self::ONE.with_query_version();
277 pub const TWO_WITH_QUERY_VERSION: Self = Self::TWO.with_query_version();
278 pub const THREE_WITH_QUERY_VERSION: Self = Self::THREE.with_query_version();
279}
280
281#[derive(Debug, Copy, Clone, PartialEq, Eq)]
285pub enum BlockId {
286 Number(BlockNumber),
287 Hash(BlockHash),
288 Latest,
289}
290
291impl BlockId {
292 pub fn is_latest(&self) -> bool {
293 self == &Self::Latest
294 }
295}
296
297impl BlockNumber {
298 pub const GENESIS: BlockNumber = BlockNumber::new_or_panic(0);
299 pub const MAX: BlockNumber = BlockNumber::new_or_panic(i64::MAX as u64);
302
303 pub fn parent(&self) -> Option<Self> {
306 if self == &Self::GENESIS {
307 None
308 } else {
309 Some(*self - 1)
310 }
311 }
312
313 pub fn is_zero(&self) -> bool {
314 self == &Self::GENESIS
315 }
316
317 pub fn checked_add(&self, rhs: u64) -> Option<Self> {
318 Self::new(self.0.checked_add(rhs)?)
319 }
320
321 pub fn checked_sub(&self, rhs: u64) -> Option<Self> {
322 self.0.checked_sub(rhs).map(Self)
323 }
324
325 pub fn saturating_sub(&self, rhs: u64) -> Self {
326 Self(self.0.saturating_sub(rhs))
327 }
328}
329
330impl std::ops::Add<u64> for BlockNumber {
331 type Output = BlockNumber;
332
333 fn add(self, rhs: u64) -> Self::Output {
334 Self(self.0 + rhs)
335 }
336}
337
338impl std::ops::AddAssign<u64> for BlockNumber {
339 fn add_assign(&mut self, rhs: u64) {
340 self.0 += rhs;
341 }
342}
343
344impl std::ops::Sub<u64> for BlockNumber {
345 type Output = BlockNumber;
346
347 fn sub(self, rhs: u64) -> Self::Output {
348 Self(self.0 - rhs)
349 }
350}
351
352impl std::ops::SubAssign<u64> for BlockNumber {
353 fn sub_assign(&mut self, rhs: u64) {
354 self.0 -= rhs;
355 }
356}
357
358#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize)]
360pub struct EthereumAddress(pub H160);
361
362impl<T> Dummy<T> for EthereumAddress {
363 fn dummy_with_rng<R: rand::Rng + ?Sized>(_: &T, rng: &mut R) -> Self {
364 Self(H160::random_using(rng))
365 }
366}
367
368#[derive(Debug, thiserror::Error)]
369#[error("expected slice length of 16 or less, got {0}")]
370pub struct FromSliceError(usize);
371
372impl GasPrice {
373 pub const ZERO: GasPrice = GasPrice(0u128);
374
375 pub fn to_be_bytes(&self) -> [u8; 16] {
377 self.0.to_be_bytes()
378 }
379
380 pub fn from_be_bytes(src: [u8; 16]) -> Self {
383 Self(u128::from_be_bytes(src))
384 }
385
386 pub fn from_be_slice(src: &[u8]) -> Result<Self, FromSliceError> {
389 if src.len() > 16 {
390 return Err(FromSliceError(src.len()));
391 }
392
393 let mut buf = [0u8; 16];
394 buf[16 - src.len()..].copy_from_slice(src);
395
396 Ok(Self::from_be_bytes(buf))
397 }
398}
399
400impl From<u64> for GasPrice {
401 fn from(src: u64) -> Self {
402 Self(u128::from(src))
403 }
404}
405
406impl TryFrom<Felt> for GasPrice {
407 type Error = anyhow::Error;
408
409 fn try_from(src: Felt) -> Result<Self, Self::Error> {
410 anyhow::ensure!(
411 src.as_be_bytes()[0..16] == [0; 16],
412 "Gas price fits into u128"
413 );
414
415 let mut bytes = [0u8; 16];
416 bytes.copy_from_slice(&src.as_be_bytes()[16..]);
417 Ok(Self(u128::from_be_bytes(bytes)))
418 }
419}
420
421impl From<BlockNumber> for BlockId {
422 fn from(number: BlockNumber) -> Self {
423 Self::Number(number)
424 }
425}
426
427impl From<BlockHash> for BlockId {
428 fn from(hash: BlockHash) -> Self {
429 Self::Hash(hash)
430 }
431}
432
433#[derive(Debug, Clone, Copy, PartialEq, Eq)]
435pub enum EthereumChain {
436 Mainnet,
437 Sepolia,
438 Other(primitive_types::U256),
439}
440
441#[derive(Debug, Clone, Copy, PartialEq, Eq)]
443pub enum Chain {
444 Mainnet,
445 SepoliaTestnet,
446 SepoliaIntegration,
447 Custom,
448}
449
450#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
451pub struct ChainId(pub Felt);
452
453impl ChainId {
454 const fn from_slice_unwrap(slice: &[u8]) -> Self {
456 Self(match Felt::from_be_slice(slice) {
457 Ok(v) => v,
458 Err(_) => panic!("Bad value"),
459 })
460 }
461
462 pub fn to_hex_str(&self) -> std::borrow::Cow<'static, str> {
465 self.0.to_hex_str()
466 }
467
468 pub fn as_str(&self) -> &str {
470 std::str::from_utf8(self.0.as_be_bytes())
471 .expect("valid utf8")
472 .trim_start_matches('\0')
473 }
474
475 pub const MAINNET: Self = Self::from_slice_unwrap(b"SN_MAIN");
476 pub const SEPOLIA_TESTNET: Self = Self::from_slice_unwrap(b"SN_SEPOLIA");
477 pub const SEPOLIA_INTEGRATION: Self = Self::from_slice_unwrap(b"SN_INTEGRATION_SEPOLIA");
478}
479
480impl std::fmt::Display for Chain {
481 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
482 match self {
483 Chain::Mainnet => f.write_str("Mainnet"),
484 Chain::SepoliaTestnet => f.write_str("Testnet/Sepolia"),
485 Chain::SepoliaIntegration => f.write_str("Integration/Sepolia"),
486 Chain::Custom => f.write_str("Custom"),
487 }
488 }
489}
490
491#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Dummy)]
492pub struct StarknetVersion(u8, u8, u8, u8);
493
494impl StarknetVersion {
495 pub const fn new(a: u8, b: u8, c: u8, d: u8) -> Self {
496 StarknetVersion(a, b, c, d)
497 }
498
499 pub fn as_u32(&self) -> u32 {
500 u32::from_le_bytes([self.0, self.1, self.2, self.3])
501 }
502
503 pub fn from_u32(version: u32) -> Self {
504 let [a, b, c, d] = version.to_le_bytes();
505 StarknetVersion(a, b, c, d)
506 }
507
508 #[inline]
509 pub fn major(&self) -> u8 {
510 self.0
511 }
512
513 #[inline]
514 pub fn minor(&self) -> u8 {
515 self.1
516 }
517
518 #[inline]
519 pub fn patch(&self) -> u8 {
520 self.2
521 }
522
523 pub const V_0_13_2: Self = Self::new(0, 13, 2, 0);
524
525 pub const V_0_13_4: Self = Self::new(0, 13, 4, 0);
528 pub const V_0_14_0: Self = Self::new(0, 14, 0, 0);
531 pub const V_0_14_1: Self = Self::new(0, 14, 1, 0);
532}
533
534impl FromStr for StarknetVersion {
535 type Err = anyhow::Error;
536
537 fn from_str(s: &str) -> Result<Self, Self::Err> {
538 if s.is_empty() {
539 return Ok(StarknetVersion::new(0, 0, 0, 0));
540 }
541
542 let parts: Vec<_> = s.split('.').collect();
543 anyhow::ensure!(
544 parts.len() == 3 || parts.len() == 4,
545 "Invalid version string, expected 3 or 4 parts but got {}",
546 parts.len()
547 );
548
549 let a = parts[0].parse()?;
550 let b = parts[1].parse()?;
551 let c = parts[2].parse()?;
552 let d = parts.get(3).map(|x| x.parse()).transpose()?.unwrap_or(0);
553
554 Ok(StarknetVersion(a, b, c, d))
555 }
556}
557
558impl Display for StarknetVersion {
559 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
560 if self.0 == 0 && self.1 == 0 && self.2 == 0 && self.3 == 0 {
561 return Ok(());
562 }
563 if self.3 == 0 {
564 write!(f, "{}.{}.{}", self.0, self.1, self.2)
565 } else {
566 write!(f, "{}.{}.{}.{}", self.0, self.1, self.2, self.3)
567 }
568 }
569}
570
571macros::felt_newtypes!(
572 [
573 AccountDeploymentDataElem,
574 BlockHash,
575 ByteCodeOffset,
576 BlockCommitmentSignatureElem,
577 CallParam,
578 CallResultValue,
579 ClassCommitment,
580 ClassCommitmentLeafHash,
581 ConstructorParam,
582 ContractAddressSalt,
583 ContractNonce,
584 ContractStateHash,
585 ContractRoot,
586 EntryPoint,
587 EventCommitment,
588 EventData,
589 EventKey,
590 Fee,
591 L1ToL2MessageNonce,
592 L1ToL2MessagePayloadElem,
593 L2ToL1MessagePayloadElem,
594 PaymasterDataElem,
595 ProofFactElem,
596 ProposalCommitment,
597 PublicKey,
598 SequencerAddress,
599 StateCommitment,
600 StateDiffCommitment,
601 StorageCommitment,
602 StorageValue,
603 TransactionCommitment,
604 ReceiptCommitment,
605 TransactionHash,
606 TransactionNonce,
607 TransactionSignatureElem,
608 ];
609 [
610 CasmHash,
611 ClassHash,
612 ContractAddress,
613 SierraHash,
614 StorageAddress,
615 ]
616);
617
618macros::fmt::thin_display!(BlockNumber);
619macros::fmt::thin_display!(BlockTimestamp);
620
621impl ContractAddress {
622 pub fn deployed_contract_address(
623 constructor_calldata: impl Iterator<Item = CallParam>,
624 contract_address_salt: &ContractAddressSalt,
625 class_hash: &ClassHash,
626 ) -> Self {
627 let constructor_calldata_hash = constructor_calldata
628 .fold(HashChain::default(), |mut h, param| {
629 h.update(param.0);
630 h
631 })
632 .finalize();
633
634 let contract_address = [
635 Felt::from_be_slice(b"STARKNET_CONTRACT_ADDRESS").expect("prefix is convertible"),
636 Felt::ZERO,
637 contract_address_salt.0,
638 class_hash.0,
639 constructor_calldata_hash,
640 ]
641 .into_iter()
642 .fold(HashChain::default(), |mut h, e| {
643 h.update(e);
644 h
645 })
646 .finalize();
647
648 const MAX_CONTRACT_ADDRESS: Felt =
650 felt!("0x7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00");
651 let contract_address = if contract_address >= MAX_CONTRACT_ADDRESS {
652 contract_address - MAX_CONTRACT_ADDRESS
653 } else {
654 contract_address
655 };
656
657 ContractAddress::new_or_panic(contract_address)
658 }
659
660 pub fn is_system_contract(&self) -> bool {
661 (*self == ContractAddress::ONE) || (*self == ContractAddress::TWO)
662 }
663}
664
665impl From<ContractAddress> for Vec<u8> {
666 fn from(value: ContractAddress) -> Self {
667 value.0.to_be_bytes().to_vec()
668 }
669}
670
671#[derive(Clone, Debug, PartialEq)]
672pub enum AllowedOrigins {
673 Any,
674 List(Vec<String>),
675}
676
677impl<S> From<S> for AllowedOrigins
678where
679 S: ToString,
680{
681 fn from(value: S) -> Self {
682 let s = value.to_string();
683
684 if s == "*" {
685 Self::Any
686 } else {
687 Self::List(vec![s])
688 }
689 }
690}
691
692pub fn truncated_keccak(mut plain: [u8; 32]) -> Felt {
695 plain[0] &= 0x03;
698 Felt::from_be_bytes(plain).expect("cannot overflow: smaller than modulus")
699}
700
701pub fn calculate_class_commitment_leaf_hash(
705 compiled_class_hash: CasmHash,
706) -> ClassCommitmentLeafHash {
707 const CONTRACT_CLASS_HASH_VERSION: pathfinder_crypto::Felt =
708 felt_bytes!(b"CONTRACT_CLASS_LEAF_V0");
709 ClassCommitmentLeafHash(
710 pathfinder_crypto::hash::poseidon_hash(
711 CONTRACT_CLASS_HASH_VERSION.into(),
712 compiled_class_hash.0.into(),
713 )
714 .into(),
715 )
716}
717
718#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
720pub struct Proof(pub Vec<u8>);
721
722impl Proof {
723 pub fn is_empty(&self) -> bool {
724 self.0.is_empty()
725 }
726}
727
728impl serde::Serialize for Proof {
729 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
730 use base64::Engine;
731
732 let encoded = base64::engine::general_purpose::STANDARD.encode(&self.0);
733 serializer.serialize_str(&encoded)
734 }
735}
736
737impl<'de> serde::Deserialize<'de> for Proof {
738 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
739 use base64::Engine;
740
741 let s = String::deserialize(deserializer)?;
742 if s.is_empty() {
743 return Ok(Proof::default());
744 }
745 let bytes = base64::engine::general_purpose::STANDARD
746 .decode(&s)
747 .map_err(serde::de::Error::custom)?;
748 Ok(Proof(bytes))
749 }
750}
751
752#[cfg(test)]
753mod tests {
754 use crate::{felt, CallParam, ClassHash, ContractAddress, ContractAddressSalt};
755
756 #[test]
757 fn constructor_entry_point() {
758 use sha3::{Digest, Keccak256};
759
760 use crate::{truncated_keccak, EntryPoint};
761
762 let mut keccak = Keccak256::default();
763 keccak.update(b"constructor");
764 let expected = EntryPoint(truncated_keccak(<[u8; 32]>::from(keccak.finalize())));
765
766 assert_eq!(EntryPoint::CONSTRUCTOR, expected);
767 }
768
769 mod starknet_version {
770 use std::str::FromStr;
771
772 use super::super::StarknetVersion;
773
774 #[test]
775 fn valid_version_parsing() {
776 let cases = [
777 ("1.2.3.4", "1.2.3.4", StarknetVersion::new(1, 2, 3, 4)),
778 ("1.2.3", "1.2.3", StarknetVersion::new(1, 2, 3, 0)),
779 ("1.2.3.0", "1.2.3", StarknetVersion::new(1, 2, 3, 0)),
780 ("", "", StarknetVersion::new(0, 0, 0, 0)),
781 ];
782
783 for (input, output, actual) in cases.iter() {
784 let version = StarknetVersion::from_str(input).unwrap();
785 assert_eq!(version, *actual);
786 assert_eq!(version.to_string(), *output);
787 }
788 }
789
790 #[test]
791 fn invalid_version_parsing() {
792 assert!(StarknetVersion::from_str("1.2").is_err());
793 assert!(StarknetVersion::from_str("1").is_err());
794 assert!(StarknetVersion::from_str("1.2.a").is_err());
795 }
796 }
797
798 #[test]
799 fn deployed_contract_address() {
800 let expected_contract_address = ContractAddress(felt!(
801 "0x2fab82e4aef1d8664874e1f194951856d48463c3e6bf9a8c68e234a629a6f50"
802 ));
803 let actual_contract_address = ContractAddress::deployed_contract_address(
804 std::iter::once(CallParam(felt!(
805 "0x5cd65f3d7daea6c63939d659b8473ea0c5cd81576035a4d34e52fb06840196c"
806 ))),
807 &ContractAddressSalt(felt!("0x0")),
808 &ClassHash(felt!(
809 "0x2338634f11772ea342365abd5be9d9dc8a6f44f159ad782fdebd3db5d969738"
810 )),
811 );
812 assert_eq!(actual_contract_address, expected_contract_address);
813 }
814
815 mod proof_serde {
816 use super::super::Proof;
817
818 #[test]
819 fn round_trip() {
820 let proof = Proof(vec![0, 0, 0, 0, 0, 0, 0, 123, 0, 0, 1, 200]);
821 let json = serde_json::to_string(&proof).unwrap();
822 assert_eq!(json, r#""AAAAAAAAAHsAAAHI""#);
823 let deserialized: Proof = serde_json::from_str(&json).unwrap();
824 assert_eq!(deserialized, proof);
825 }
826
827 #[test]
828 fn empty_string_deserializes_to_default() {
829 let proof: Proof = serde_json::from_str(r#""""#).unwrap();
830 assert_eq!(proof, Proof::default());
831 }
832
833 #[test]
834 fn invalid_base64_returns_error() {
835 let result = serde_json::from_str::<Proof>(r#""not-valid-base64!@#""#);
836 assert!(result.is_err());
837 }
838
839 #[test]
840 fn empty_proof_serializes_to_empty_string() {
841 let proof = Proof::default();
842 let json = serde_json::to_string(&proof).unwrap();
843 assert_eq!(json, r#""""#);
844 }
845 }
846}