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