hedera/transaction/
execute.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::borrow::Cow;
4use std::collections::HashMap;
5
6use hedera_proto::services;
7use prost::Message;
8use tonic::transport::Channel;
9
10use super::chunked::ChunkInfo;
11use super::source::SourceChunk;
12use super::{
13    ChunkData,
14    TransactionSources,
15};
16use crate::execute::Execute;
17use crate::ledger_id::RefLedgerId;
18use crate::transaction::any::AnyTransactionData;
19use crate::transaction::protobuf::ToTransactionDataProtobuf;
20use crate::transaction::DEFAULT_TRANSACTION_VALID_DURATION;
21use crate::{
22    AccountId,
23    BoxGrpcFuture,
24    Client,
25    Error,
26    Hbar,
27    PublicKey,
28    ToProtobuf,
29    Transaction,
30    TransactionHash,
31    TransactionId,
32    TransactionResponse,
33    ValidateChecksums,
34};
35
36#[derive(Debug)]
37pub(super) struct SignaturePair {
38    signature: Vec<u8>,
39    public: PublicKey,
40}
41
42impl SignaturePair {
43    pub fn into_protobuf(self) -> services::SignaturePair {
44        let signature = match self.public.kind() {
45            crate::key::KeyKind::Ed25519 => {
46                services::signature_pair::Signature::Ed25519(self.signature)
47            }
48            crate::key::KeyKind::Ecdsa => {
49                services::signature_pair::Signature::EcdsaSecp256k1(self.signature)
50            }
51        };
52        services::SignaturePair {
53            signature: Some(signature),
54            // TODO: is there any way to utilize the _prefix_ nature of this field?
55            pub_key_prefix: self.public.to_bytes_raw(),
56        }
57    }
58}
59
60impl From<(PublicKey, Vec<u8>)> for SignaturePair {
61    fn from((public, signature): (PublicKey, Vec<u8>)) -> Self {
62        Self { signature, public }
63    }
64}
65
66impl<D> Transaction<D>
67where
68    D: TransactionData + ToTransactionDataProtobuf,
69{
70    pub(crate) fn make_request_inner(
71        &self,
72        chunk_info: &ChunkInfo,
73    ) -> (services::Transaction, TransactionHash) {
74        let transaction_body = self.to_transaction_body_protobuf(chunk_info);
75
76        let body_bytes = transaction_body.encode_to_vec();
77
78        let mut signatures = Vec::with_capacity(1 + self.signers.len());
79
80        if let Some(operator) = &self.body.operator {
81            let operator_signature = operator.sign(&body_bytes);
82
83            signatures.push(SignaturePair::from(operator_signature).into_protobuf());
84        }
85
86        for signer in &self.signers {
87            let public_key = signer.public_key().to_bytes();
88            if !signatures.iter().any(|it| public_key.starts_with(&it.pub_key_prefix)) {
89                let signature = signer.sign(&body_bytes);
90                signatures.push(SignaturePair::from(signature).into_protobuf());
91            }
92        }
93
94        let signed_transaction = services::SignedTransaction {
95            body_bytes,
96            sig_map: Some(services::SignatureMap { sig_pair: signatures }),
97            use_serialized_tx_message_hash_algorithm: false,
98        };
99
100        let signed_transaction_bytes = signed_transaction.encode_to_vec();
101
102        let transaction_hash = TransactionHash::new(&signed_transaction_bytes);
103
104        let transaction =
105            services::Transaction { signed_transaction_bytes, ..services::Transaction::default() };
106
107        (transaction, transaction_hash)
108    }
109}
110
111/// Pre-execute associated fields for transaction data.
112pub trait TransactionData: Clone + Into<AnyTransactionData> {
113    /// Whether this transaction is intended to be executed to return a cost estimate.
114    #[doc(hidden)]
115    fn for_cost_estimate(&self) -> bool {
116        false
117    }
118
119    /// Returns the maximum allowed transaction fee if none is specified.
120    ///
121    /// Specifically, this default will be used in the following case:
122    /// - The transaction itself (direct user input) has no `max_transaction_fee` specified, AND
123    /// - The [`Client`](crate::Client) has no `max_transaction_fee` specified.
124    fn default_max_transaction_fee(&self) -> Hbar {
125        Hbar::new(2)
126    }
127
128    /// Returns the chunk data for this transaction if this is a chunked transaction.
129    fn maybe_chunk_data(&self) -> Option<&ChunkData> {
130        None
131    }
132
133    /// Returns `true` if `self` is a chunked transaction *and* it should wait for receipts between each chunk.
134    fn wait_for_receipt(&self) -> bool {
135        false
136    }
137}
138
139pub trait TransactionExecute:
140    ToTransactionDataProtobuf + TransactionData + ValidateChecksums
141{
142    fn execute(
143        &self,
144        channel: Channel,
145        request: services::Transaction,
146    ) -> BoxGrpcFuture<'_, services::TransactionResponse>;
147}
148
149impl<D> Execute for Transaction<D>
150where
151    D: TransactionExecute,
152{
153    type GrpcRequest = services::Transaction;
154
155    type GrpcResponse = services::TransactionResponse;
156
157    type Context = TransactionHash;
158
159    type Response = TransactionResponse;
160
161    fn node_account_ids(&self) -> Option<&[AccountId]> {
162        self.body.node_account_ids.as_deref()
163    }
164
165    fn transaction_id(&self) -> Option<TransactionId> {
166        self.body.transaction_id
167    }
168
169    fn requires_transaction_id(&self) -> bool {
170        true
171    }
172
173    fn operator_account_id(&self) -> Option<&AccountId> {
174        self.body.operator.as_deref().map(|it| &it.account_id)
175    }
176
177    fn regenerate_transaction_id(&self) -> Option<bool> {
178        self.body.regenerate_transaction_id
179    }
180
181    fn grpc_deadline(&self) -> Option<std::time::Duration> {
182        self.grpc_deadline
183    }
184
185    fn request_timeout(&self) -> Option<std::time::Duration> {
186        self.request_timeout
187    }
188
189    fn make_request(
190        &self,
191        transaction_id: Option<&TransactionId>,
192        node_account_id: AccountId,
193    ) -> crate::Result<(Self::GrpcRequest, Self::Context)> {
194        assert!(self.is_frozen());
195
196        Ok(self.make_request_inner(&ChunkInfo::single(
197            *transaction_id.ok_or(Error::NoPayerAccountOrTransactionId)?,
198            node_account_id,
199        )))
200    }
201
202    fn execute(
203        &self,
204        channel: Channel,
205        request: Self::GrpcRequest,
206    ) -> BoxGrpcFuture<'_, Self::GrpcResponse> {
207        self.body.data.execute(channel, request)
208    }
209
210    fn make_response(
211        &self,
212        _response: Self::GrpcResponse,
213        transaction_hash: Self::Context,
214        node_account_id: AccountId,
215        transaction_id: Option<&TransactionId>,
216    ) -> crate::Result<Self::Response> {
217        Ok(TransactionResponse {
218            node_account_id,
219            transaction_id: *transaction_id.unwrap(),
220            transaction_hash,
221            validate_status: true,
222        })
223    }
224
225    fn make_error_pre_check(
226        &self,
227        status: crate::Status,
228        transaction_id: Option<&TransactionId>,
229        response: Self::GrpcResponse,
230    ) -> crate::Error {
231        crate::Error::TransactionPreCheckStatus {
232            status,
233            cost: (response.cost != 0).then(|| Hbar::from_tinybars(response.cost as i64)),
234            transaction_id: Box::new(
235                *transaction_id.expect("transactions must have transaction IDs"),
236            ),
237        }
238    }
239
240    fn response_pre_check_status(response: &Self::GrpcResponse) -> crate::Result<i32> {
241        Ok(response.node_transaction_precheck_code)
242    }
243}
244
245/// Marker trait for transactions that support Chunking.
246pub trait TransactionExecuteChunked: TransactionExecute {}
247
248impl<D: ValidateChecksums> ValidateChecksums for Transaction<D> {
249    fn validate_checksums(&self, ledger_id: &RefLedgerId) -> Result<(), Error> {
250        if let Some(node_account_ids) = &self.body.node_account_ids {
251            for node_account_id in node_account_ids {
252                node_account_id.validate_checksums(ledger_id)?;
253            }
254        }
255        self.body.transaction_id.validate_checksums(ledger_id)?;
256        self.body.data.validate_checksums(ledger_id)
257    }
258}
259
260impl<D> Transaction<D>
261where
262    D: TransactionData + ToTransactionDataProtobuf,
263{
264    #[allow(deprecated)]
265    fn to_transaction_body_protobuf(&self, chunk_info: &ChunkInfo) -> services::TransactionBody {
266        let data = self.body.data.to_transaction_data_protobuf(chunk_info);
267
268        let transaction_fee = if self.body.data.for_cost_estimate() {
269            0
270        } else {
271            self.body
272                .max_transaction_fee
273                .unwrap_or_else(|| self.body.data.default_max_transaction_fee())
274                .to_tinybars() as u64
275        };
276
277        services::TransactionBody {
278            data: Some(data),
279            transaction_id: Some(chunk_info.current_transaction_id.to_protobuf()),
280            transaction_valid_duration: Some(
281                self.body
282                    .transaction_valid_duration
283                    .unwrap_or(DEFAULT_TRANSACTION_VALID_DURATION)
284                    .into(),
285            ),
286            memo: self.body.transaction_memo.clone(),
287            node_account_id: chunk_info.node_account_id.to_protobuf(),
288            generate_record: false,
289            transaction_fee,
290            max_custom_fees: vec![],
291            batch_key: None,
292        }
293    }
294}
295
296// fixme: find a better name.
297pub(crate) struct SourceTransaction<'a, D> {
298    inner: &'a Transaction<D>,
299    sources: Cow<'a, TransactionSources>,
300}
301
302impl<'a, D> SourceTransaction<'a, D> {
303    pub(crate) fn new(transaction: &'a Transaction<D>, sources: &'a TransactionSources) -> Self {
304        // fixme: be way more lazy.
305        let sources = sources.sign_with(&transaction.signers);
306
307        Self { inner: transaction, sources }
308    }
309
310    pub(crate) async fn execute(
311        &self,
312        client: &Client,
313        timeout: Option<std::time::Duration>,
314    ) -> crate::Result<TransactionResponse>
315    where
316        D: TransactionExecute,
317    {
318        Ok(self.execute_all(client, timeout).await?.swap_remove(0))
319    }
320
321    pub(crate) async fn execute_all(
322        &self,
323        client: &Client,
324        timeout_per_chunk: Option<std::time::Duration>,
325    ) -> crate::Result<Vec<TransactionResponse>>
326    where
327        D: TransactionExecute,
328    {
329        let mut responses = Vec::with_capacity(self.sources.chunks_len());
330        for chunk in self.sources.chunks() {
331            let response = crate::execute::execute(
332                client,
333                &SourceTransactionExecuteView::new(self.inner, chunk),
334                timeout_per_chunk,
335            )
336            .await?;
337
338            if self.inner.data().wait_for_receipt() {
339                response.get_receipt(client).await?;
340            }
341
342            responses.push(response);
343        }
344
345        Ok(responses)
346    }
347}
348
349// fixme: better name.
350struct SourceTransactionExecuteView<'a, D> {
351    transaction: &'a Transaction<D>,
352    chunk: SourceChunk<'a>,
353    indecies_by_node_id: HashMap<AccountId, usize>,
354}
355
356impl<'a, D> SourceTransactionExecuteView<'a, D> {
357    fn new(transaction: &'a Transaction<D>, chunk: SourceChunk<'a>) -> Self {
358        let indecies_by_node_id =
359            chunk.node_ids().iter().copied().enumerate().map(|it| (it.1, it.0)).collect();
360        Self { transaction, chunk, indecies_by_node_id }
361    }
362}
363
364impl<'a, D: ValidateChecksums> ValidateChecksums for SourceTransactionExecuteView<'a, D> {
365    fn validate_checksums(&self, ledger_id: &RefLedgerId) -> Result<(), Error> {
366        self.transaction.validate_checksums(ledger_id)
367    }
368}
369
370impl<'a, D: TransactionExecute> Execute for SourceTransactionExecuteView<'a, D> {
371    type GrpcRequest = <Transaction<D> as Execute>::GrpcRequest;
372
373    type GrpcResponse = <Transaction<D> as Execute>::GrpcResponse;
374
375    type Context = <Transaction<D> as Execute>::Context;
376
377    type Response = <Transaction<D> as Execute>::Response;
378
379    fn node_account_ids(&self) -> Option<&[AccountId]> {
380        let node_ids = self.chunk.node_ids();
381        if node_ids.is_empty() {
382            None // Use client's default nodes
383        } else {
384            Some(node_ids)
385        }
386    }
387
388    fn transaction_id(&self) -> Option<TransactionId> {
389        self.chunk.transaction_id()
390    }
391
392    fn requires_transaction_id(&self) -> bool {
393        true
394    }
395
396    fn operator_account_id(&self) -> Option<&AccountId> {
397        None
398    }
399
400    fn regenerate_transaction_id(&self) -> Option<bool> {
401        Some(self.chunk.transaction_id().is_none())
402    }
403
404    fn make_request(
405        &self,
406        transaction_id: Option<&TransactionId>,
407        node_account_id: AccountId,
408    ) -> crate::Result<(Self::GrpcRequest, Self::Context)> {
409        debug_assert_eq!(transaction_id, self.transaction_id().as_ref());
410
411        let index = *self.indecies_by_node_id.get(&node_account_id).unwrap();
412        Ok((self.chunk.transactions()[index].clone(), self.chunk.transaction_hashes()[index]))
413    }
414
415    fn execute(
416        &self,
417        channel: Channel,
418        request: Self::GrpcRequest,
419    ) -> BoxGrpcFuture<Self::GrpcResponse> {
420        self.transaction.execute(channel, request)
421    }
422
423    fn make_response(
424        &self,
425        response: Self::GrpcResponse,
426        context: Self::Context,
427        node_account_id: AccountId,
428        transaction_id: Option<&TransactionId>,
429    ) -> crate::Result<Self::Response> {
430        self.transaction.make_response(response, context, node_account_id, transaction_id)
431    }
432
433    fn make_error_pre_check(
434        &self,
435        status: crate::Status,
436        transaction_id: Option<&TransactionId>,
437        response: Self::GrpcResponse,
438    ) -> crate::Error {
439        self.transaction.make_error_pre_check(status, transaction_id, response)
440    }
441
442    fn response_pre_check_status(response: &Self::GrpcResponse) -> crate::Result<i32> {
443        Transaction::<D>::response_pre_check_status(response)
444    }
445}