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
26const MAX_REQUEST_SIZE: usize = 1024 * 1024;
33
34const MAX_RESPONSE_SIZE: usize = 100 * 1024 * 1024;
58
59const 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;
63const MIN_RESPONSE_SIZE_IN_BYTES_ESTIMATE: usize = 1024;
65
66const MINER_TRANSACTION_OUTPUT_BOUND: usize = 10_000;
78const 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]
104pub trait HttpTransport: Sync + Clone {
113 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#[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 pub async fn new(transport: T) -> Result<Self, InterfaceError> {
143 let mut result =
150 Self { transport, response_size_limits: true, supports_json_rpc_batch_requests: true };
151
152 {
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 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 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 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 #[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 #[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 let result: Value = self.json_rpc_call_internal(method, params, response_size_limit).await?;
314 Ok(result.to_string())
316 }
317
318 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 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 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 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 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 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 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 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
652pub mod prelude {
654 pub use monero_interface::prelude::*;
655 pub use crate::{HttpTransport, MoneroDaemon};
656}