1use super::protocol::normalize_hex;
4pub use super::protocol::{BrowserCommitmentArtifact, BrowserQuoteArtifact};
5use ant_protocol::evm::{Amount, PaymentQuote, RewardsAddress};
6#[cfg(test)]
7use ant_protocol::payment::commitment::commitment_hash;
8use ant_protocol::payment::commitment::{StorageCommitment, MAX_COMMITMENT_SIDECAR_BYTES};
9use serde::{Deserialize, Serialize};
10
11pub fn payment_quote_hash(payload: &[u8], public_key: &[u8], signature: &[u8]) -> [u8; 32] {
13 PaymentQuote::hash_signed_bytes(payload, public_key, signature).into()
14}
15
16#[cfg(test)]
17const PAYMENT_MULTIPLIER: u128 = crate::payment_policy::SINGLE_NODE_PAYMENT_MULTIPLIER as u128;
18#[cfg(test)]
19fn calculate_price_wei(count: u32) -> u128 {
20 ant_protocol::payment::calculate_price(count as usize).to::<u128>()
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub struct VerifiedStorageQuote {
26 pub quote: BrowserQuoteArtifact,
28 #[serde(rename = "quoteHash")]
30 pub quote_hash: String,
31 #[serde(rename = "rewardsAddress")]
33 pub rewards_address: String,
34 pub amount: String,
36}
37
38#[derive(Debug, thiserror::Error)]
40#[error("invalid storage quote: {0}")]
41pub struct StorageQuoteError(pub String);
42
43pub fn select_storage_quote(
51 mut quotes: Vec<VerifiedStorageQuote>,
52) -> Result<VerifiedStorageQuote, StorageQuoteError> {
53 let prices = quotes
54 .iter()
55 .map(|quote| parse_decimal_amount("e.quote.price, "quote price"))
56 .collect::<Result<Vec<_>, _>>()?;
57 let plan = crate::payment_policy::SingleNodePaymentPlan::from_prices(&prices)
58 .map_err(|error| StorageQuoteError(error.to_string()))?;
59 let paid = plan.paid_quote();
60 let mut selected = quotes.swap_remove(paid.quote_index);
61 selected.amount = paid.amount.to_string();
62 Ok(selected)
63}
64
65pub fn storage_payment_total(quotes: &[VerifiedStorageQuote]) -> Result<String, StorageQuoteError> {
68 quotes
69 .iter()
70 .try_fold(Amount::ZERO, |total, quote| {
71 let amount = parse_decimal_amount("e.amount, "storage payment amount")?;
72 total
73 .checked_add(amount)
74 .ok_or_else(|| StorageQuoteError("storage payment total overflow".to_string()))
75 })
76 .map(|total| total.to_string())
77}
78
79pub fn verify_storage_quote(
81 mut quote: BrowserQuoteArtifact,
82 expected_address: &str,
83 expected_peer_id: &str,
84) -> Result<VerifiedStorageQuote, StorageQuoteError> {
85 let expected_address = normalize_hex(expected_address, 32).map_err(StorageQuoteError)?;
86 let expected_peer_id = normalize_hex(expected_peer_id, 32).map_err(StorageQuoteError)?;
87 quote.content = normalize_hex("e.content, 32).map_err(StorageQuoteError)?;
88 quote.peer_id = normalize_hex("e.peer_id, 32).map_err(StorageQuoteError)?;
89 if quote.content != expected_address {
90 return Err(StorageQuoteError(
91 "storage quote is for a different chunk".to_string(),
92 ));
93 }
94 if quote.peer_id != expected_peer_id {
95 return Err(StorageQuoteError(
96 "storage quote belongs to a different WebRtcDirect peer".to_string(),
97 ));
98 }
99 let public_key = decode_unbounded_hex("e.public_key, "quote public key")?;
100 let signature = decode_unbounded_hex("e.signature, "quote signature")?;
101 let price = parse_decimal_amount("e.price, "quote price")?;
102 let rewards = normalize_hex("e.rewards_address, 20).map_err(StorageQuoteError)?;
103 quote.rewards_address.clone_from(&rewards);
104 let commitment_pin = quote
105 .commitment_pin
106 .as_deref()
107 .map(|pin| normalize_hex(pin, 32).map_err(StorageQuoteError))
108 .transpose()?;
109 quote.commitment_pin.clone_from(&commitment_pin);
110 let native_quote = PaymentQuote {
111 content: xor_name::XorName(decode_hex_array("e.content, "quote content")?),
112 timestamp: std::time::UNIX_EPOCH
113 .checked_add(std::time::Duration::from_secs(quote.timestamp_secs))
114 .ok_or_else(|| StorageQuoteError("quote timestamp out of range".into()))?,
115 price,
116 rewards_address: RewardsAddress::from(decode_hex_array::<20>(
117 &rewards,
118 "quote rewards address",
119 )?),
120 pub_key: public_key.clone(),
121 signature: signature.clone(),
122 committed_key_count: quote.committed_key_count,
123 commitment_pin: commitment_pin
124 .as_deref()
125 .map(|pin| decode_hex_array(pin, "commitment pin"))
126 .transpose()?,
127 };
128 let sidecar = if quote.committed_key_count > 0 {
129 quote
130 .commitment
131 .as_ref()
132 .map(|artifact| decode_unbounded_hex(&artifact.encoded, "storage commitment sidecar"))
133 .transpose()?
134 } else {
135 None
136 };
137 crate::quote_validation::validate_quote::<_, StorageCommitment>(
138 &decode_hex_array(&expected_peer_id, "peer ID")?,
139 &decode_hex_array(&expected_address, "content address")?,
140 &crate::quote_validation::QuoteFields {
141 public_key: &public_key,
142 content: &decode_hex_array("e.content, "quote content")?,
143 price,
144 committed_key_count: quote.committed_key_count,
145 commitment_pin: commitment_pin
146 .as_deref()
147 .map(|pin| decode_hex_array(pin, "commitment pin"))
148 .transpose()?,
149 },
150 || ant_protocol::payment::verify_quote_signature(&native_quote),
151 sidecar.as_deref(),
152 )
153 .map_err(|error| StorageQuoteError(error.to_string()))?;
154 let quote_hash = hex::encode(native_quote.hash());
155 if normalize_hex("e.quote_hash, 32).map_err(StorageQuoteError)? != quote_hash {
156 return Err(StorageQuoteError(
157 "storage quote hash does not match its signed fields".to_string(),
158 ));
159 }
160 quote.quote_hash.clone_from("e_hash);
161
162 if quote.committed_key_count > 0 {
163 if let Some(artifact) = quote.commitment.as_mut() {
166 normalize_commitment_artifact(artifact)?;
167 }
168 }
169
170 let amount = crate::payment_policy::enhanced_payment_amount(price)
171 .map_err(|error| StorageQuoteError(error.to_string()))?;
172 Ok(VerifiedStorageQuote {
173 quote,
174 quote_hash,
175 rewards_address: format!("0x{rewards}"),
176 amount: amount.to_string(),
177 })
178}
179
180#[cfg(test)]
181fn canonical_quote_bytes(
182 quote: &BrowserQuoteArtifact,
183 price: u128,
184 rewards: &str,
185 commitment_pin: Option<&str>,
186) -> Result<Vec<u8>, StorageQuoteError> {
187 let content = decode_hex_array::<32>("e.content, "quote content")?;
188 let rewards = decode_hex_array::<20>(rewards, "quote rewards address")?;
189 let commitment_pin = commitment_pin
190 .map(|pin| decode_hex_array::<32>(pin, "storage commitment pin"))
191 .transpose()?;
192 Ok(PaymentQuote::bytes_for_signing(
193 xor_name::XorName(content),
194 std::time::UNIX_EPOCH + std::time::Duration::from_secs(quote.timestamp_secs),
195 &Amount::from(price),
196 &RewardsAddress::from(rewards),
197 quote.committed_key_count,
198 &commitment_pin,
199 ))
200}
201
202fn normalize_commitment_artifact(
203 artifact: &mut BrowserCommitmentArtifact,
204) -> Result<(), StorageQuoteError> {
205 let encoded = decode_unbounded_hex(&artifact.encoded, "storage commitment sidecar")?;
206 if encoded.len() > MAX_COMMITMENT_SIDECAR_BYTES {
207 return Err(StorageQuoteError(
208 "storage commitment sidecar exceeds the protocol limit".to_string(),
209 ));
210 }
211 let commitment: StorageCommitment = rmp_serde::from_slice(&encoded).map_err(|error| {
212 StorageQuoteError(format!(
213 "storage commitment sidecar is not valid MessagePack: {error}"
214 ))
215 })?;
216 let root = normalize_hex(&artifact.root, 32).map_err(StorageQuoteError)?;
217 let peer_id = normalize_hex(&artifact.sender_peer_id, 32).map_err(StorageQuoteError)?;
218 let public_key =
219 decode_unbounded_hex(&artifact.sender_public_key, "storage commitment public key")?;
220 let signature = decode_unbounded_hex(&artifact.signature, "storage commitment signature")?;
221 if commitment.root != decode_hex_array::<32>(&root, "storage commitment root")?
222 || commitment.key_count != artifact.key_count
223 || commitment.sender_peer_id
224 != decode_hex_array::<32>(&peer_id, "storage commitment peer ID")?
225 || commitment.sender_public_key != public_key
226 || commitment.signature != signature
227 {
228 return Err(StorageQuoteError(
229 "storage commitment sidecar differs from the verified commitment".to_string(),
230 ));
231 }
232 artifact.root = root;
233 artifact.sender_peer_id = peer_id.clone();
234 artifact.sender_public_key = hex::encode(&public_key);
235 artifact.signature = hex::encode(&signature);
236 artifact.encoded = hex::encode(&encoded);
237 Ok(())
238}
239
240fn parse_decimal_amount(value: &str, label: &str) -> Result<Amount, StorageQuoteError> {
241 if value.is_empty()
242 || (value.len() > 1 && value.starts_with('0'))
243 || !value.bytes().all(|byte| byte.is_ascii_digit())
244 {
245 return Err(StorageQuoteError(format!("invalid {label}")));
246 }
247 value
248 .parse::<Amount>()
249 .map_err(|_| StorageQuoteError(format!("{label} exceeds the supported protocol range")))
250}
251
252fn decode_unbounded_hex(value: &str, label: &str) -> Result<Vec<u8>, StorageQuoteError> {
253 let value = value.strip_prefix("0x").unwrap_or(value);
254 if (value.len() & 1) != 0 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
255 return Err(StorageQuoteError(format!("invalid {label}")));
256 }
257 hex::decode(value).map_err(|error| StorageQuoteError(format!("invalid {label}: {error}")))
258}
259
260fn decode_hex_array<const LENGTH: usize>(
261 value: &str,
262 label: &str,
263) -> Result<[u8; LENGTH], StorageQuoteError> {
264 let decoded = hex::decode(value).map_err(|error| StorageQuoteError(error.to_string()))?;
265 decoded.try_into().map_err(|bytes: Vec<u8>| {
266 StorageQuoteError(format!(
267 "expected {LENGTH} bytes for {label}, received {}",
268 bytes.len()
269 ))
270 })
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use ant_protocol::payment::commitment::{
277 commitment_signed_payload as storage_commitment_bytes_for_signing, DOMAIN_COMMITMENT,
278 };
279 use ant_protocol::pqc::api::ml_dsa_65;
280
281 fn baseline_quote() -> (BrowserQuoteArtifact, String, String) {
282 let content = [0x31; 32];
283 let rewards = [0x44; 20];
284 let timestamp = 1_775_000_000;
285 let (public_key, secret_key) = ml_dsa_65().generate_keypair().expect("keypair");
286 let public_key = public_key.to_bytes();
287 let peer_id = blake3::hash(&public_key).to_hex().to_string();
288 let mut quote = BrowserQuoteArtifact {
289 peer_id: peer_id.clone(),
290 content: hex::encode(content),
291 timestamp_secs: timestamp,
292 price: calculate_price_wei(0).to_string(),
293 rewards_address: hex::encode(rewards),
294 public_key: hex::encode(&public_key),
295 signature: String::new(),
296 committed_key_count: 0,
297 commitment_pin: None,
298 quote_hash: String::new(),
299 commitment: None,
300 };
301 let payload =
302 canonical_quote_bytes("e, calculate_price_wei(0), &hex::encode(rewards), None)
303 .expect("payload");
304 let signature = ml_dsa_65()
305 .sign(&secret_key, &payload)
306 .expect("signature")
307 .to_bytes();
308 quote.signature = hex::encode(&signature);
309 quote.quote_hash = hex::encode(payment_quote_hash(&payload, &public_key, &signature));
310 (quote, hex::encode(content), peer_id)
311 }
312
313 fn bound_quote(key_count: u32) -> (BrowserQuoteArtifact, String, String) {
314 let content = [0x31; 32];
315 let rewards = [0x42; 20];
316 let root = [0x53; 32];
317 let timestamp = 1_775_000_001;
318 let (public_key, secret_key) = ml_dsa_65().generate_keypair().expect("keypair");
319 let public_key = public_key.to_bytes();
320 let peer_id = blake3::hash(&public_key).into();
321 let mut commitment = StorageCommitment {
322 root,
323 key_count,
324 sender_peer_id: peer_id,
325 sender_public_key: public_key.clone(),
326 signature: Vec::new(),
327 };
328 let commitment_payload = storage_commitment_bytes_for_signing(
329 &commitment.root,
330 commitment.key_count,
331 &commitment.sender_peer_id,
332 &commitment.sender_public_key,
333 );
334 commitment.signature = ml_dsa_65()
335 .sign_with_context(&secret_key, &commitment_payload, DOMAIN_COMMITMENT)
336 .expect("commitment signature")
337 .to_bytes();
338 let native_commitment = ant_protocol::payment::StorageCommitment {
339 root: commitment.root,
340 key_count: commitment.key_count,
341 sender_peer_id: commitment.sender_peer_id,
342 sender_public_key: commitment.sender_public_key.clone(),
343 signature: commitment.signature.clone(),
344 };
345 assert!(ant_protocol::payment::verify_commitment_signature(
346 &native_commitment
347 ));
348 assert_eq!(
349 commitment_hash(&commitment),
350 ant_protocol::payment::commitment_hash(&native_commitment)
351 );
352 let encoded = rmp_serde::to_vec(&commitment).expect("MessagePack commitment");
353 assert_eq!(
354 encoded,
355 rmp_serde::to_vec(&native_commitment).expect("native MessagePack commitment")
356 );
357 let pin = hex::encode(commitment_hash(&commitment).expect("commitment hash"));
358 let peer_id = hex::encode(peer_id);
359 let price = calculate_price_wei(key_count);
360 assert_eq!(
361 ant_protocol::evm::Amount::from(price),
362 ant_protocol::payment::calculate_price(key_count as usize)
363 );
364 let mut quote = BrowserQuoteArtifact {
365 peer_id: peer_id.clone(),
366 content: hex::encode(content),
367 timestamp_secs: timestamp,
368 price: price.to_string(),
369 rewards_address: hex::encode(rewards),
370 public_key: hex::encode(&public_key),
371 signature: String::new(),
372 committed_key_count: key_count,
373 commitment_pin: Some(pin.clone()),
374 quote_hash: String::new(),
375 commitment: Some(BrowserCommitmentArtifact {
376 encoded: hex::encode(encoded),
377 root: hex::encode(commitment.root),
378 key_count,
379 sender_peer_id: peer_id.clone(),
380 sender_public_key: hex::encode(&public_key),
381 signature: hex::encode(&commitment.signature),
382 }),
383 };
384 let payload = canonical_quote_bytes("e, price, &hex::encode(rewards), Some(&pin))
385 .expect("quote payload");
386 let native_quote = ant_protocol::evm::PaymentQuote {
387 content: xor_name::XorName(content),
388 timestamp: std::time::SystemTime::UNIX_EPOCH
389 + std::time::Duration::from_secs(timestamp),
390 price: ant_protocol::evm::Amount::from(price),
391 rewards_address: ant_protocol::evm::RewardsAddress::from(rewards),
392 pub_key: public_key.clone(),
393 signature: Vec::new(),
394 committed_key_count: key_count,
395 commitment_pin: Some(
396 hex::decode(&pin)
397 .expect("pin")
398 .try_into()
399 .expect("32-byte pin"),
400 ),
401 };
402 assert_eq!(payload, native_quote.bytes_for_sig());
403 let signature = ml_dsa_65()
404 .sign(&secret_key, &payload)
405 .expect("quote signature")
406 .to_bytes();
407 quote.signature = hex::encode(&signature);
408 quote.quote_hash = hex::encode(payment_quote_hash(&payload, &public_key, &signature));
409 let native_quote = ant_protocol::evm::PaymentQuote {
410 signature: signature.clone(),
411 ..native_quote
412 };
413 assert_eq!(quote.quote_hash, hex::encode(native_quote.hash()));
414 (quote, hex::encode(content), peer_id)
415 }
416
417 #[test]
418 fn payment_hash_matches_evmlib_vector() {
419 assert_eq!(
420 hex::encode(payment_quote_hash(&[0, 1], &[2], &[3])),
421 "d98f2e8134922f73748703c8e7084d42f13d2fa1439936ef5a3abcf5646fe83f"
422 );
423 }
424
425 #[test]
426 fn verifies_baseline_quote_and_rejects_tampering() {
427 let (quote, content, peer_id) = baseline_quote();
428 let verified =
429 verify_storage_quote(quote.clone(), &content, &peer_id).expect("valid quote");
430 assert_eq!(verified.amount, (calculate_price_wei(0) * 3).to_string());
431 let mut tampered = quote;
432 tampered.price = (calculate_price_wei(0) + 1).to_string();
433 assert!(verify_storage_quote(tampered, &content, &peer_id).is_err());
434 }
435
436 #[test]
437 fn verifies_bound_commitment_and_exact_native_sidecar() {
438 let (quote, content, peer_id) = bound_quote(23);
439 let verified =
440 verify_storage_quote(quote.clone(), &content, &peer_id).expect("valid bound quote");
441 assert_eq!(
442 storage_payment_total(&[verified]).expect("payment total"),
443 (calculate_price_wei(23) * PAYMENT_MULTIPLIER).to_string()
444 );
445
446 let mut tampered = quote;
447 tampered.commitment.as_mut().expect("commitment").root = hex::encode([0x99; 32]);
448 let error = verify_storage_quote(tampered, &content, &peer_id)
449 .expect_err("sidecar mismatch must fail");
450 assert!(error.to_string().contains("sidecar differs"));
451 }
452
453 fn native_quote(quote: &BrowserQuoteArtifact) -> ant_protocol::evm::PaymentQuote {
454 use ant_protocol::evm::{Amount, PaymentQuote, RewardsAddress};
455 PaymentQuote {
456 content: xor_name::XorName(decode_hex_array("e.content, "content").unwrap()),
457 timestamp: std::time::SystemTime::UNIX_EPOCH
458 + std::time::Duration::from_secs(quote.timestamp_secs),
459 price: Amount::from(quote.price.parse::<Amount>().unwrap()),
460 rewards_address: RewardsAddress::from(
461 decode_hex_array::<20>("e.rewards_address, "rewards").unwrap(),
462 ),
463 pub_key: hex::decode("e.public_key).unwrap(),
464 signature: hex::decode("e.signature).unwrap(),
465 committed_key_count: quote.committed_key_count,
466 commitment_pin: quote
467 .commitment_pin
468 .as_deref()
469 .map(|pin| decode_hex_array(pin, "pin").unwrap()),
470 }
471 }
472
473 #[test]
474 fn native_and_browser_validation_accept_and_reject_identical_signed_artifacts() {
475 use ant_protocol::transport::PeerId;
476 let (baseline, content, peer) = baseline_quote();
477 let (bound, _, bound_peer) = bound_quote(23);
478 let mut extra_baseline_sidecar = baseline.clone();
479 extra_baseline_sidecar.commitment = bound.commitment.clone();
480 extra_baseline_sidecar.commitment.as_mut().unwrap().encoded = "c1".into();
481 let mut missing = bound.clone();
482 missing.commitment = None;
483 let mut corrupt = bound.clone();
484 corrupt.commitment.as_mut().unwrap().encoded = "c1".into();
485 let mut wrong_key = baseline.clone();
486 wrong_key.public_key = bound.public_key.clone();
487 let mut bad_signature = baseline.clone();
488 bad_signature.signature = "00".repeat(3309);
489 let mut wrong_content = baseline.clone();
490 wrong_content.content = "01".repeat(32);
491 for (quote, peer, expected) in [
492 (baseline, peer.clone(), true),
493 (extra_baseline_sidecar, peer.clone(), true),
494 (bound, bound_peer.clone(), true),
495 (missing, bound_peer.clone(), false),
496 (corrupt, bound_peer, false),
497 (wrong_key, peer.clone(), false),
498 (bad_signature, peer.clone(), false),
499 (wrong_content, peer, false),
500 ] {
501 let native = native_quote("e);
502 let sidecar = quote
503 .commitment
504 .as_ref()
505 .map(|artifact| hex::decode(&artifact.encoded).unwrap());
506 let native_result = crate::data::client::quote::classify_quote_response(
507 &PeerId::from_bytes(decode_hex_array(&peer, "peer").unwrap()),
508 &decode_hex_array(&content, "content").unwrap(),
509 &rmp_serde::to_vec(&native).unwrap(),
510 false,
511 sidecar,
512 );
513 assert_eq!(native_result.is_ok(), expected);
514 assert_eq!(
515 verify_storage_quote(quote, &content, &peer).is_ok(),
516 expected
517 );
518 }
519 }
520
521 #[test]
522 fn browser_and_native_adapters_select_the_same_signed_quote_and_payment() {
523 use crate::data::client::batch::SingleNodeQuotePayment;
524 for (counts, expected_index) in [
525 (vec![1_000_000, 0, 0, 0, 0, 0, 0], 4),
526 (vec![0, 6000, 6000, 6000, 6000, 6000, 6000], 3),
527 (vec![6000, 1000, 4000, 2000], 2),
528 (vec![23, 23, 23, 23], 2),
529 (vec![23], 0),
530 ] {
531 let verified = counts
532 .iter()
533 .map(|&count| {
534 let (quote, address, peer) = if count == 0 {
535 baseline_quote()
536 } else {
537 bound_quote(count)
538 };
539 verify_storage_quote(quote, &address, &peer).expect("valid signed quote")
540 })
541 .collect::<Vec<_>>();
542 let expected_hash = verified[expected_index].quote_hash.clone();
543 let native_quotes = verified
544 .iter()
545 .map(|verified| native_quote(&verified.quote))
546 .collect();
547 let native = SingleNodeQuotePayment::from_quotes(native_quotes).expect("native plan");
548 let browser = select_storage_quote(verified).expect("browser plan");
549 let paid = native
550 .quotes
551 .iter()
552 .filter(|quote| !quote.amount.is_zero())
553 .collect::<Vec<_>>();
554 assert_eq!(paid.len(), 1);
555 assert_eq!(hex::encode(paid[0].quote_hash), expected_hash);
556 assert_eq!(browser.quote_hash, expected_hash);
557 assert_eq!(browser.amount, native.total_amount().to_string());
558 assert_eq!(
559 browser.amount,
560 (calculate_price_wei(counts[expected_index]) * 3).to_string()
561 );
562 assert_eq!(native.quotes.len(), counts.len());
563 }
564 }
565}
566
567#[derive(Debug, Clone, Serialize, Deserialize)]
569#[serde(rename_all = "camelCase")]
570pub struct MerklePaymentRequest {
571 pub calldata: String,
573 pub maximum_amount: String,
575 pub depth: u8,
577 pub timestamp: u64,
579 pub pool_hashes: Vec<String>,
581}
582
583impl MerklePaymentRequest {
584 pub fn from_batch(batch: &crate::data::client::merkle::PreparedMerkleBatch) -> Self {
586 Self {
587 calldata: format!(
588 "0x{}",
589 hex::encode(
590 ant_protocol::evm::contract::payment_vault::encode_merkle_payment(
591 batch.depth,
592 batch.pool_commitments.clone(),
593 batch.merkle_payment_timestamp
594 )
595 )
596 ),
597 maximum_amount: ant_protocol::evm::Network::default()
598 .estimate_merkle_payment_cost(batch.depth, &batch.pool_commitments)
599 .to_string(),
600 depth: batch.depth,
601 timestamp: batch.merkle_payment_timestamp,
602 pool_hashes: batch
603 .pool_commitments
604 .iter()
605 .map(|pool| hex::encode(pool.pool_hash))
606 .collect(),
607 }
608 }
609}
610
611#[derive(Deserialize)]
613pub struct PaymentLog {
614 pub address: String,
616 pub topics: Vec<String>,
618 pub data: String,
620}
621
622#[derive(Serialize)]
624#[serde(rename_all = "camelCase")]
625pub struct MerkleSettlement {
626 pub winner_pool_hash: String,
628 pub total_amount: String,
630}
631
632pub fn decode_merkle_receipt(
634 request: &MerklePaymentRequest,
635 vault: &str,
636 logs: &[PaymentLog],
637) -> Result<MerkleSettlement, StorageQuoteError> {
638 let vault = normalize_hex(vault, 20).map_err(StorageQuoteError)?;
639 for log in logs {
640 if normalize_hex(&log.address, 20).map_err(StorageQuoteError)? != vault {
641 continue;
642 }
643 let topics = log
644 .topics
645 .iter()
646 .map(|topic| decode_hex_array(topic.strip_prefix("0x").unwrap_or(topic), "event topic"))
647 .collect::<Result<Vec<[u8; 32]>, _>>()?;
648 let data = decode_unbounded_hex(&log.data, "event data")?;
649 let Ok(event) =
650 ant_protocol::evm::contract::payment_vault::decode_merkle_payment_event(&topics, &data)
651 else {
652 continue;
653 };
654 let winner = hex::encode(event.winnerPoolHash);
655 if event.depth != request.depth
656 || event.merklePaymentTimestamp != request.timestamp
657 || !request.pool_hashes.contains(&winner)
658 {
659 continue;
660 }
661 if event.totalAmount
662 > parse_decimal_amount(&request.maximum_amount, "maximum Merkle payment")?
663 {
664 return Err(StorageQuoteError(
665 "Merkle event exceeds authorized amount".into(),
666 ));
667 }
668 return Ok(MerkleSettlement {
669 winner_pool_hash: winner,
670 total_amount: event.totalAmount.to_string(),
671 });
672 }
673 Err(StorageQuoteError(
674 "receipt has no matching Merkle payment event".into(),
675 ))
676}