iota_sdk/wallet/account/operations/syncing/
outputs.rs

1// Copyright 2021 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use crypto::keys::bip44::Bip44;
5use instant::Instant;
6
7use crate::{
8    client::{secret::SecretManage, Client},
9    types::{
10        api::core::response::OutputWithMetadataResponse,
11        block::{
12            input::Input,
13            output::{OutputId, OutputWithMetadata},
14            payload::{
15                transaction::{TransactionEssence, TransactionId},
16                Payload, TransactionPayload,
17            },
18        },
19    },
20    wallet::{
21        account::{build_transaction_from_payload_and_inputs, types::OutputData, Account, AddressWithUnspentOutputs},
22        task,
23    },
24};
25
26impl<S: 'static + SecretManage> Account<S>
27where
28    crate::wallet::Error: From<S::Error>,
29{
30    /// Convert OutputWithMetadataResponse to OutputData with the network_id added
31    pub(crate) async fn output_response_to_output_data(
32        &self,
33        outputs_with_meta: Vec<OutputWithMetadata>,
34        associated_address: &AddressWithUnspentOutputs,
35    ) -> crate::wallet::Result<Vec<OutputData>> {
36        log::debug!("[SYNC] convert output_responses");
37        // store outputs with network_id
38        let network_id = self.client().get_network_id().await?;
39        let account_details = self.details().await;
40
41        Ok(outputs_with_meta
42            .into_iter()
43            .map(|output_with_meta| {
44                // check if we know the transaction that created this output and if we created it (if we store incoming
45                // transactions separated, then this check wouldn't be required)
46                let remainder = account_details
47                    .transactions
48                    .get(output_with_meta.metadata().transaction_id())
49                    .map_or(false, |tx| !tx.incoming);
50
51                // BIP 44 (HD wallets) and 4218 is the registered index for IOTA https://github.com/satoshilabs/slips/blob/master/slip-0044.md
52                let chain = Bip44::new(account_details.coin_type)
53                    .with_account(account_details.index)
54                    .with_change(associated_address.internal as _)
55                    .with_address_index(associated_address.key_index);
56
57                OutputData {
58                    output_id: output_with_meta.metadata().output_id().to_owned(),
59                    metadata: *output_with_meta.metadata(),
60                    output: output_with_meta.output().clone(),
61                    is_spent: output_with_meta.metadata().is_spent(),
62                    address: associated_address.address.inner,
63                    network_id,
64                    remainder,
65                    chain: Some(chain),
66                }
67            })
68            .collect())
69    }
70
71    /// Gets outputs by their id, already known outputs are not requested again, but loaded from the account set as
72    /// unspent, because we wouldn't get them from the node if they were spent
73    pub(crate) async fn get_outputs(
74        &self,
75        output_ids: Vec<OutputId>,
76    ) -> crate::wallet::Result<Vec<OutputWithMetadata>> {
77        log::debug!("[SYNC] start get_outputs");
78        let get_outputs_start_time = Instant::now();
79        let mut outputs = Vec::new();
80        let mut unknown_outputs = Vec::new();
81        let mut unspent_outputs = Vec::new();
82        let mut account_details = self.details_mut().await;
83
84        for output_id in output_ids {
85            match account_details.outputs.get_mut(&output_id) {
86                // set unspent
87                Some(output_data) => {
88                    output_data.is_spent = false;
89                    unspent_outputs.push((output_id, output_data.clone()));
90                    outputs.push(OutputWithMetadata::new(
91                        output_data.output.clone(),
92                        output_data.metadata,
93                    ));
94                }
95                None => unknown_outputs.push(output_id),
96            }
97        }
98        // known output is unspent, so insert it to the unspent outputs again, because if it was an
99        // alias/nft/foundry output it could have been removed when syncing without them
100        for (output_id, output_data) in unspent_outputs {
101            account_details.unspent_outputs.insert(output_id, output_data);
102        }
103
104        drop(account_details);
105
106        if !unknown_outputs.is_empty() {
107            outputs.extend(self.client().get_outputs(&unknown_outputs).await?);
108        }
109
110        log::debug!(
111            "[SYNC] finished get_outputs in {:.2?}",
112            get_outputs_start_time.elapsed()
113        );
114
115        Ok(outputs)
116    }
117
118    // Try to get transactions and inputs for received outputs
119    // Because the transactions and outputs are pruned, we might can not get them anymore, in that case errors are not
120    // returned
121    pub(crate) async fn request_incoming_transaction_data(
122        &self,
123        mut transaction_ids: Vec<TransactionId>,
124    ) -> crate::wallet::Result<()> {
125        log::debug!("[SYNC] request_incoming_transaction_data");
126
127        let account_details = self.details().await;
128        transaction_ids.retain(|transaction_id| {
129            !(account_details.transactions.contains_key(transaction_id)
130                || account_details.incoming_transactions.contains_key(transaction_id)
131                || account_details
132                    .inaccessible_incoming_transactions
133                    .contains(transaction_id))
134        });
135        drop(account_details);
136
137        // Limit parallel requests to 100, to avoid timeouts
138        let results =
139            futures::future::try_join_all(transaction_ids.chunks(100).map(|x| x.to_vec()).map(|transaction_ids| {
140                let client = self.client().clone();
141                async move {
142                    task::spawn(async move {
143                        futures::future::try_join_all(transaction_ids.iter().map(|transaction_id| async {
144                            let transaction_id = *transaction_id;
145                            match client.get_included_block(&transaction_id).await {
146                                Ok(block) => {
147                                    if let Some(Payload::Transaction(transaction_payload)) = block.payload() {
148                                        let inputs_with_meta =
149                                            get_inputs_for_transaction_payload(&client, transaction_payload).await?;
150                                        let inputs_response: Vec<OutputWithMetadataResponse> = inputs_with_meta
151                                            .into_iter()
152                                            .map(OutputWithMetadataResponse::from)
153                                            .collect();
154
155                                        let transaction = build_transaction_from_payload_and_inputs(
156                                            transaction_id,
157                                            *transaction_payload.clone(),
158                                            inputs_response,
159                                        )?;
160
161                                        Ok((transaction_id, Some(transaction)))
162                                    } else {
163                                        Ok((transaction_id, None))
164                                    }
165                                }
166                                Err(crate::client::Error::Node(crate::client::node_api::error::Error::NotFound(_))) => {
167                                    Ok((transaction_id, None))
168                                }
169                                Err(e) => Err(crate::wallet::Error::Client(e.into())),
170                            }
171                        }))
172                        .await
173                    })
174                    .await?
175                }
176            }))
177            .await?;
178
179        // Update account with new transactions
180        let mut account_details = self.details_mut().await;
181        for (transaction_id, txn) in results.into_iter().flatten() {
182            if let Some(transaction) = txn {
183                account_details
184                    .incoming_transactions
185                    .insert(transaction_id, transaction);
186            } else {
187                log::debug!("[SYNC] adding {transaction_id} to inaccessible_incoming_transactions");
188                // Save transactions that weren't found by the node to avoid requesting them endlessly.
189                // Will be cleared when new client options are provided.
190                account_details
191                    .inaccessible_incoming_transactions
192                    .insert(transaction_id);
193            }
194        }
195
196        Ok(())
197    }
198}
199
200// Try to fetch the inputs of the transaction
201pub(crate) async fn get_inputs_for_transaction_payload(
202    client: &Client,
203    transaction_payload: &TransactionPayload,
204) -> crate::wallet::Result<Vec<OutputWithMetadata>> {
205    let TransactionEssence::Regular(essence) = transaction_payload.essence();
206
207    let output_ids = essence
208        .inputs()
209        .iter()
210        .filter_map(|input| {
211            if let Input::Utxo(input) = input {
212                Some(*input.output_id())
213            } else {
214                None
215            }
216        })
217        .collect::<Vec<_>>();
218
219    client
220        .get_outputs_ignore_errors(&output_ids)
221        .await
222        .map_err(|e| e.into())
223}