rpc.rs (21898B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2022-2025 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 //! This is a very simple RPC client designed only for a specific bitcoind version 17 //! and to use on an secure localhost connection to a trusted node 18 //! 19 //! No http format or body length check as we trust the node output 20 //! No asynchronous request as bitcoind put requests in a queue and process 21 //! them synchronously and we do not want to fill this queue 22 //! 23 //! We only parse the thing we actually use, this reduce memory usage and 24 //! make our code more compatible with future deprecation 25 //! 26 //! bitcoincore RPC documentation: <https://bitcoincore.org/en/doc/29.0.0/> 27 28 use base64::Engine; 29 use base64::prelude::BASE64_STANDARD; 30 use bitcoin::{Address, Amount, BlockHash, SignedAmount, Txid, address::NetworkUnchecked}; 31 use serde_json::{Value, json}; 32 use std::{ 33 fmt::Debug, 34 io::{ErrorKind, IoSlice, Write as _}, 35 net::SocketAddr, 36 path::PathBuf, 37 str::FromStr as _, 38 time::Duration, 39 }; 40 use tokio::{ 41 io::{self, AsyncReadExt, AsyncWriteExt as _}, 42 net::TcpStream, 43 time::timeout, 44 }; 45 use tracing::trace; 46 47 use crate::config::{RpcAuth, RpcCfg, WalletCfg}; 48 49 /// Create a rpc connection with an unlocked wallet 50 pub async fn rpc_wallet(config: &RpcCfg, wallet: &WalletCfg) -> Result<Rpc> { 51 let mut rpc = Rpc::wallet(config, &wallet.name).await?; 52 rpc.load_wallet(&wallet.name).await?; 53 if let Some(password) = &wallet.password { 54 rpc.unlock_wallet(password).await?; 55 } 56 Ok(rpc) 57 } 58 59 /// Create a rpc connection 60 pub async fn rpc_common(config: &RpcCfg) -> Result<Rpc> { 61 Ok(Rpc::common(config).await?) 62 } 63 64 #[derive(Debug, serde::Serialize)] 65 struct RpcRequest<'a, T: serde::Serialize> { 66 method: &'a str, 67 id: u64, 68 params: &'a T, 69 } 70 71 #[derive(Debug, serde::Deserialize)] 72 #[serde(untagged)] 73 enum RpcResponse<T> { 74 RpcResponse { 75 result: Option<T>, 76 error: Option<RpcError>, 77 id: u64, 78 }, 79 Error(String), 80 } 81 82 #[derive(Debug, serde::Deserialize)] 83 struct RpcError { 84 code: ErrorCode, 85 message: String, 86 } 87 88 #[derive(Debug, thiserror::Error)] 89 pub enum Error { 90 #[error("IO: {0:?}")] 91 Transport(#[from] std::io::Error), 92 #[error("RPC: {code:?} - {msg}")] 93 RPC { code: ErrorCode, msg: String }, 94 #[error("BTC: {0}")] 95 Bitcoin(String), 96 #[error("JSON: {0}")] 97 Json(#[from] serde_json::Error), 98 #[error("connect: {0}")] 99 Connect(#[from] RpcConnectErr), 100 #[error("Null rpc, no result or error")] 101 Null, 102 } 103 104 pub type Result<T> = std::result::Result<T, Error>; 105 106 const EMPTY: [(); 0] = []; 107 108 fn expect_null(result: Result<()>) -> Result<()> { 109 match result { 110 Err(Error::Null) => Ok(()), 111 i => i, 112 } 113 } 114 115 pub struct JsonSocket { 116 path: String, 117 cookie: String, 118 sock: TcpStream, 119 buf: Vec<u8>, 120 } 121 122 impl JsonSocket { 123 async fn call<T>(&mut self, body: &impl serde::Serialize) -> Result<T> 124 where 125 T: serde::de::DeserializeOwned, 126 { 127 let buf = &mut self.buf; 128 let sock = &mut self.sock; 129 buf.clear(); 130 serde_json::to_writer(&mut *buf, body)?; 131 let body_len = buf.len(); 132 133 // Write HTTP request 134 writeln!(buf, "POST {} HTTP/1.1\r", self.path)?; 135 // Write headers 136 writeln!(buf, "Accept: application/json-rpc\r")?; 137 writeln!(buf, "Authorization: {}\r", self.cookie)?; 138 writeln!(buf, "Content-Type: application/json-rpc\r")?; 139 writeln!(buf, "Content-Length: {body_len}\r")?; 140 // Write separator 141 writeln!(buf, "\r")?; 142 let (body, head) = buf.split_at(body_len); 143 let mut vectors = [IoSlice::new(head), IoSlice::new(body)]; 144 let mut vectors = vectors.as_mut_slice(); 145 while !vectors.is_empty() { 146 let written = sock.write_vectored(vectors).await?; 147 IoSlice::advance_slices(&mut vectors, written); 148 } 149 sock.flush().await?; 150 151 // Skip response 152 153 buf.clear(); 154 let header_pos = loop { 155 let amount = sock.read_buf(buf).await?; 156 if amount == 0 { 157 return Err(Error::Transport(io::Error::new( 158 io::ErrorKind::UnexpectedEof, 159 "End of file reached unexpectedly", 160 ))); 161 } 162 if let Some(header_pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") 163 && buf.ends_with(b"\n") 164 { 165 break header_pos; 166 } 167 }; 168 // Read body 169 let response = serde_json::from_slice(&buf[header_pos + 4..])?; 170 Ok(response) 171 } 172 } 173 174 #[derive(Debug, thiserror::Error)] 175 pub enum RpcConnectErr { 176 #[error("failed to read cookie file at '{0}': {1}")] 177 Cookie(String, ErrorKind), 178 #[error("failed to connect {0}: {1}")] 179 Tcp(SocketAddr, ErrorKind), 180 #[error("failed to connect {0}: {1}")] 181 Elapsed(SocketAddr, tokio::time::error::Elapsed), 182 } 183 184 /// Bitcoin RPC connection 185 pub struct Rpc { 186 socket: JsonSocket, 187 id: u64, 188 } 189 190 impl Rpc { 191 /// Start a RPC connection 192 pub async fn common(cfg: &RpcCfg) -> std::result::Result<Self, RpcConnectErr> { 193 Self::new(cfg, None).await 194 } 195 196 /// Start a wallet RPC connection 197 pub async fn wallet(cfg: &RpcCfg, wallet: &str) -> std::result::Result<Self, RpcConnectErr> { 198 Self::new(cfg, Some(wallet)).await 199 } 200 201 async fn new(cfg: &RpcCfg, wallet: Option<&str>) -> std::result::Result<Self, RpcConnectErr> { 202 let path = if let Some(wallet) = wallet { 203 format!("/wallet/{wallet}") 204 } else { 205 String::from("/") 206 }; 207 208 let token = match &cfg.auth { 209 RpcAuth::Basic(s) => s.as_bytes().to_vec(), 210 RpcAuth::Cookie(path) => match std::fs::read(path) { 211 Ok(content) => content, 212 Err(e) if e.kind() == ErrorKind::IsADirectory => { 213 let path = PathBuf::from_str(path).unwrap().join(".cookie"); 214 std::fs::read(&path).map_err(|e| { 215 RpcConnectErr::Cookie(path.to_string_lossy().to_string(), e.kind()) 216 })? 217 } 218 Err(e) => return Err(RpcConnectErr::Cookie(path.clone(), e.kind())), 219 }, 220 }; 221 // Open connection 222 let sock = timeout(Duration::from_secs(5), TcpStream::connect(&cfg.addr)) 223 .await 224 .map_err(|e| RpcConnectErr::Elapsed(cfg.addr, e))? 225 .map_err(|e| RpcConnectErr::Tcp(cfg.addr, e.kind()))?; 226 227 sock.set_nodelay(true).ok(); 228 229 Ok(Self { 230 id: 0, 231 socket: JsonSocket { 232 path, 233 cookie: format!("Basic {}", BASE64_STANDARD.encode(&token)), 234 sock, 235 buf: Vec::with_capacity(16 * 1024), 236 }, 237 }) 238 } 239 240 async fn call<T>(&mut self, method: &str, params: &(impl serde::Serialize + Debug)) -> Result<T> 241 where 242 T: serde::de::DeserializeOwned + Debug, 243 { 244 trace!("RPC > {method} {params:?}"); 245 let request = RpcRequest { 246 method, 247 id: self.id, 248 params, 249 }; 250 let response: RpcResponse<T> = self.socket.call(&request).await?; 251 trace!("RPC < {response:?}"); 252 match response { 253 RpcResponse::RpcResponse { result, error, id } => { 254 assert_eq!(self.id, id); 255 self.id += 1; 256 if let Some(ok) = result { 257 Ok(ok) 258 } else { 259 Err(match error { 260 Some(err) => Error::RPC { 261 code: err.code, 262 msg: err.message, 263 }, 264 None => Error::Null, 265 }) 266 } 267 } 268 RpcResponse::Error(msg) => Err(Error::Bitcoin(msg)), 269 } 270 } 271 272 /* ----- Wallet management ----- */ 273 274 /// Create encrypted native bitcoin wallet 275 pub async fn create_wallet(&mut self, name: &str, passwd: &str) -> Result<Wallet> { 276 self.call("createwallet", &(name, (), (), passwd, (), true)) 277 .await 278 } 279 280 /// Load existing wallet 281 pub async fn load_wallet(&mut self, name: &str) -> Result<Wallet> { 282 match self.call("loadwallet", &[name]).await { 283 Err(Error::RPC { 284 code: ErrorCode::RpcWalletAlreadyLoaded, 285 .. 286 }) => Ok(Wallet { 287 name: name.to_string(), 288 }), 289 it => it, 290 } 291 } 292 293 /// Unlock loaded wallet 294 pub async fn unlock_wallet(&mut self, passwd: &str) -> Result<()> { 295 expect_null(self.call("walletpassphrase", &(passwd, 100000000)).await) 296 } 297 298 /* ----- Wallet utils ----- */ 299 300 /// Generate a new address for the current wallet 301 pub async fn gen_addr(&mut self) -> Result<Address> { 302 Ok(self 303 .call::<Address<NetworkUnchecked>>("getnewaddress", &EMPTY) 304 .await? 305 .assume_checked()) 306 } 307 308 /// Get current balance amount 309 pub async fn get_balance(&mut self) -> Result<Amount> { 310 let btc: f64 = self.call("getbalance", &EMPTY).await?; 311 Ok(Amount::from_btc(btc).unwrap()) 312 } 313 314 /// Get current balance amount 315 pub async fn addr_info(&mut self, addr: &Address) -> Result<AddressInfo> { 316 self.call("getaddressinfo", &[addr]).await 317 } 318 319 /* ----- Mining ----- */ 320 321 /// Mine a certain amount of block to profit a given address 322 pub async fn mine(&mut self, nb: u16, address: &Address) -> Result<Vec<BlockHash>> { 323 self.call("generatetoaddress", &(nb, address)).await 324 } 325 326 /* ----- Getter ----- */ 327 328 /// Get blockchain info 329 pub async fn get_blockchain_info(&mut self) -> Result<BlockchainInfo> { 330 self.call("getblockchaininfo", &EMPTY).await 331 } 332 333 /// Get blockchain info 334 pub async fn get_wallet_info(&mut self) -> Result<WalletInfo> { 335 self.call("getwalletinfo", &EMPTY).await 336 } 337 338 /// Get chain tips 339 pub async fn get_chain_tips(&mut self) -> Result<Vec<ChainTips>> { 340 self.call("getchaintips", &EMPTY).await 341 } 342 343 /// Get wallet transaction info from id 344 pub async fn get_tx(&mut self, id: &Txid) -> Result<Transaction> { 345 self.call("gettransaction", &(id, (), true)).await 346 } 347 348 /// Get transaction inputs and outputs 349 pub async fn get_input_output(&mut self, id: &Txid) -> Result<InputOutput> { 350 self.call("getrawtransaction", &(id, true)).await 351 } 352 353 /// Get genesis block hash 354 pub async fn get_genesis(&mut self) -> Result<BlockHash> { 355 self.call("getblockhash", &[0]).await 356 } 357 358 /* ----- Transactions ----- */ 359 360 /// Send bitcoin transaction 361 pub async fn send( 362 &mut self, 363 to: &Address, 364 amount: &Amount, 365 data: Option<&[u8]>, 366 subtract_fee: bool, 367 ) -> Result<Txid> { 368 self.send_custom([], [(to, amount)], data, subtract_fee) 369 .await 370 .map(|it| it.txid) 371 } 372 373 /// Send bitcoin transaction with multiple recipients 374 pub async fn send_many<'a>( 375 &mut self, 376 to: impl IntoIterator<Item = (&'a Address, &'a Amount)>, 377 ) -> Result<Txid> { 378 self.send_custom([], to, None, false) 379 .await 380 .map(|it| it.txid) 381 } 382 383 async fn send_custom<'a>( 384 &mut self, 385 from: impl IntoIterator<Item = &'a Txid>, 386 to: impl IntoIterator<Item = (&'a Address, &'a Amount)>, 387 data: Option<&[u8]>, 388 subtract_fee: bool, 389 ) -> Result<SendResult> { 390 // We use the experimental 'send' rpc command as it is the only capable to send metadata in a single rpc call 391 let inputs: Vec<_> = from 392 .into_iter() 393 .enumerate() 394 .map(|(i, id)| json!({"txid": id, "vout": i})) 395 .collect(); 396 let mut outputs: Vec<Value> = to 397 .into_iter() 398 .map(|(addr, amount)| json!({&addr.to_string(): amount.to_btc()})) 399 .collect(); 400 let nb_outputs = outputs.len(); 401 if let Some(data) = data { 402 assert!(!data.is_empty(), "No medatata"); 403 assert!(data.len() <= 80, "Max 80 bytes"); 404 outputs.push(json!({ "data".to_string(): hex::encode(data) })); 405 } 406 self.call( 407 "send", 408 &( 409 outputs, 410 (), 411 (), 412 (), 413 SendOption { 414 add_inputs: true, 415 inputs, 416 subtract_fee_from_outputs: if subtract_fee { 417 (0..nb_outputs).collect() 418 } else { 419 vec![] 420 }, 421 replaceable: true, 422 }, 423 ), 424 ) 425 .await 426 } 427 428 /// Bump transaction fees of a wallet debit 429 pub async fn bump_fee(&mut self, id: &Txid) -> Result<BumpResult> { 430 self.call("bumpfee", &[id]).await 431 } 432 433 /// Abandon a pending transaction 434 pub async fn abandon_tx(&mut self, id: &Txid) -> Result<()> { 435 expect_null(self.call("abandontransaction", &[&id]).await) 436 } 437 438 /* ----- Watcher ----- */ 439 440 /// Block until a new block is mined 441 pub async fn wait_for_new_block(&mut self) -> Result<Nothing> { 442 self.call("waitfornewblock", &[0]).await 443 } 444 445 /// List new and removed transaction since a block 446 pub async fn list_since_block( 447 &mut self, 448 hash: Option<&BlockHash>, 449 confirmation: u32, 450 ) -> Result<ListSinceBlock> { 451 self.call("listsinceblock", &(hash, confirmation.max(1), (), true)) 452 .await 453 } 454 455 /* ----- Cluster ----- */ 456 457 /// Try a connection to a node once 458 pub async fn add_node(&mut self, addr: &str) -> Result<()> { 459 expect_null(self.call("addnode", &(addr, "onetry")).await) 460 } 461 462 /// Immediately disconnects from the specified peer node. 463 pub async fn disconnect_node(&mut self, addr: &str) -> Result<()> { 464 expect_null(self.call("disconnectnode", &(addr, ())).await) 465 } 466 467 /* ----- Control ------ */ 468 469 /// Request a graceful shutdown 470 pub async fn stop(&mut self) -> Result<String> { 471 self.call("stop", &()).await 472 } 473 } 474 475 #[derive(Debug, serde::Deserialize)] 476 pub struct Wallet { 477 pub name: String, 478 } 479 480 #[derive(Clone, Debug, serde::Deserialize)] 481 pub struct BlockchainInfo { 482 pub chain: String, 483 #[serde(rename = "verificationprogress")] 484 pub verification_progress: f32, 485 #[serde(rename = "initialblockdownload")] 486 pub initial_block_download: bool, 487 pub blocks: u64, 488 #[serde(rename = "bestblockhash")] 489 pub best_block_hash: BlockHash, 490 } 491 492 #[derive(Clone, Debug, serde::Deserialize)] 493 pub struct WalletInfo { 494 pub walletname: String, 495 pub scanning: Option<Scanning>, 496 } 497 498 #[derive(Clone, Debug, serde::Deserialize)] 499 pub struct Scanning { 500 pub duration: u64, 501 pub progress: f32, 502 } 503 504 #[derive(Debug, serde::Deserialize)] 505 pub struct BumpResult { 506 pub txid: Txid, 507 } 508 509 #[derive(Debug, serde::Serialize)] 510 pub struct SendOption { 511 pub add_inputs: bool, 512 pub inputs: Vec<Value>, 513 pub subtract_fee_from_outputs: Vec<usize>, 514 pub replaceable: bool, 515 } 516 517 #[derive(Debug, serde::Deserialize)] 518 pub struct SendResult { 519 pub txid: Txid, 520 } 521 522 /// Enum to represent the category of a transaction. 523 #[derive(Copy, PartialEq, Eq, Clone, Debug, serde::Deserialize)] 524 #[serde(rename_all = "lowercase")] 525 pub enum Category { 526 Send, 527 Receive, 528 Generate, 529 Immature, 530 Orphan, 531 } 532 533 #[derive(Debug, serde::Deserialize)] 534 pub struct TransactionDetail { 535 pub address: Option<Address<NetworkUnchecked>>, 536 pub category: Category, 537 #[serde(with = "bitcoin::amount::serde::as_btc")] 538 pub amount: SignedAmount, 539 #[serde(default, with = "bitcoin::amount::serde::as_btc::opt")] 540 pub fee: Option<SignedAmount>, 541 /// Only for send transaction 542 pub abandoned: Option<bool>, 543 } 544 545 #[derive(Debug, serde::Deserialize)] 546 pub struct ListTransaction { 547 pub confirmations: i32, 548 pub txid: Txid, 549 pub category: Category, 550 pub vout: i32, 551 } 552 553 #[derive(Debug, serde::Deserialize)] 554 pub struct ListSinceBlock { 555 pub transactions: Vec<ListTransaction>, 556 #[serde(default)] 557 pub removed: Vec<ListTransaction>, 558 pub lastblock: BlockHash, 559 } 560 561 #[derive(Debug, serde::Deserialize)] 562 pub struct VoutScriptPubKey { 563 pub asm: String, 564 // nulldata do not have an address 565 pub address: Option<Address<NetworkUnchecked>>, 566 } 567 568 #[derive(Debug, serde::Deserialize)] 569 #[serde(rename_all = "camelCase")] 570 pub struct Vout { 571 #[serde(with = "bitcoin::amount::serde::as_btc")] 572 pub value: Amount, 573 pub n: u32, 574 pub script_pub_key: VoutScriptPubKey, 575 } 576 577 #[derive(Debug, serde::Deserialize)] 578 pub struct Vin { 579 /// Not provided for coinbase txs. 580 pub txid: Option<Txid>, 581 /// Not provided for coinbase txs. 582 pub vout: Option<u32>, 583 } 584 585 #[derive(Debug, serde::Deserialize)] 586 pub struct InputOutput { 587 pub vin: Vec<Vin>, 588 pub vout: Vec<Vout>, 589 } 590 591 #[derive(Debug, serde::Deserialize)] 592 pub struct Transaction { 593 pub confirmations: i32, 594 pub time: u64, 595 #[serde(with = "bitcoin::amount::serde::as_btc")] 596 pub amount: SignedAmount, 597 #[serde(default, with = "bitcoin::amount::serde::as_btc::opt")] 598 pub fee: Option<SignedAmount>, 599 pub replaces_txid: Option<Txid>, 600 pub replaced_by_txid: Option<Txid>, 601 pub details: Vec<TransactionDetail>, 602 pub decoded: InputOutput, 603 } 604 605 #[derive(Clone, PartialEq, Eq, serde::Deserialize, Debug)] 606 pub struct ChainTips { 607 #[serde(rename = "branchlen")] 608 pub length: usize, 609 pub status: ChainTipsStatus, 610 } 611 612 #[derive(Copy, serde::Deserialize, Clone, PartialEq, Eq, Debug)] 613 #[serde(rename_all = "lowercase")] 614 pub enum ChainTipsStatus { 615 Invalid, 616 #[serde(rename = "headers-only")] 617 HeadersOnly, 618 #[serde(rename = "valid-headers")] 619 ValidHeaders, 620 #[serde(rename = "valid-fork")] 621 ValidFork, 622 Active, 623 } 624 625 #[derive(Debug, serde::Deserialize)] 626 pub struct AddressInfo { 627 pub ismine: bool, 628 pub iswatchonly: bool, 629 pub solvable: bool, 630 } 631 632 #[derive(Debug, serde::Deserialize)] 633 pub struct Nothing {} 634 635 /// Bitcoin RPC error codes <https://github.com/bitcoin/bitcoin/blob/master/src/rpc/protocol.h> 636 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde_repr::Deserialize_repr)] 637 #[repr(i32)] 638 pub enum ErrorCode { 639 RpcInvalidRequest = -32600, 640 RpcMethodNotFound = -32601, 641 RpcInvalidParams = -32602, 642 RpcInternalError = -32603, 643 RpcParseError = -32700, 644 645 /// std::exception thrown in command handling 646 RpcMiscError = -1, 647 /// Unexpected type was passed as parameter 648 RpcTypeError = -3, 649 /// Invalid address or key 650 RpcInvalidAddressOrKey = -5, 651 /// Ran out of memory during operation 652 RpcOutOfMemory = -7, 653 /// Invalid, missing or duplicate parameter 654 RpcInvalidParameter = -8, 655 /// Database error 656 RpcDatabaseError = -20, 657 /// Error parsing or validating structure in raw format 658 RpcDeserializationError = -22, 659 /// General error during transaction or block submission 660 RpcVerifyError = -25, 661 /// Transaction or block was rejected by network rules 662 RpcVerifyRejected = -26, 663 /// Transaction already in chain 664 RpcVerifyAlreadyInChain = -27, 665 /// Client still warming up 666 RpcInWarmup = -28, 667 /// RPC method is deprecated 668 RpcMethodDeprecated = -32, 669 /// Bitcoin is not connected 670 RpcClientNotConnected = -9, 671 /// Still downloading initial blocks 672 RpcClientInInitialDownload = -10, 673 /// Node is already added 674 RpcClientNodeAlreadyAdded = -23, 675 /// Node has not been added before 676 RpcClientNodeNotAdded = -24, 677 /// Node to disconnect not found in connected nodes 678 RpcClientNodeNotConnected = -29, 679 /// Invalid IP/Subnet 680 RpcClientInvalidIpOrSubnet = -30, 681 /// No valid connection manager instance found 682 RpcClientP2pDisabled = -31, 683 /// Max number of outbound or block-relay connections already open 684 RpcClientNodeCapacityReached = -34, 685 /// No mempool instance found 686 RpcClientMempoolDisabled = -33, 687 /// Unspecified problem with wallet (key not found etc.) 688 RpcWalletError = -4, 689 /// Not enough funds in wallet or account 690 RpcWalletInsufficientFunds = -6, 691 /// Invalid label name 692 RpcWalletInvalidLabelName = -11, 693 /// Keypool ran out, call keypoolrefill first 694 RpcWalletKeypoolRanOut = -12, 695 /// Enter the wallet passphrase with walletpassphrase first 696 RpcWalletUnlockNeeded = -13, 697 /// The wallet passphrase entered was incorrect 698 RpcWalletPassphraseIncorrect = -14, 699 /// Command given in wrong wallet encryption state (encrypting an encrypted wallet etc.) 700 RpcWalletWrongEncState = -15, 701 /// Failed to encrypt the wallet 702 RpcWalletEncryptionFailed = -16, 703 /// Wallet is already unlocked 704 RpcWalletAlreadyUnlocked = -17, 705 /// Invalid wallet specified 706 RpcWalletNotFound = -18, 707 /// No wallet specified (error when there are multiple wallets loaded) 708 RpcWalletNotSpecified = -19, 709 /// This same wallet is already loaded 710 RpcWalletAlreadyLoaded = -35, 711 /// Server is in safe mode, and command is not allowed in safe mode 712 RpcForbiddenBySafeMode = -2, 713 }