hopr_chain_connector/connector/
tickets.rs

1use blokli_client::api::{BlokliQueryClient, BlokliTransactionClient};
2use futures::{FutureExt, TryFutureExt, future::BoxFuture};
3use hopr_api::chain::{ChainReceipt, ChainValues, TicketRedeemError};
4use hopr_chain_types::prelude::*;
5use hopr_crypto_types::prelude::*;
6use hopr_internal_types::prelude::*;
7
8use crate::{backend::Backend, connector::HoprBlockchainConnector, errors::ConnectorError};
9
10impl<B, C, P> HoprBlockchainConnector<C, B, P, P::TxRequest>
11where
12    B: Backend + Send + Sync + 'static,
13    C: BlokliTransactionClient + BlokliQueryClient + Send + Sync + 'static,
14    P: PayloadGenerator + Send + Sync + 'static,
15    P::TxRequest: Send + Sync + 'static,
16{
17    async fn prepare_ticket_redeem_payload(&self, ticket: AcknowledgedTicket) -> Result<P::TxRequest, ConnectorError> {
18        self.check_connection_state()?;
19
20        let channel_id = ticket.verified_ticket().channel_id;
21        if generate_channel_id(ticket.ticket.verified_issuer(), self.chain_key.public().as_ref()) != channel_id {
22            tracing::error!(%channel_id, "redeemed ticket is not ours");
23            return Err(ConnectorError::InvalidTicket);
24        }
25
26        // Do a fresh Blokli query to ensure the channel is not closed.
27        let channel = self
28            .client
29            .query_channels(blokli_client::api::ChannelSelector {
30                filter: blokli_client::api::ChannelFilter::ChannelId(ticket.verified_ticket().channel_id.into()),
31                status: None,
32            })
33            .await?
34            .first()
35            .cloned()
36            .ok_or_else(|| {
37                tracing::error!(%channel_id, "trying to redeem a ticket on a channel that does not exist");
38                ConnectorError::ChannelDoesNotExist(channel_id)
39            })?;
40
41        if channel.status == blokli_client::api::types::ChannelStatus::Closed {
42            tracing::error!(%channel_id, "trying to redeem a ticket on a closed channel");
43            return Err(ConnectorError::ChannelClosed(channel_id));
44        }
45
46        if channel.epoch as u32 != ticket.verified_ticket().channel_epoch {
47            tracing::error!(
48                channel_epoch = channel.epoch,
49                ticket_epoch = ticket.verified_ticket().channel_epoch,
50                "invalid redeemed ticket epoch"
51            );
52            return Err(ConnectorError::InvalidTicket);
53        }
54
55        let channel_index: u64 = channel
56            .ticket_index
57            .0
58            .parse()
59            .map_err(|e| ConnectorError::TypeConversion(format!("unparseable channel index at redemption: {e}")))?;
60
61        if channel_index > ticket.verified_ticket().index {
62            tracing::error!(
63                channel_index,
64                ticket_index = ticket.verified_ticket().index,
65                "invalid redeemed ticket index"
66            );
67            return Err(ConnectorError::InvalidTicket);
68        }
69
70        // `into_redeemable` is a CPU-intensive operation. See #7616 for a future resolution.
71        let channel_dst = self.domain_separators().await?.channel;
72        let chain_key = self.chain_key.clone();
73        let ticket =
74            hopr_async_runtime::prelude::spawn_blocking(move || ticket.into_redeemable(&chain_key, &channel_dst))
75                .await
76                .map_err(|e| ConnectorError::OtherError(e.into()))??;
77
78        Ok(self.payload_generator.redeem_ticket(ticket.clone())?)
79    }
80}
81
82#[async_trait::async_trait]
83impl<B, C, P> hopr_api::chain::ChainWriteTicketOperations for HoprBlockchainConnector<C, B, P, P::TxRequest>
84where
85    B: Backend + Send + Sync + 'static,
86    C: BlokliTransactionClient + BlokliQueryClient + Send + Sync + 'static,
87    P: PayloadGenerator + Send + Sync + 'static,
88    P::TxRequest: Send + Sync + 'static,
89{
90    type Error = ConnectorError;
91
92    async fn redeem_ticket<'a>(
93        &'a self,
94        ticket: AcknowledgedTicket,
95    ) -> Result<
96        BoxFuture<'a, Result<(VerifiedTicket, ChainReceipt), TicketRedeemError<Self::Error>>>,
97        TicketRedeemError<Self::Error>,
98    > {
99        match self.prepare_ticket_redeem_payload(ticket.clone()).await {
100            Ok(tx_req) => {
101                let ticket_clone = ticket.clone();
102                Ok(self
103                    .send_tx(tx_req)
104                    .await
105                    .map_err(|e| TicketRedeemError::ProcessingError(ticket.ticket.clone(), e))?
106                    .map_err(move |tx_tracking_error|
107                        // For ticket redemption, certain errors are to be handled differently
108                        if let Some(reject_error) = tx_tracking_error.as_transaction_rejection_error() {
109                            TicketRedeemError::Rejected(ticket.ticket.clone(), format!("on-chain rejection: {reject_error:?}"))
110                        } else {
111                            TicketRedeemError::ProcessingError(ticket.ticket.clone(), tx_tracking_error)
112                        })
113                    .and_then(move |receipt| futures::future::ok((ticket_clone.ticket, receipt)))
114                    .boxed())
115            }
116            Err(e @ ConnectorError::InvalidTicket)
117            | Err(e @ ConnectorError::ChannelDoesNotExist(_))
118            | Err(e @ ConnectorError::ChannelClosed(_)) => {
119                Err(TicketRedeemError::Rejected(ticket.ticket, e.to_string()))
120            }
121            Err(e) => Err(TicketRedeemError::ProcessingError(ticket.ticket, e)),
122        }
123    }
124}