1#[cfg(feature = "native-grpc")]
5use iota_grpc_client::Client as GrpcClient;
6use iota_sdk_types::{ObjectId, ObjectReference, TransactionDigest};
7use iota_types::effects::TransactionEffectsExt;
8use iota_types::event::EventID;
9use iota_types::object::Object;
10
11use crate::{Proof, ProofTargets, ProofV1, Source, SourceError, TransactionProof};
12
13#[derive(Debug, thiserror::Error)]
15#[non_exhaustive]
16pub enum ProofBuilderError {
17 #[error("proof builder requires a target")]
19 MissingTarget,
20 #[error("source failed while reading proof evidence")]
22 Source {
23 #[source]
25 source: SourceError,
26 },
27 #[error("transaction {transaction_digest} was not found")]
29 TransactionNotFound {
30 transaction_digest: TransactionDigest,
32 },
33 #[error("checkpoint {sequence_number} was not found")]
35 CheckpointNotFound {
36 sequence_number: u64,
38 },
39 #[error("object {object_id} was not found")]
41 ObjectNotFound {
42 object_id: ObjectId,
44 },
45 #[error("event {event_id:?} was not found")]
47 EventNotFound {
48 event_id: EventID,
50 },
51 #[error("object {object_id} reference does not match the requested object")]
53 ObjectReferenceMismatch {
54 object_id: ObjectId,
56 },
57 #[error(
59 "transaction {transaction_digest} did not write a provable value for object {object_id}; deleted and wrapped objects are unsupported"
60 )]
61 ObjectNotChangedByTransaction {
62 object_id: ObjectId,
64 transaction_digest: TransactionDigest,
66 },
67 #[error("proof targets belong to different transactions: {actual}, expected {expected}")]
69 TransactionMismatch {
70 expected: TransactionDigest,
72 actual: TransactionDigest,
74 },
75}
76
77#[derive(Debug)]
83pub struct ProofBuilder<S> {
84 source: S,
85 transaction_digests: Vec<TransactionDigest>,
86 object_ids: Vec<ObjectId>,
87 event_ids: Vec<EventID>,
88}
89
90#[cfg(feature = "native-grpc")]
91impl ProofBuilder<GrpcClient> {
92 pub fn mainnet() -> iota_grpc_client::Result<Self> {
97 GrpcClient::new_mainnet().map(Self::from_grpc_client)
98 }
99
100 pub fn testnet() -> iota_grpc_client::Result<Self> {
105 GrpcClient::new_testnet().map(Self::from_grpc_client)
106 }
107
108 pub fn devnet() -> iota_grpc_client::Result<Self> {
113 GrpcClient::new_devnet().map(Self::from_grpc_client)
114 }
115
116 pub fn from_grpc_client(client: GrpcClient) -> Self {
118 Self::new(client)
119 }
120}
121
122impl<S: Source> ProofBuilder<S> {
123 pub fn new(source: S) -> Self {
125 Self {
126 source,
127 transaction_digests: Vec::new(),
128 object_ids: Vec::new(),
129 event_ids: Vec::new(),
130 }
131 }
132
133 pub fn transaction(mut self, transaction_digest: TransactionDigest) -> Self {
135 Self::push_unique(&mut self.transaction_digests, transaction_digest);
136 self
137 }
138
139 pub fn object(mut self, object_id: ObjectId) -> Self {
144 Self::push_unique(&mut self.object_ids, object_id);
145 self
146 }
147
148 pub fn objects(mut self, object_ids: impl IntoIterator<Item = ObjectId>) -> Self {
152 for object_id in object_ids {
153 Self::push_unique(&mut self.object_ids, object_id);
154 }
155 self
156 }
157
158 pub fn event(mut self, event_id: EventID) -> Self {
160 Self::push_unique(&mut self.event_ids, event_id);
161 self
162 }
163
164 pub fn events(mut self, event_ids: impl IntoIterator<Item = EventID>) -> Self {
166 for event_id in event_ids {
167 Self::push_unique(&mut self.event_ids, event_id);
168 }
169 self
170 }
171
172 pub async fn build(self) -> Result<Proof, ProofBuilderError> {
174 if self.transaction_digests.is_empty() && self.object_ids.is_empty() && self.event_ids.is_empty() {
175 return Err(ProofBuilderError::MissingTarget);
176 }
177
178 self.build_proof().await
179 }
180
181 async fn build_proof(&self) -> Result<Proof, ProofBuilderError> {
182 let mut selected_transaction = None;
183
184 for transaction_digest in self.transaction_digests.iter().copied() {
185 Self::ensure_same_transaction(&mut selected_transaction, transaction_digest)?;
186 }
187 for event_id in &self.event_ids {
188 Self::ensure_same_transaction(&mut selected_transaction, event_id.tx_digest)?;
189 }
190
191 let (transaction, objects) = if let Some(transaction_digest) = selected_transaction {
192 let transaction = self.fetch_transaction(transaction_digest).await?;
193 let changed_objects = transaction.effects.all_changed_objects();
194 let mut objects = Vec::with_capacity(self.object_ids.len());
195
196 for object_id in self.object_ids.iter().copied() {
197 let object_ref = changed_objects
198 .iter()
199 .find_map(|(object_ref, _, _)| (object_ref.object_id == object_id).then_some(*object_ref))
200 .ok_or(ProofBuilderError::ObjectNotChangedByTransaction {
201 object_id,
202 transaction_digest,
203 })?;
204 objects.push(self.fetch_object(object_id, Some(object_ref)).await?);
205 }
206
207 (transaction, objects)
208 } else {
209 let mut objects = Vec::with_capacity(self.object_ids.len());
210
211 for object_id in self.object_ids.iter().copied() {
212 let object = self.fetch_object(object_id, None).await?;
213 Self::ensure_same_transaction(&mut selected_transaction, object.previous_transaction)?;
214 objects.push(object);
215 }
216
217 let transaction_digest =
218 selected_transaction.expect("ProofBuilder only builds a proof for non-empty targets");
219 let transaction = self.fetch_transaction(transaction_digest).await?;
220
221 (transaction, objects)
222 };
223
224 let chain_identifier = self
225 .source
226 .chain_identifier()
227 .await
228 .map_err(|source| ProofBuilderError::Source { source })?;
229 let checkpoint_sequence_number = transaction.checkpoint_sequence_number;
230 let checkpoint = self
231 .source
232 .checkpoint(checkpoint_sequence_number)
233 .await
234 .map_err(|source| ProofBuilderError::Source { source })?
235 .ok_or(ProofBuilderError::CheckpointNotFound {
236 sequence_number: checkpoint_sequence_number,
237 })?;
238 if !self.event_ids.is_empty() {
239 let events = transaction
240 .events
241 .as_ref()
242 .ok_or_else(|| ProofBuilderError::EventNotFound {
243 event_id: self.event_ids[0],
244 })?;
245
246 for event_id in &self.event_ids {
247 let event_exists = usize::try_from(event_id.event_seq)
248 .ok()
249 .is_some_and(|index| events.get(index).is_some());
250 if !event_exists {
251 return Err(ProofBuilderError::EventNotFound { event_id: *event_id });
252 }
253 }
254 }
255 let transaction_proof = TransactionProof::new(transaction.transaction, transaction.effects, transaction.events);
256 let mut targets = ProofTargets::new();
257 if let Some(transaction_digest) = self.transaction_digests.first().copied() {
258 targets = targets.set_transaction(transaction_digest);
259 }
260 for object in objects {
261 targets = targets.add_object(object);
262 }
263 for event_id in self.event_ids.iter().copied() {
264 targets = targets.add_event(event_id);
265 }
266
267 Ok(ProofV1::new(
268 chain_identifier,
269 targets,
270 checkpoint.summary,
271 checkpoint.contents,
272 transaction_proof,
273 )
274 .into())
275 }
276
277 async fn fetch_transaction(
278 &self,
279 transaction_digest: TransactionDigest,
280 ) -> Result<crate::SourceTransaction, ProofBuilderError> {
281 self.source
282 .transaction(transaction_digest)
283 .await
284 .map_err(|source| ProofBuilderError::Source { source })?
285 .ok_or(ProofBuilderError::TransactionNotFound { transaction_digest })
286 }
287
288 async fn fetch_object(
289 &self,
290 object_id: ObjectId,
291 expected_ref: Option<ObjectReference>,
292 ) -> Result<Object, ProofBuilderError> {
293 let object = self
294 .source
295 .object(object_id, expected_ref.map(|object_ref| object_ref.version))
296 .await
297 .map_err(|source| ProofBuilderError::Source { source })?
298 .ok_or(ProofBuilderError::ObjectNotFound { object_id })?;
299 let object_ref = object.as_inner().object_ref();
300
301 if object_ref.object_id != object_id || expected_ref.is_some_and(|expected| expected != object_ref) {
302 return Err(ProofBuilderError::ObjectReferenceMismatch { object_id });
303 }
304
305 Ok(object)
306 }
307
308 fn ensure_same_transaction(
309 selected: &mut Option<TransactionDigest>,
310 transaction_digest: TransactionDigest,
311 ) -> Result<(), ProofBuilderError> {
312 if let Some(expected) = selected {
313 if *expected != transaction_digest {
314 return Err(ProofBuilderError::TransactionMismatch {
315 expected: *expected,
316 actual: transaction_digest,
317 });
318 }
319 } else {
320 *selected = Some(transaction_digest);
321 }
322
323 Ok(())
324 }
325
326 fn push_unique<T: PartialEq>(values: &mut Vec<T>, value: T) {
327 if !values.contains(&value) {
328 values.push(value);
329 }
330 }
331}