1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
use core::num::{ParseFloatError, ParseIntError};
use sp_io::offchain::timestamp;
use sp_runtime::offchain::{
    http::Request,
    http::{Error, Method},
    Duration, HttpError,
};
use sp_std::{str, vec, vec::Vec};

use core::convert::TryInto;

use crate::{AccountId, IntoAccountId, StellarSdkError};

use super::{
    api_response_types::FeeStats, json_response_types, Horizon, HTTP_HEADER_CLIENT_NAME,
    HTTP_HEADER_CLIENT_VERSION,
};

impl From<ParseIntError> for FetchError {
    fn from(error: ParseIntError) -> Self {
        FetchError::ParseIntError(error)
    }
}

impl From<ParseFloatError> for FetchError {
    fn from(error: ParseFloatError) -> Self {
        FetchError::ParseFloatError(error)
    }
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub enum FetchError {
    DeadlineReached,
    IoError,
    Invalid,
    Unknown,
    UnexpectedResponseStatus { status: u16, body: Vec<u8> },
    JsonParseError,
    InvalidSequenceNumber,
    ParseIntError(ParseIntError),
    ParseFloatError(ParseFloatError),
    AccountRequiredMemo(AccountId),
}

impl From<Error> for FetchError {
    fn from(error: Error) -> Self {
        match error {
            Error::DeadlineReached => FetchError::DeadlineReached,
            Error::IoError => FetchError::IoError,
            Error::Unknown => FetchError::Unknown,
        }
    }
}

impl From<FetchError> for StellarSdkError {
    fn from(error: FetchError) -> Self {
        StellarSdkError::FetchError(error)
    }
}

impl From<HttpError> for FetchError {
    fn from(error: HttpError) -> Self {
        match error {
            HttpError::DeadlineReached => FetchError::DeadlineReached,
            HttpError::IoError => FetchError::IoError,
            HttpError::Invalid => FetchError::Invalid,
        }
    }
}

impl From<serde_json::Error> for FetchError {
    fn from(_error: serde_json::Error) -> Self {
        FetchError::JsonParseError
    }
}

impl Horizon {
    pub fn request(
        &self,
        path: Vec<&[u8]>,
        method: Method,
        timeout_milliseconds: u64,
    ) -> Result<Vec<u8>, FetchError> {
        let mut url = self.base_url.clone();
        for path_segment in path {
            url.extend_from_slice(path_segment);
        }

        let request =
            Request::<Vec<&'static [u8]>>::new(str::from_utf8(&url).unwrap()).method(method);
        let timeout = timestamp().add(Duration::from_millis(timeout_milliseconds));
        let pending = request
            .add_header("X-Client-Name", HTTP_HEADER_CLIENT_NAME)
            .add_header("X-Client-Version", HTTP_HEADER_CLIENT_VERSION)
            .deadline(timeout)
            .send()?;

        let response = pending
            .try_wait(timeout)
            .map_err(|_| FetchError::DeadlineReached)?;
        let response = response?;

        if response.code != 200 {
            return Err(FetchError::UnexpectedResponseStatus {
                status: response.code,
                body: response.body().collect(),
            });
        }

        Ok(response.body().collect())
    }

    /// Fetch the sequence number of an account
    ///
    /// The sequence number is defined to be of type [i64](https://github.com/stellar/stellar-core/blob/master/src/xdr/Stellar-ledger-entries.x)
    pub fn fetch_fee_stats(&self, timeout_milliseconds: u64) -> Result<FeeStats, FetchError> {
        let json = self.request(vec![b"/fee_stats"], Method::Get, timeout_milliseconds)?;

        let response: json_response_types::FeeStats = serde_json::from_slice(&json)?;

        response.try_into()
    }

    /// Fetch the sequence number of an account
    ///
    /// The sequence number is defined to be of type [i64](https://github.com/stellar/stellar-core/blob/master/src/xdr/Stellar-ledger-entries.x)
    pub fn fetch_account<T: IntoAccountId>(
        &self,
        account_id: T,
        timeout_milliseconds: u64,
    ) -> Result<json_response_types::AccountResponse, FetchError> {
        let json = self.request(
            vec![b"/accounts/", account_id.into_encoding().as_slice()],
            Method::Get,
            timeout_milliseconds,
        )?;

        let account_response: json_response_types::AccountResponse = serde_json::from_slice(&json)?;

        Ok(account_response)
    }

    /// Fetch the sequence number of an account
    ///
    /// The sequence number is defined to be of type [i64](https://github.com/stellar/stellar-core/blob/master/src/xdr/Stellar-ledger-entries.x)
    pub fn fetch_next_sequence_number<T: IntoAccountId>(
        &self,
        account_id: T,
        timeout_milliseconds: u64,
    ) -> Result<i64, FetchError> {
        let account_response = self.fetch_account(account_id, timeout_milliseconds)?;

        let sequence_number: i64 = match account_response.sequence.parse() {
            Ok(n) => n,
            Err(_) => return Err(FetchError::InvalidSequenceNumber),
        };
        let next_sequence_number = sequence_number + 1;
        Ok(next_sequence_number)
    }
}