|
| 1 | +// Copyright 2023-, Semiotic AI, Inc. |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +//! # Receipt Aggregate Voucher v2 |
| 5 | +
|
| 6 | +use std::cmp; |
| 7 | + |
| 8 | +use alloy::{ |
| 9 | + primitives::{Address, Bytes}, |
| 10 | + sol, |
| 11 | +}; |
| 12 | +use serde::{Deserialize, Serialize}; |
| 13 | +use tap_eip712_message::Eip712SignedMessage; |
| 14 | +use tap_receipt::{ |
| 15 | + rav::{Aggregate, AggregationError}, |
| 16 | + state::Checked, |
| 17 | + ReceiptWithState, WithValueAndTimestamp, |
| 18 | +}; |
| 19 | + |
| 20 | +use super::{Receipt, SignedReceipt}; |
| 21 | + |
| 22 | +/// EIP712 signed message for ReceiptAggregateVoucher |
| 23 | +pub type SignedRav = Eip712SignedMessage<ReceiptAggregateVoucher>; |
| 24 | + |
| 25 | +sol! { |
| 26 | + /// Holds information needed for promise of payment signed with ECDSA |
| 27 | + /// |
| 28 | + /// We use camelCase for field names to match the Ethereum ABI encoding |
| 29 | + #[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] |
| 30 | + struct ReceiptAggregateVoucher { |
| 31 | + /// Unique allocation id this RAV belongs to |
| 32 | + address allocationId; |
| 33 | + // The address of the payer the RAV was issued by |
| 34 | + address payer; |
| 35 | + // The address of the data service the RAV was issued to |
| 36 | + address dataService; |
| 37 | + // The address of the service provider the RAV was issued to |
| 38 | + address serviceProvider; |
| 39 | + // The RAV timestamp, indicating the latest TAP Receipt in the RAV |
| 40 | + uint64 timestampNs; |
| 41 | + // Total amount owed to the service provider since the beginning of the |
| 42 | + // payer-service provider relationship, including all debt that is already paid for. |
| 43 | + uint128 valueAggregate; |
| 44 | + // Arbitrary metadata to extend functionality if a data service requires it |
| 45 | + bytes metadata; |
| 46 | + } |
| 47 | +} |
| 48 | + |
| 49 | +impl ReceiptAggregateVoucher { |
| 50 | + /// Aggregates a batch of validated receipts with optional validated previous RAV, |
| 51 | + /// returning a new RAV if all provided items are valid or an error if not. |
| 52 | + /// |
| 53 | + /// # Errors |
| 54 | + /// |
| 55 | + /// Returns [`Error::AggregateOverflow`] if any receipt value causes aggregate |
| 56 | + /// value to overflow |
| 57 | + pub fn aggregate_receipts( |
| 58 | + allocation_id: Address, |
| 59 | + payer: Address, |
| 60 | + data_service: Address, |
| 61 | + service_provider: Address, |
| 62 | + receipts: &[Eip712SignedMessage<Receipt>], |
| 63 | + previous_rav: Option<Eip712SignedMessage<Self>>, |
| 64 | + ) -> Result<Self, AggregationError> { |
| 65 | + //TODO(#29): When receipts in flight struct in created check that the state |
| 66 | + // of every receipt is OK with all checks complete (relies on #28) |
| 67 | + // If there is a previous RAV get initialize values from it, otherwise get default values |
| 68 | + let mut timestamp_max = 0u64; |
| 69 | + let mut value_aggregate = 0u128; |
| 70 | + |
| 71 | + if let Some(prev_rav) = previous_rav { |
| 72 | + timestamp_max = prev_rav.message.timestampNs; |
| 73 | + value_aggregate = prev_rav.message.valueAggregate; |
| 74 | + } |
| 75 | + |
| 76 | + for receipt in receipts { |
| 77 | + value_aggregate = value_aggregate |
| 78 | + .checked_add(receipt.message.value) |
| 79 | + .ok_or(AggregationError::AggregateOverflow)?; |
| 80 | + |
| 81 | + timestamp_max = cmp::max(timestamp_max, receipt.message.timestamp_ns) |
| 82 | + } |
| 83 | + |
| 84 | + Ok(Self { |
| 85 | + allocationId: allocation_id, |
| 86 | + timestampNs: timestamp_max, |
| 87 | + valueAggregate: value_aggregate, |
| 88 | + payer, |
| 89 | + dataService: data_service, |
| 90 | + serviceProvider: service_provider, |
| 91 | + metadata: Bytes::new(), |
| 92 | + }) |
| 93 | + } |
| 94 | +} |
| 95 | + |
| 96 | +impl Aggregate<SignedReceipt> for ReceiptAggregateVoucher { |
| 97 | + fn aggregate_receipts( |
| 98 | + receipts: &[ReceiptWithState<Checked, SignedReceipt>], |
| 99 | + previous_rav: Option<Eip712SignedMessage<Self>>, |
| 100 | + ) -> Result<Self, AggregationError> { |
| 101 | + if receipts.is_empty() { |
| 102 | + return Err(AggregationError::NoValidReceiptsForRavRequest); |
| 103 | + } |
| 104 | + let allocation_id = receipts[0].signed_receipt().message.allocation_id; |
| 105 | + let payer = receipts[0].signed_receipt().message.payer; |
| 106 | + let data_service = receipts[0].signed_receipt().message.data_service; |
| 107 | + let service_provider = receipts[0].signed_receipt().message.service_provider; |
| 108 | + let receipts = receipts |
| 109 | + .iter() |
| 110 | + .map(|rx_receipt| rx_receipt.signed_receipt().clone()) |
| 111 | + .collect::<Vec<_>>(); |
| 112 | + ReceiptAggregateVoucher::aggregate_receipts( |
| 113 | + allocation_id, |
| 114 | + payer, |
| 115 | + data_service, |
| 116 | + service_provider, |
| 117 | + receipts.as_slice(), |
| 118 | + previous_rav, |
| 119 | + ) |
| 120 | + } |
| 121 | +} |
| 122 | + |
| 123 | +impl WithValueAndTimestamp for ReceiptAggregateVoucher { |
| 124 | + fn value(&self) -> u128 { |
| 125 | + self.valueAggregate |
| 126 | + } |
| 127 | + |
| 128 | + fn timestamp_ns(&self) -> u64 { |
| 129 | + self.timestampNs |
| 130 | + } |
| 131 | +} |
0 commit comments