iota_client/node/
address.rs

1// Copyright 2021 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{Api, Client, Error, Result};
5
6use bee_message::prelude::{TransactionId, UtxoInput};
7
8use bee_rest_api::types::{
9    body::SuccessBody,
10    responses::{BalanceAddressResponse, OutputsAddressResponse},
11};
12
13use std::convert::TryInto;
14
15const OUTPUT_ID_LENGTH: usize = 68;
16const TRANSACTION_ID_LENGTH: usize = 64;
17
18/// Output type filter.
19#[derive(Clone, Debug, Serialize, Deserialize)]
20pub enum OutputType {
21    /// Signature locked single output.
22    SignatureLockedSingle,
23    /// Dust allowance output.
24    SignatureLockedDustAllowance,
25}
26
27impl From<OutputType> for u16 {
28    fn from(value: OutputType) -> Self {
29        match value {
30            OutputType::SignatureLockedSingle => 0,
31            OutputType::SignatureLockedDustAllowance => 1,
32        }
33    }
34}
35
36/// The outputs query options.
37#[derive(Default, Clone, Serialize, Deserialize)]
38pub struct OutputsOptions {
39    /// Whether the query should include spent outputs or not.
40    #[serde(rename = "includeSpent")]
41    pub include_spent: bool,
42    #[serde(rename = "outputType")]
43    /// The output type filter.
44    pub output_type: Option<OutputType>,
45}
46
47impl OutputsOptions {
48    fn into_query(self) -> Option<String> {
49        let mut params = Vec::new();
50        if self.include_spent {
51            params.push("include-spent=true".to_string());
52        }
53        if let Some(output_type) = self.output_type {
54            params.push(format!("type={}", u16::from(output_type)))
55        }
56        if params.is_empty() {
57            None
58        } else {
59            Some(params.join("&"))
60        }
61    }
62}
63
64/// Builder of GET /api/v1/address/{address} endpoint
65pub struct GetAddressBuilder<'a> {
66    client: &'a Client,
67}
68
69impl<'a> GetAddressBuilder<'a> {
70    /// Create GET /api/v1/address/{address} endpoint builder
71    pub fn new(client: &'a Client) -> Self {
72        Self { client }
73    }
74
75    /// Consume the builder and get the balance of a given Bech32 encoded address.
76    /// If count equals maxResults, then there might be more outputs available but those were skipped for performance
77    /// reasons. User should sweep the address to reduce the amount of outputs.
78    pub async fn balance(self, address: &str) -> Result<BalanceAddressResponse> {
79        let path = &format!("api/v1/addresses/{address}");
80
81        let resp: SuccessBody<BalanceAddressResponse> = self
82            .client
83            .node_manager
84            .get_request(path, None, self.client.get_timeout(Api::GetBalance))
85            .await?;
86
87        Ok(resp.data)
88    }
89
90    /// Consume the builder and get all outputs that use a given address.
91    /// If count equals maxResults, then there might be more outputs available but those were skipped for performance
92    /// reasons. User should sweep the address to reduce the amount of outputs.
93    pub async fn outputs(self, address: &str, options: OutputsOptions) -> Result<Box<[UtxoInput]>> {
94        let path = format!("api/v1/addresses/{address}/outputs");
95
96        let resp: SuccessBody<OutputsAddressResponse> = self
97            .client
98            .node_manager
99            .get_request(
100                &path,
101                options.into_query().as_deref(),
102                self.client.get_timeout(Api::GetOutput),
103            )
104            .await?;
105
106        resp.data
107            .output_ids
108            .iter()
109            .map(|s| {
110                if s.len() == OUTPUT_ID_LENGTH {
111                    let mut transaction_id = [0u8; 32];
112                    hex::decode_to_slice(&s[..TRANSACTION_ID_LENGTH], &mut transaction_id)?;
113                    let index = u16::from_le_bytes(
114                        hex::decode(&s[TRANSACTION_ID_LENGTH..]).map_err(|_| Error::InvalidParameter("index"))?[..]
115                            .try_into()
116                            .map_err(|_| Error::InvalidParameter("index"))?,
117                    );
118                    Ok(UtxoInput::new(TransactionId::new(transaction_id), index)?)
119                } else {
120                    Err(Error::OutputError("Invalid output length from API response"))
121                }
122            })
123            .collect::<Result<Box<[UtxoInput]>>>()
124    }
125
126    /// Consume the builder and get the OutputsAddressResponse for a given address.
127    /// If count equals maxResults, then there might be more outputs available but those were skipped for performance
128    /// reasons. User should sweep the address to reduce the amount of outputs.
129    pub async fn outputs_response(self, address: &str, options: OutputsOptions) -> Result<OutputsAddressResponse> {
130        let path = format!("api/v1/addresses/{address}/outputs");
131
132        let resp: SuccessBody<OutputsAddressResponse> = self
133            .client
134            .node_manager
135            .get_request(
136                &path,
137                options.into_query().as_deref(),
138                self.client.get_timeout(Api::GetOutput),
139            )
140            .await?;
141
142        Ok(resp.data)
143    }
144}