1use std::{error::Error, fmt::Display, str::FromStr};
2
3use base64::Engine;
4use celestia_types::{blob::Commitment, block::Height as BlockHeight, nmt::Namespace};
5use serde::{Deserialize, Serialize};
6
7#[derive(Serialize, Deserialize, Clone, PartialEq)]
8pub struct BlobId {
9 pub height: BlockHeight,
10 pub namespace: Namespace,
11 pub commitment: Commitment,
12}
13
14#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
15pub struct JobId {
16 pub blob_id: BlobId,
17 pub l2_chain_id: u64,
19 pub batch_number: u32,
20}
21
22impl JobId {
23 pub fn new(blob_id: BlobId, l2_chain_id: u64, batch_number: u32) -> Self {
24 Self {
25 blob_id,
26 l2_chain_id,
27 batch_number,
28 }
29 }
30}
31
32impl FromStr for JobId {
34 type Err = Box<dyn Error>;
35
36 fn from_str(s: &str) -> Result<Self, Self::Err> {
37 let mut parts = s.rsplitn(3, ':');
39
40 let batch_number: u32 = parts.next().ok_or("Batch number missing (u32)")?.parse()?;
41
42 let l2_chain_id: u64 = parts.next().ok_or("L2 chain ID missing (u64)")?.parse()?;
43
44 let blob_str = parts.next().ok_or("BlobId missing")?;
45 let blob_id = BlobId::from_str(blob_str)?;
46
47 Ok(Self {
48 blob_id,
49 l2_chain_id,
50 batch_number,
51 })
52 }
53}
54
55impl Display for JobId {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 write!(
59 f,
60 "{}:{}:{}",
61 self.blob_id, self.l2_chain_id, self.batch_number
62 )
63 }
64}
65
66impl BlobId {
67 pub fn new(height: BlockHeight, namespace: Namespace, commitment: Commitment) -> Self {
68 Self {
69 height,
70 namespace,
71 commitment,
72 }
73 }
74}
75
76impl std::fmt::Debug for BlobId {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 let namespace_string = if let Some(namespace) = &self.namespace.id_v0() {
79 base64::engine::general_purpose::STANDARD.encode(namespace)
80 } else {
81 "Invalid v0 ID".to_string()
82 };
83 let commitment_string =
84 base64::engine::general_purpose::STANDARD.encode(&self.commitment.hash());
85 f.debug_struct("BlobId")
86 .field("height", &self.height.value())
87 .field("namespace", &namespace_string)
88 .field("commitment", &commitment_string)
89 .finish()
90 }
91}
92
93impl Display for BlobId {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 let namespace_string = if let Some(namespace) = &self.namespace.id_v0() {
97 base64::engine::general_purpose::STANDARD.encode(namespace)
98 } else {
99 "Invalid v0 ID".to_string()
100 };
101 let commitment_string =
102 base64::engine::general_purpose::STANDARD.encode(&self.commitment.hash());
103 write!(
104 f,
105 "{}:{}:{}",
106 self.height.value(),
107 namespace_string,
108 commitment_string
109 )
110 }
111}
112
113impl FromStr for BlobId {
115 type Err = Box<dyn Error>;
116
117 fn from_str(s: &str) -> Result<Self, Self::Err> {
118 let mut parts = s.splitn(5, ":");
119
120 let height = BlockHeight::from_str(parts.next().ok_or("Height missing (u64)")?)?;
121
122 let n_base64 = parts
123 .next()
124 .ok_or("Namespace missing (base64)")?
125 .to_string();
126 println!("{}", &n_base64);
127 let n_bytes = base64::engine::general_purpose::STANDARD.decode(n_base64)?;
128 let namespace = Namespace::new_v0(&n_bytes)?;
129
130 let c_base64 = parts
131 .next()
132 .ok_or("Commitment missing (base64)")?
133 .to_string();
134 let c_bytes = base64::engine::general_purpose::STANDARD.decode(c_base64)?;
135 let c_hash: [u8; 32] = c_bytes
136 .try_into()
137 .map_err(|_| "Commitment must be 32 bytes!")?;
138 let commitment = Commitment::new(c_hash.into());
139
140 Ok(Self {
141 height,
142 namespace,
143 commitment,
144 })
145 }
146}
147
148#[cfg(test)]
149mod test {
150 use base64::engine::general_purpose::STANDARD;
151
152 use super::*;
153 use bincode;
154
155 #[test]
156 fn test_job_id_from_str() {
157 let height: u32 = 6952283;
158 let namespace = "c292LW1pbmktYQ==";
160 let commitment = "JkVWHw0eLp6eeCEG28rLwF1xwUWGDI3+DbEyNNKq9fE=";
161 let l2_chain_id = 0u64;
162 let batch_number = 0u32;
163
164 let blob_id = BlobId::new(
165 BlockHeight::from(height),
166 Namespace::new_v0(STANDARD.decode(namespace).unwrap().as_slice()).unwrap(),
167 Commitment::new(STANDARD.decode(commitment).unwrap().try_into().unwrap()),
168 );
169
170 let job_id = JobId {
171 blob_id,
172 l2_chain_id,
173 batch_number,
174 };
175
176 let job_id_to_str = job_id.to_string();
177 let job_id_from_str = JobId::from_str(
178 "6952283:c292LW1pbmktYQ==:JkVWHw0eLp6eeCEG28rLwF1xwUWGDI3+DbEyNNKq9fE=:0:0",
179 )
180 .unwrap();
181
182 assert_eq!(job_id_from_str, job_id);
183 assert_eq!(
184 job_id_to_str,
185 "6952283:c292LW1pbmktYQ==:JkVWHw0eLp6eeCEG28rLwF1xwUWGDI3+DbEyNNKq9fE=:0:0"
186 );
187 }
188
189 #[test]
190 fn test_job_id_bincode() {
191 let height: u32 = 7640999;
192 let namespace = "J3fU2WHHWlJt2A==";
194 let commitment = "SLzsmvT0rHZtgxS2yHHB7Hr7N6FkPi/UtUOHW0mtIqQ=";
195 let l2_chain_id = 0u64;
196 let batch_number = 0u32;
197
198 let blob_id = BlobId::new(
199 BlockHeight::from(height),
200 Namespace::new_v0(STANDARD.decode(namespace).unwrap().as_slice()).unwrap(),
201 Commitment::new(STANDARD.decode(commitment).unwrap().try_into().unwrap()),
202 );
203 let job_id = JobId::new(blob_id, l2_chain_id, batch_number);
204
205 println!("job_id: {:?}", job_id);
206 let job_id_bincode = bincode::serialize(&job_id).unwrap();
207 let job_id_from_bincode: JobId = bincode::deserialize(&job_id_bincode).unwrap();
208
209 assert_eq!(job_id_from_bincode, job_id);
210 }
211}