Skip to main content

monero_daemon_rpc/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3#![deny(missing_docs)]
4#![cfg_attr(not(feature = "std"), no_std)]
5
6use core::{fmt::Debug, future::Future};
7
8extern crate alloc;
9use alloc::{
10  format, vec,
11  vec::Vec,
12  string::{String, ToString},
13};
14
15use serde::{Deserialize, de::DeserializeOwned};
16use serde_json::Value;
17
18use monero_oxide::transaction::{Input, Output, NotPruned, Pruned, Transaction};
19use monero_address::Address;
20
21use monero_interface::*;
22
23mod blocks;
24mod bin_rpc;
25
26// https://github.com/monero-project/monero/blob/8e9ab9677f90492bca3c7555a246f2a8677bd570
27//   /src/cryptonote_config.h#L134
28// https://github.com/monero-project/monero/blob/8e9ab9677f90492bca3c7555a246f2a8677bd570
29//   /src/rpc/core_rpc_server.cpp#L427
30// https://github.com/monero-project/monero/blob/8e9ab9677f90492bca3c7555a246f2a8677bd57
31//   /contrib/epee/include/net/http_protocol_handler.inl#L283
32const MAX_REQUEST_SIZE: usize = 1024 * 1024;
33
34/*
35  Monero has two distinct limits on responses. The first is the maximum size for the queue of
36  messages to send, which is 100 MiB.
37
38  https://github.com/monero-project/monero/blob/b591866fcfed400bc89631686655aa769ec5f2dd
39    /contrib/epee/include/net/abstract_tcp_server2.h#L68
40
41  The second is the maximum size for the amount of in-flight bytes when a new response is queued,
42  which is 25 MiB.
43
44  https://github.com/monero-project/monero/blob/8e9ab9677f90492bca3c7555a246f2a8677bd570/
45    src/cryptonote_config.h#L133
46  https://github.com/monero-project/monero/blob/8e9ab9677f90492bca3c7555a246f2a8677bd570
47    /src/rpc/core_rpc_server.cpp#L3922-L3925
48
49  We only apply the former bound per the following comment:
50
51  https://github.com/monero-project/monero/blob/8e9ab9677f90492bca3c7555a246f2a8677bd570
52    /contrib/epee/include/net/abstract_tcp_server2.inl#L793-L797
53
54  and our documented bound that the transport completely read a response _before_ reusing it for
55  the next request.
56*/
57const MAX_RESPONSE_SIZE: usize = 100 * 1024 * 1024;
58
59// These are our own constants used for determining our own bounds on response sizes
60const HTTP_OVERHEAD_ESTIMATE: usize = u16::MAX as usize;
61const REQUEST_SIZE_TARGET: usize = MAX_REQUEST_SIZE - HTTP_OVERHEAD_ESTIMATE - 2048;
62const JSON_BYTE_OVERHEAD_FACTOR_ESTIMATE: usize = 8;
63// Every response should have _some_ amount of bytes, for which this is an estimate
64const MIN_RESPONSE_SIZE_IN_BYTES_ESTIMATE: usize = 1024;
65
66/*
67  Monero doesn't have a size limit on miner transactions and accordingly doesn't have a size limit
68  on transactions, yet we would like _a_ bound (even if absurd) to limit a malicious remote node
69  from sending a gigantic HTTP response and wasting our bandwidth.
70
71  We use the bounds intended with the FCMP++ hard fork _now_ (as they should be absurd, exceeding
72  the entire default block size) to determine a bound (despite the fact these bounds aren't in
73  force yet).
74
75  https://github.com/seraphis-migration/monero/pull/104
76*/
77const MINER_TRANSACTION_OUTPUT_BOUND: usize = 10_000;
78// 2048 is used an approximation for the bounded size of the prefix for a miner transaction
79const MINER_TRANSACTION_SIZE_BOUND: usize =
80  2048 + (MINER_TRANSACTION_OUTPUT_BOUND * Output::SIZE_UPPER_BOUND.0);
81const TRANSACTION_SIZE_BOUND: usize = monero_oxide::primitives::const_max!(
82  MINER_TRANSACTION_SIZE_BOUND,
83  Transaction::<NotPruned>::NON_MINER_SIZE_UPPER_BOUND.0
84);
85
86fn rpc_hex(value: &str) -> Result<Vec<u8>, InterfaceError> {
87  hex::decode(value)
88    .map_err(|_| InterfaceError::InvalidInterface("expected hex wasn't hex".to_string()))
89}
90
91fn hash_hex(hash: &str) -> Result<[u8; 32], InterfaceError> {
92  rpc_hex(hash)?
93    .try_into()
94    .map_err(|_| InterfaceError::InvalidInterface("hash wasn't 32-bytes".to_string()))
95}
96
97#[derive(Deserialize)]
98struct JsonRpcResponse<T> {
99  result: T,
100  id: Option<usize>,
101}
102
103#[rustfmt::skip]
104/// An HTTP transport usable with a Monero daemon.
105///
106/// This is abstract such that users can use an HTTP library (which being their choice), a
107/// Tor/i2p-based transport, or even a memory buffer an external service somehow routes.
108///
109/// While no implementors are directly provided, [monero-simple-request-rpc](
110///   https://github.com/monero-oxide/monero-oxide/tree/main/monero-oxide/interface/daemon/simple-request
111/// ) is recommended.
112pub trait HttpTransport: Sync + Clone {
113  /// Perform a POST request to the specified route with the specified body.
114  ///
115  /// The response must be read in full BEFORE the underlying connection is reused for another
116  /// request. This is due to `monerod` terminating connections which have additional responses
117  /// sent while more than 25 MB from prior responses has yet to be read.
118  ///
119  /// The implementor is left to handle anything such as authentication.
120  fn post(
121    &self,
122    route: &str,
123    body: Vec<u8>,
124    response_size_limit: Option<usize>,
125  ) -> impl Send + Future<Output = Result<Vec<u8>, InterfaceError>>;
126}
127
128/// A connection to a Monero daemon.
129///
130/// This interface, if unable to fulfill a request (such as when requesting a non-existent block),
131/// may represent that as the interface being invalid (on the assumption requests made should be
132/// fulfilled). Please be mindful accordingly.
133#[derive(Clone)]
134pub struct MoneroDaemon<T: HttpTransport> {
135  transport: T,
136  response_size_limits: bool,
137  supports_json_rpc_batch_requests: bool,
138}
139
140impl<T: HttpTransport> MoneroDaemon<T> {
141  /// Construct a new connection to a Monero daemon.
142  pub async fn new(transport: T) -> Result<Self, InterfaceError> {
143    /*
144      TODO: We don't currently fetch the RPC version here. If we did, we would be able to know
145      which RPC routes are available and optimize accordingly. It'd also provide some level of
146      validation over the functionality expected to be offered.
147    */
148
149    let mut result =
150      Self { transport, response_size_limits: true, supports_json_rpc_batch_requests: true };
151
152    // https://github.com/monero-project/monero/issues/10118
153    {
154      const BATCH_REQUEST: &str = r#"[
155       { "jsonrpc": "2.0", "method": "on_get_block_hash", "params": [0], "id": 0 },
156       { "jsonrpc": "2.0", "method": "on_get_block_hash", "params": [1], "id": 1 }
157      ]"#;
158      let response: serde_json::Value =
159        result.rpc_call_internal("json_rpc", Some(BATCH_REQUEST.to_string()), 0).await?;
160      if let Some(error) = response.get("error") {
161        /*
162          If the server failed to parse our valid JSON, we assume it's because it's expecting an
163          object (while we sent an array, as allowed under the JSON-RPC 2.0 specification).
164
165          https://www.jsonrpc.org/specification#batch
166        */
167        if error.get("code") == Some(&serde_json::Value::from(-32700i32)) {
168          result.supports_json_rpc_batch_requests = false;
169        } else {
170          Err(InterfaceError::InvalidInterface(format!(
171            "interface returned error when attempting a batch request, code {:?}",
172            error.get("code").map(|code| code.as_number())
173          )))?;
174        }
175      }
176    }
177
178    Ok(result)
179  }
180
181  /// Whether to enable or disable response size limits.
182  ///
183  /// The default is to enable size limits on the response, preventing a malicious daemon from
184  /// transmitting a 1 GB response to a request for a single transaction. However, as Monero has
185  /// unbounded block sizes, miner transaction sizes, a completely correct transport cannot bound
186  /// any responses. This allows disable size limits on responses (not recommended) to ensure
187  /// correctness.
188  pub fn response_size_limits(&mut self, enabled: bool) {
189    self.response_size_limits = enabled;
190  }
191}
192
193impl<T: Debug + HttpTransport> core::fmt::Debug for MoneroDaemon<T> {
194  fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
195    fmt
196      .debug_struct("MoneroDaemon")
197      .field("transport", &self.transport)
198      .field("response_size_limits", &self.response_size_limits)
199      .field("supports_json_rpc_batch_requests", &self.supports_json_rpc_batch_requests)
200      .finish()
201  }
202}
203
204impl<T: HttpTransport> MoneroDaemon<T> {
205  async fn rpc_call_core(
206    &self,
207    route: &str,
208    params: Option<String>,
209    response_size_limit: usize,
210  ) -> Result<String, InterfaceError> {
211    let response_size_limit = {
212      let response_size_limit = response_size_limit.max(MIN_RESPONSE_SIZE_IN_BYTES_ESTIMATE);
213      let json_response_size_limt =
214        JSON_BYTE_OVERHEAD_FACTOR_ESTIMATE.saturating_mul(response_size_limit);
215      let full_response_size_limit = HTTP_OVERHEAD_ESTIMATE.saturating_add(json_response_size_limt);
216      full_response_size_limit.min(MAX_RESPONSE_SIZE)
217    };
218
219    let mut res = self
220      .transport
221      .post(
222        route,
223        if let Some(params) = params { params.into_bytes() } else { vec![] },
224        self.response_size_limits.then_some(response_size_limit),
225      )
226      .await?;
227
228    /*
229      If the transport erroneously returned more bytes, truncate it before we expand it into an
230      object. This may invalidate it, but it limits the impact of a DoS the transport was supposed
231      to prevent.
232    */
233    res.truncate(response_size_limit);
234
235    std_shims::string::String::from_utf8(res)
236      .map_err(|_| InterfaceError::InvalidInterface("response wasn't utf-8".to_string()))
237  }
238
239  async fn rpc_call_internal<Response: DeserializeOwned>(
240    &self,
241    route: &str,
242    params: Option<String>,
243    response_size_limit: usize,
244  ) -> Result<Response, InterfaceError> {
245    let res = self.rpc_call_core(route, params, response_size_limit).await?;
246    serde_json::from_str(&res).map_err(|_| {
247      InterfaceError::InvalidInterface("response wasn't the expected json".to_string())
248    })
249  }
250
251  /// Perform a RPC call to the specified route with the provided parameters.
252  ///
253  /// This is NOT a JSON-RPC call. They use a route of "json_rpc" and are available via
254  /// `json_rpc_call`.
255  ///
256  /// The `response_size_limit` is expected to be in terms of the amount of bytes of data
257  /// communicated with the response. A scaling factor for the overhead of JSON, and a flat amount
258  /// for the overhead of HTTP, will be automatically applied.
259  ///
260  /// This method is NOT guaranteed by SemVer and may be removed in a future release. No guarantees
261  /// on the safety nor correctness of bespoke calls made with this function are guaranteed.
262  #[doc(hidden)]
263  pub async fn rpc_call(
264    &self,
265    route: &str,
266    params: Option<String>,
267    response_size_limit: usize,
268  ) -> Result<String, InterfaceError> {
269    Ok(
270      self
271        .rpc_call_internal::<serde_json::Value>(route, params, response_size_limit)
272        .await?
273        .to_string(),
274    )
275  }
276
277  async fn json_rpc_call_internal<Response: DeserializeOwned>(
278    &self,
279    method: &str,
280    params: Option<String>,
281    response_size_limit: usize,
282  ) -> Result<Response, InterfaceError> {
283    let req = if let Some(params) = params {
284      format!(r#"{{ "jsonrpc": "2.0", "method": "{method}", "params": {params}, "id": 0 }}"#)
285    } else {
286      format!(r#"{{ "jsonrpc": "2.0", "method": "{method}", "params": [], "id": 0 }}"#)
287    };
288
289    Ok(
290      self
291        .rpc_call_internal::<JsonRpcResponse<Response>>("json_rpc", Some(req), response_size_limit)
292        .await?
293        .result,
294    )
295  }
296
297  /// Perform a JSON-RPC call with the specified method with the provided parameters.
298  ///
299  /// The `response_size_limit` is expected to be in terms of the amount of bytes of data
300  /// communicated with the response. A scaling factor for the overhead of JSON, and a flat amount
301  /// for the overhead of HTTP, will be automatically applied.
302  ///
303  /// This method is NOT guaranteed by SemVer and may be removed in a future release. No guarantees
304  /// on the safety nor correctness of bespoke calls made with this function are guaranteed.
305  #[doc(hidden)]
306  pub async fn json_rpc_call(
307    &self,
308    method: &str,
309    params: Option<String>,
310    response_size_limit: usize,
311  ) -> Result<String, InterfaceError> {
312    // Untyped response
313    let result: Value = self.json_rpc_call_internal(method, params, response_size_limit).await?;
314    // Return the response as a string
315    Ok(result.to_string())
316  }
317
318  /// Generate blocks, with the specified address receiving the block reward.
319  ///
320  /// Returns the hashes of the generated blocks and the last block's alleged number.
321  ///
322  /// This is intended for testing purposes and does not validate the result in any way.
323  pub async fn generate_blocks<const ADDR_BYTES: u128>(
324    &self,
325    address: &Address<ADDR_BYTES>,
326    block_count: usize,
327  ) -> Result<(Vec<[u8; 32]>, usize), InterfaceError> {
328    #[derive(Deserialize)]
329    struct BlocksResponse {
330      blocks: Vec<String>,
331      height: usize,
332    }
333
334    let res = self
335      .json_rpc_call_internal::<BlocksResponse>(
336        "generateblocks",
337        Some(format!(r#"{{ "wallet_address": "{address}", "amount_of_blocks": {block_count} }}"#)),
338        block_count.saturating_mul(32),
339      )
340      .await?;
341
342    let mut blocks = Vec::with_capacity(res.blocks.len());
343    for block in res.blocks {
344      blocks.push(hash_hex(&block)?);
345    }
346    Ok((blocks, res.height))
347  }
348}
349
350impl<T: HttpTransport> ProvidesBlockchainMeta for MoneroDaemon<T> {
351  fn latest_block_number(&self) -> impl Send + Future<Output = Result<usize, InterfaceError>> {
352    async move {
353      #[derive(Deserialize)]
354      struct HeightResponse {
355        height: usize,
356      }
357      let res = self.rpc_call_internal::<HeightResponse>("get_height", None, 0).await?.height;
358      res.checked_sub(1).ok_or_else(|| {
359        InterfaceError::InvalidInterface(
360          "node claimed the blockchain didn't even have the genesis block".to_string(),
361        )
362      })
363    }
364  }
365}
366
367mod provides_transaction {
368  use super::*;
369
370  /*
371    Monero errors if more than 100 is requested unless using a non-restricted RPC.
372
373    https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
374      /src/rpc/core_rpc_server.cpp#L75
375  */
376  const EXPLICIT_TRANSACTIONS_PER_REQUEST_LIMIT: usize = 100;
377  const IMPLICIT_TRANSACTIONS_PER_REQUEST_LIMIT: usize =
378    REQUEST_SIZE_TARGET / (JSON_BYTE_OVERHEAD_FACTOR_ESTIMATE.saturating_mul(32));
379  const TRANSACTIONS_PER_REQUEST_LIMIT: usize = monero_oxide::primitives::const_min!(
380    EXPLICIT_TRANSACTIONS_PER_REQUEST_LIMIT,
381    IMPLICIT_TRANSACTIONS_PER_REQUEST_LIMIT
382  );
383
384  // And of course, the response limit also applies here
385  const TRANSACTIONS_PER_RESPONSE_LIMIT: usize =
386    (MAX_RESPONSE_SIZE - HTTP_OVERHEAD_ESTIMATE).div_ceil(TRANSACTION_SIZE_BOUND);
387
388  const TRANSACTIONS_LIMIT: usize = monero_oxide::primitives::const_min!(
389    TRANSACTIONS_PER_REQUEST_LIMIT,
390    TRANSACTIONS_PER_RESPONSE_LIMIT
391  );
392
393  #[derive(Deserialize)]
394  struct TransactionResponse {
395    tx_hash: String,
396    as_hex: String,
397    pruned_as_hex: String,
398    prunable_hash: String,
399  }
400  #[derive(Deserialize)]
401  struct TransactionsResponse {
402    #[serde(default)]
403    missed_tx: Vec<String>,
404    txs: Vec<TransactionResponse>,
405  }
406
407  #[rustfmt::skip]
408  impl<T: HttpTransport> ProvidesUnvalidatedTransactions for MoneroDaemon<T> {
409    fn transactions(
410      &self,
411      hashes: &[[u8; 32]],
412    ) -> impl Send + Future<Output = Result<Vec<Transaction>, TransactionsError>> {
413      async move {
414        let mut hashes_hex = hashes.iter().map(hex::encode).collect::<Vec<_>>();
415        let mut all_txs = Vec::with_capacity(hashes.len());
416        while !hashes_hex.is_empty() {
417          let this_count = TRANSACTIONS_LIMIT.min(hashes_hex.len());
418
419          let txs = "\"".to_string() + &hashes_hex.drain(.. this_count).collect::<Vec<_>>().join("\",\"") + "\"";
420          let txs: TransactionsResponse = self
421            .rpc_call_internal(
422              "get_transactions",
423              Some(format!(r#"{{ "txs_hashes": [{txs}] }}"#)),
424              this_count.saturating_mul(TRANSACTION_SIZE_BOUND),
425            )
426            .await?;
427
428          if !txs.missed_tx.is_empty() {
429            Err(TransactionsError::TransactionNotFound)?;
430          }
431          if txs.txs.len() != this_count {
432            Err(InterfaceError::InvalidInterface(
433              "not missing any transactions yet didn't return all transactions".to_string(),
434            ))?;
435          }
436
437          all_txs.extend(txs.txs);
438        }
439
440        all_txs
441          .iter()
442          .map(|res| {
443            // https://github.com/monero-project/monero/issues/8311
444            let buf =
445              rpc_hex(if !res.as_hex.is_empty() { &res.as_hex } else { &res.pruned_as_hex })?;
446            let mut buf = buf.as_slice();
447            let tx = Transaction::read(&mut buf).map_err(|_| {
448              InterfaceError::InvalidInterface(format!(
449                "node yielded transaction allegedly with hash {:?} which was invalid",
450                rpc_hex(&res.tx_hash).ok().map(hex::encode),
451              ))
452            })?;
453            if !buf.is_empty() {
454              Err(InterfaceError::InvalidInterface("transaction had extra bytes after it".to_string()))?;
455            }
456
457            // We check this to ensure we didn't read a pruned transaction when we meant to read an
458            // actual transaction. That shouldn't be possible, as they have different
459            // serializations, yet it helps to ensure that if we applied the above exception (using
460            //  the pruned data), it was for the right reason
461            if res.as_hex.is_empty() {
462              match tx.prefix().inputs.first() {
463                Some(Input::Gen { .. }) => (),
464                _ => Err(TransactionsError::PrunedTransaction)?,
465              }
466            }
467
468            Ok(tx)
469          })
470          .collect()
471      }
472    }
473
474    fn pruned_transactions(
475      &self,
476      hashes: &[[u8; 32]],
477    ) -> impl Send + Future<Output = Result<Vec<PrunedTransactionWithPrunableHash>, TransactionsError>>
478    {
479      async move {
480        let mut hashes_hex = hashes.iter().map(hex::encode).collect::<Vec<_>>();
481        let mut all_txs = Vec::with_capacity(hashes.len());
482        while !hashes_hex.is_empty() {
483          let this_count = TRANSACTIONS_LIMIT.min(hashes_hex.len());
484
485          let txs = "\"".to_string() + &hashes_hex.drain(.. this_count).collect::<Vec<_>>().join("\",\"") + "\"";
486          let txs: TransactionsResponse = self
487            .rpc_call_internal(
488              "get_transactions",
489              Some(format!(r#"{{ "txs_hashes": [{txs}], "prune": true }}"#)),
490              this_count.saturating_mul(TRANSACTION_SIZE_BOUND),
491            )
492            .await?;
493
494          if !txs.missed_tx.is_empty() {
495            Err(TransactionsError::TransactionNotFound)?;
496          }
497          if txs.txs.len() != this_count {
498            Err(InterfaceError::InvalidInterface(
499              "not missing any transactions yet didn't return all pruned transactions".to_string(),
500            ))?;
501          }
502
503          all_txs.extend(txs.txs);
504        }
505
506        all_txs
507          .iter()
508          .map(|res| {
509            let buf = rpc_hex(&res.pruned_as_hex)?;
510            let mut buf = buf.as_slice();
511            let tx = Transaction::<Pruned>::read(&mut buf).map_err(|_| {
512              InterfaceError::InvalidInterface(
513                format!("node yielded transaction allegedly with hash {:?} which was invalid",
514                rpc_hex(&res.tx_hash).ok().map(hex::encode),
515            ))
516            })?;
517            if !buf.is_empty() {
518              Err(InterfaceError::InvalidInterface(
519                "pruned transaction had extra bytes after it".to_string(),
520              ))?;
521            }
522            let prunable_hash = (!matches!(tx, Transaction::V1 { .. }))
523              .then(|| hash_hex(&res.prunable_hash))
524              .transpose()?;
525            Ok(
526              PrunedTransactionWithPrunableHash::new(tx, prunable_hash)
527                .expect(
528                  "couldn't create `PrunedTransactionWithPrunableHash` despite providing prunable hash if version != 1"
529                )
530            )
531          })
532          .collect()
533      }
534    }
535  }
536}
537
538impl<T: HttpTransport> PublishTransaction for MoneroDaemon<T> {
539  fn publish_transaction(
540    &self,
541    tx: &Transaction,
542  ) -> impl Send + Future<Output = Result<(), PublishTransactionError>> {
543    async move {
544      #[allow(dead_code)]
545      #[derive(Deserialize)]
546      struct SendRawResponse {
547        status: String,
548        double_spend: bool,
549        fee_too_low: bool,
550        invalid_input: bool,
551        invalid_output: bool,
552        low_mixin: bool,
553        not_relayed: bool,
554        overspend: bool,
555        too_big: bool,
556        too_few_outputs: bool,
557        reason: String,
558      }
559
560      let res: SendRawResponse = self
561        .rpc_call_internal(
562          "send_raw_transaction",
563          Some(format!(
564            r#"{{ "tx_as_hex": "{}", "do_sanity_checks": false }}"#,
565            hex::encode(tx.serialize())
566          )),
567          0,
568        )
569        .await?;
570
571      if res.status != "OK" {
572        Err(PublishTransactionError::TransactionRejected(res.reason))?;
573      }
574
575      Ok(())
576    }
577  }
578}
579
580mod provides_fee_rates {
581  use super::*;
582
583  // Number of blocks the fee estimate will be valid for
584  // https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c
585  //   /src/wallet/wallet2.cpp#L121
586  const GRACE_BLOCKS_FOR_FEE_ESTIMATE: u64 = 10;
587
588  impl<T: HttpTransport> ProvidesUnvalidatedFeeRates for MoneroDaemon<T> {
589    fn fee_rate(
590      &self,
591      priority: FeePriority,
592    ) -> impl Send + Future<Output = Result<FeeRate, FeeError>> {
593      async move {
594        #[derive(Deserialize)]
595        struct FeeResponse {
596          status: String,
597          fees: Option<Vec<u64>>,
598          fee: u64,
599          quantization_mask: u64,
600        }
601
602        let res: FeeResponse = self
603          .json_rpc_call_internal(
604            "get_fee_estimate",
605            Some(format!(r#"{{ "grace_blocks": {GRACE_BLOCKS_FOR_FEE_ESTIMATE} }}"#)),
606            0,
607          )
608          .await?;
609
610        if res.status != "OK" {
611          Err(FeeError::InvalidFee)?;
612        }
613
614        if let Some(fees) = res.fees {
615          // https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
616          // src/wallet/wallet2.cpp#L7615-L7620
617          let priority_idx = usize::try_from(if priority.to_u32() >= 4 {
618            3
619          } else {
620            priority.to_u32().saturating_sub(1)
621          })
622          .map_err(|_| FeeError::InvalidFeePriority)?;
623
624          if priority_idx >= fees.len() {
625            Err(FeeError::InvalidFeePriority)?
626          } else {
627            FeeRate::new(fees[priority_idx], res.quantization_mask).ok_or(FeeError::InvalidFee)
628          }
629        } else {
630          // https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
631          //   src/wallet/wallet2.cpp#L7569-L7584
632          // https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
633          //   src/wallet/wallet2.cpp#L7660-L7661
634          let priority_idx =
635            usize::try_from(if priority.to_u32() == 0 { 1 } else { priority.to_u32() - 1 })
636              .map_err(|_| FeeError::InvalidFeePriority)?;
637          const MULTIPLIERS: [u64; 4] = [1, 5, 25, 1000];
638          let fee_multiplier =
639            *MULTIPLIERS.get(priority_idx).ok_or(FeeError::InvalidFeePriority)?;
640
641          FeeRate::new(
642            res.fee.checked_mul(fee_multiplier).ok_or(FeeError::InvalidFee)?,
643            res.quantization_mask,
644          )
645          .ok_or(FeeError::InvalidFee)
646        }
647      }
648    }
649  }
650}
651
652/// A prelude of recommended imports to glob import.
653pub mod prelude {
654  pub use monero_interface::prelude::*;
655  pub use crate::{HttpTransport, MoneroDaemon};
656}