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