Skip to main content

hiero_sdk/transaction/
execute.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::borrow::Cow;
4use std::collections::HashMap;
5
6use hiero_sdk_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: { self.body.custom_fee_limits.to_protobuf() },
291            batch_key: None,
292            high_volume: self.body.high_volume,
293        }
294    }
295}
296
297// fixme: find a better name.
298pub(crate) struct SourceTransaction<'a, D> {
299    inner: &'a Transaction<D>,
300    sources: Cow<'a, TransactionSources>,
301}
302
303impl<'a, D> SourceTransaction<'a, D> {
304    pub(crate) fn new(transaction: &'a Transaction<D>, sources: &'a TransactionSources) -> Self {
305        // fixme: be way more lazy.
306        let sources = sources.sign_with(&transaction.signers);
307
308        Self { inner: transaction, sources }
309    }
310
311    pub(crate) async fn execute(
312        &self,
313        client: &Client,
314        timeout: Option<std::time::Duration>,
315    ) -> crate::Result<TransactionResponse>
316    where
317        D: TransactionExecute,
318    {
319        Ok(self.execute_all(client, timeout).await?.swap_remove(0))
320    }
321
322    pub(crate) async fn execute_all(
323        &self,
324        client: &Client,
325        timeout_per_chunk: Option<std::time::Duration>,
326    ) -> crate::Result<Vec<TransactionResponse>>
327    where
328        D: TransactionExecute,
329    {
330        let mut responses = Vec::with_capacity(self.sources.chunks_len());
331        for chunk in self.sources.chunks() {
332            let response = crate::execute::execute(
333                client,
334                &SourceTransactionExecuteView::new(self.inner, chunk),
335                timeout_per_chunk,
336            )
337            .await?;
338
339            if self.inner.data().wait_for_receipt() {
340                response.get_receipt(client).await?;
341            }
342
343            responses.push(response);
344        }
345
346        Ok(responses)
347    }
348}
349
350// fixme: better name.
351struct SourceTransactionExecuteView<'a, D> {
352    transaction: &'a Transaction<D>,
353    chunk: SourceChunk<'a>,
354    indecies_by_node_id: HashMap<AccountId, usize>,
355}
356
357impl<'a, D> SourceTransactionExecuteView<'a, D> {
358    fn new(transaction: &'a Transaction<D>, chunk: SourceChunk<'a>) -> Self {
359        let indecies_by_node_id =
360            chunk.node_ids().iter().copied().enumerate().map(|it| (it.1, it.0)).collect();
361        Self { transaction, chunk, indecies_by_node_id }
362    }
363}
364
365impl<'a, D: ValidateChecksums> ValidateChecksums for SourceTransactionExecuteView<'a, D> {
366    fn validate_checksums(&self, ledger_id: &RefLedgerId) -> Result<(), Error> {
367        self.transaction.validate_checksums(ledger_id)
368    }
369}
370
371impl<'a, D: TransactionExecute> Execute for SourceTransactionExecuteView<'a, D> {
372    type GrpcRequest = <Transaction<D> as Execute>::GrpcRequest;
373
374    type GrpcResponse = <Transaction<D> as Execute>::GrpcResponse;
375
376    type Context = <Transaction<D> as Execute>::Context;
377
378    type Response = <Transaction<D> as Execute>::Response;
379
380    fn node_account_ids(&self) -> Option<&[AccountId]> {
381        let node_ids = self.chunk.node_ids();
382        if node_ids.is_empty() {
383            None // Use client's default nodes
384        } else {
385            Some(node_ids)
386        }
387    }
388
389    fn transaction_id(&self) -> Option<TransactionId> {
390        self.chunk.transaction_id()
391    }
392
393    fn requires_transaction_id(&self) -> bool {
394        true
395    }
396
397    fn operator_account_id(&self) -> Option<&AccountId> {
398        None
399    }
400
401    fn regenerate_transaction_id(&self) -> Option<bool> {
402        Some(self.chunk.transaction_id().is_none())
403    }
404
405    fn make_request(
406        &self,
407        transaction_id: Option<&TransactionId>,
408        node_account_id: AccountId,
409    ) -> crate::Result<(Self::GrpcRequest, Self::Context)> {
410        debug_assert_eq!(transaction_id, self.transaction_id().as_ref());
411
412        let index = *self.indecies_by_node_id.get(&node_account_id).unwrap();
413        Ok((self.chunk.transactions()[index].clone(), self.chunk.transaction_hashes()[index]))
414    }
415
416    fn execute(
417        &self,
418        channel: Channel,
419        request: Self::GrpcRequest,
420    ) -> BoxGrpcFuture<Self::GrpcResponse> {
421        self.transaction.execute(channel, request)
422    }
423
424    fn make_response(
425        &self,
426        response: Self::GrpcResponse,
427        context: Self::Context,
428        node_account_id: AccountId,
429        transaction_id: Option<&TransactionId>,
430    ) -> crate::Result<Self::Response> {
431        self.transaction.make_response(response, context, node_account_id, transaction_id)
432    }
433
434    fn make_error_pre_check(
435        &self,
436        status: crate::Status,
437        transaction_id: Option<&TransactionId>,
438        response: Self::GrpcResponse,
439    ) -> crate::Error {
440        self.transaction.make_error_pre_check(status, transaction_id, response)
441    }
442
443    fn response_pre_check_status(response: &Self::GrpcResponse) -> crate::Result<i32> {
444        Transaction::<D>::response_pre_check_status(response)
445    }
446}