/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2026 Dyne.org foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use std::{ collections::BTreeMap, io::Cursor, sync::{Arc, OnceLock, Weak}, }; use parking_lot::Mutex as SyncMutex; use smol::lock::RwLock; use url::Url; use darkfi::{ blockchain::BlockInfo, rpc::{ client::RpcClient, jsonrpc::{ErrorCode, JsonError, JsonRequest, JsonResult}, util::JsonValue, }, system::{sleep, Publisher, StoppableTask, StoppableTaskPtr}, tx::Transaction, util::encoding::base64, Error as DarkFiError, Result as DarkFiResult, }; use darkfi_money_contract::{ model::TokenId, MONEY_CONTRACT_COIN_MERKLE_TREE, MONEY_CONTRACT_INFO_TREE, MONEY_CONTRACT_LATEST_COIN_ROOT, }; use darkfi_sdk::crypto::{ contract_id::MONEY_CONTRACT_ID, keypair::{Address, Network, PublicKey, StandardAddress}, MerkleNode, MerkleTree, }; use darkfi_serial::{deserialize, deserialize_async, serialize, Decodable, Encodable}; use drk::{money::KVDB_MERKLE_TREES_MONEY, rpc::DarkfidRpcClient, Drk}; use kvdb_overlay::Batch; use crate::{ error::{Error, Result}, prop::{PropertyEnum, Role}, scene::{MethodCallSub, Pimpl, SceneNodePtr, SceneNodeWeak}, ExecutorPtr, }; const DARKFID_RETRY_TIME: u64 = 20; const BLOCK_BATCHES_BUFFER: usize = 3; const NEW_WALLET_INIT_RETRY_TIME: u64 = 5; #[cfg(target_os = "android")] mod paths { use crate::android::{get_appdata_path, get_external_storage_path}; use std::path::PathBuf; pub fn get_cache_path() -> PathBuf { get_external_storage_path().join("drk/cache") } pub fn get_wallet_path() -> PathBuf { get_external_storage_path().join("drk/wallet.db") } pub fn get_use_tor_filename() -> PathBuf { get_external_storage_path().join("use_tor.txt") } } #[cfg(not(target_os = "android"))] mod paths { use std::path::PathBuf; pub fn get_cache_path() -> PathBuf { dirs::data_local_dir().unwrap().join("darkfi/app/drk/cache") } pub fn get_wallet_path() -> PathBuf { dirs::data_local_dir().unwrap().join("darkfi/app/drk/wallet.db") } pub fn get_use_tor_filename() -> PathBuf { dirs::data_local_dir().unwrap().join("darkfi/app/drk/use_tor.txt") } } use paths::*; macro_rules! t { ($($arg:tt)*) => { trace!(target: "plugin::drk", $($arg)*); } } macro_rules! d { ($($arg:tt)*) => { debug!(target: "plugin::drk", $($arg)*); } } macro_rules! i { ($($arg:tt)*) => { info!(target: "plugin::drk", $($arg)*); } } macro_rules! e { ($($arg:tt)*) => { error!(target: "plugin::drk", $($arg)*); } } #[derive(Debug, Clone)] struct TxState { id: Option, confirmed: bool, amount: Option, token_symbol: Option, recipient: Option
, } pub type DrkPluginPtr = Arc; #[derive(Debug, Clone)] struct BuildTxRequest { amount: String, token_id: TokenId, recipient: PublicKey, } pub struct DrkPlugin { node: SceneNodeWeak, sg_root: SceneNodePtr, tasks: OnceLock>>, net_transport: PropertyEnum, subscribe_task: SyncMutex>, drk: Arc>, build_tx_channel: smol::channel::Sender, last_balances: SyncMutex>>, ex: ExecutorPtr, } impl DrkPlugin { pub async fn new(node: SceneNodeWeak, sg_root: SceneNodePtr, ex: ExecutorPtr) -> Result { let setting_node = sg_root.lookup_node("/setting").unwrap(); let net_transport = PropertyEnum::wrap(&setting_node, Role::Internal, "net.transport", 0).unwrap(); let endpoint = Self::endpoint(&sg_root, &net_transport); i!("Using {endpoint} transport for darkfid connection"); let drk = match Drk::new( Network::Testnet, get_cache_path().to_string_lossy().to_string(), get_wallet_path().to_string_lossy().to_string(), "changeme".to_string(), Some(endpoint.clone()), &ex, false, ) .await { Ok(wallet) => wallet, Err(e) => { eprintln!("Error initializing wallet: {e}"); return Err(Error::ServiceFailed); // TODO: make a better error } }; if let Err(e) = drk.initialize_wallet().await { e!("Error initializing wallet: {e}"); } let mut output = vec![]; if let Err(e) = drk.initialize_money(&mut output).await { e!("Failed to initialize Money: {e}"); } if let Err(e) = drk.initialize_dao().await { e!("Failed to initialize DAO: {e}"); } if let Err(e) = drk.initialize_deployooor().await { e!("Failed to initialize Deployooor: {e}"); } // Generate a default address if needed match drk.default_address().await { Ok(_) => { i!("Default address already exists"); } Err(e) => { i!("No default address found ({}), generating one...", e); if let Err(e) = drk.money_keygen(&mut output).await { e!("Failed to generate keypair: {e}"); } else { i!("Generated default address"); match drk.addresses().await { Ok(addrs) => { if let Some((key_id, _, _, _)) = addrs.last() { i!("Setting address with key_id {} as default", key_id); if let Err(e) = drk.set_default_address(*key_id as u16).await { e!("Failed to set default address: {e}"); } } } Err(e) => { e!("Failed to get addresses: {e}"); } } } } } // Create channel for build_tx requests let (build_tx_tx, build_tx_rx) = smol::channel::unbounded(); let self_ = Arc::new(Self { node: node.clone(), sg_root, tasks: OnceLock::new(), drk: drk.into_ptr(), build_tx_channel: build_tx_tx, net_transport, subscribe_task: SyncMutex::new(None), last_balances: SyncMutex::new(None), ex: ex.clone(), }); // Start background task to process build_tx requests from channel let me3 = Arc::downgrade(&self_); let build_tx_processor = ex.spawn(async move { while let Ok(request) = build_tx_rx.recv().await { if let Some(self_) = me3.upgrade() { match self_.build_tx_request(request).await { Ok((tx, token_symbol, recipient, amount)) => { self_.emit_tx_built(amount, token_symbol, recipient, tx).await; } Err(e) => { e!("Failed to build transaction: {e}"); self_.emit_tx_built_error(e.to_string()).await; } } } } }); let node_ref = node.upgrade().unwrap(); let me2 = Arc::downgrade(&self_); let method_sub = node_ref.subscribe_method_call("get_default_address").unwrap(); let get_address_task = ex.spawn( async move { while Self::process_get_default_address(&me2, &method_sub).await {} }, ); let node_ref = node.upgrade().unwrap(); let me2 = Arc::downgrade(&self_); let method_sub_balances = node_ref.subscribe_method_call("get_balances").unwrap(); let get_balances_task = ex.spawn(async move { while Self::process_get_balances(&me2, &method_sub_balances).await {} }); let node_ref = node.upgrade().unwrap(); let me2 = Arc::downgrade(&self_); let method_sub_tx_status = node_ref.subscribe_method_call("get_tx_status").unwrap(); let get_tx_status_task = ex.spawn(async move { while Self::process_get_tx_status(&me2, &method_sub_tx_status).await {} }); let node_ref = node.upgrade().unwrap(); let me2 = Arc::downgrade(&self_); let method_sub_build_tx = node_ref.subscribe_method_call("build_tx").unwrap(); let build_tx_task = ex.spawn( async move { while Self::process_build_tx(&me2, &method_sub_build_tx).await {} }, ); let node_ref = node.upgrade().unwrap(); let me2 = Arc::downgrade(&self_); let method_sub_broadcast_tx = node_ref.subscribe_method_call("broadcast_tx").unwrap(); let broadcast_tx_task = ex.spawn(async move { while Self::process_broadcast_tx(&me2, &method_sub_broadcast_tx).await {} }); let tasks = vec![ get_address_task, get_balances_task, get_tx_status_task, build_tx_task, broadcast_tx_task, build_tx_processor, ]; self_.clone().start(tasks).await; Ok(Pimpl::Drk(self_)) } /// Endpoint for the darkfid daemon connection, derived from the /// `net.transport` and `wallet.*.rpc` settings fn endpoint(sg_root: &SceneNodePtr, net_transport: &PropertyEnum) -> Url { let setting_node = sg_root.lookup_node("/setting").unwrap(); let prop = match net_transport.get().as_str() { "tor" => "wallet.tor.rpc", "tcp" => "wallet.tcp.rpc", unhandled => panic!("Unhandled net.transport value: {unhandled}"), }; let endpoint = setting_node.get_property_str(prop).unwrap(); Url::parse(&endpoint).unwrap() } pub async fn get_default_address(&self) -> Result { let drk = self.drk.read().await; let pubkey = drk.default_address().await.map_err(|e| { e!("Failed to get default address: {e}"); Error::ServiceFailed })?; let network = drk.network; let address: darkfi_sdk::crypto::keypair::Address = StandardAddress::from_public(network, pubkey).into(); Ok(address.to_string()) } pub async fn get_balances(&self) -> Result> { let drk = self.drk.read().await; let balances = drk.money_balance().await.map_err(|e| { e!("Failed to get money balance: {e}"); Error::ServiceFailed })?; let aliases = drk.get_aliases_mapped_by_token().await.map_err(|e| { e!("Failed to get aliases: {e}"); Error::ServiceFailed })?; let mut result: Vec<(String, TokenId, u64)> = Vec::new(); for (token_id_str, balance) in balances { let alias = aliases.get(&token_id_str).cloned().unwrap_or_else(|| "UNKN".to_string()); let token_id = token_id_str.parse::().unwrap(); result.push((alias, token_id, balance)); } // Sort by balance result.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal)); Ok(result) } /// Fetch the Money contract's `info` state tree records from darkfid. async fn fetch_money_info(drk: &Drk) -> DarkFiResult, Vec>> { let params = JsonValue::Array(vec![ JsonValue::String(MONEY_CONTRACT_ID.to_string()), JsonValue::String(MONEY_CONTRACT_INFO_TREE.to_string()), ]); let rep = drk.darkfid_daemon_request("blockchain.get_contract_state", ¶ms).await?; let rep_str = rep.get::().ok_or_else(|| { DarkFiError::Custom( "Malformed reply: contract state result is not a string".to_string(), ) })?; let bytes = base64::decode(rep_str).ok_or_else(|| { DarkFiError::Custom("Malformed reply: contract state is not valid base64".to_string()) })?; let records: BTreeMap, Vec> = deserialize_async(&bytes).await?; Ok(records) } /// Initialize a brand-new wallet at the chain tip. async fn new_wallet_init(&self) -> DarkFiResult<()> { let drk = self.drk.read().await; // Only an empty wallet may be initialized here let (last_height, last_hash) = drk .get_last_scanned_block() .map_err(|e| DarkFiError::Custom(format!("Could not get last scanned block: {e}")))?; if last_height != 0 || last_hash != "-" || !drk.get_coins(true).await?.is_empty() { return Err(DarkFiError::Custom( "new_wallet_init called on a non-empty wallet".to_string(), )) } let (tip_height, tip_hash) = drk.get_last_confirmed_block().await?; if tip_height == 0 { // Genesis-only chain return Ok(()) } // Tree and root come from one consistent state snapshot let records = Self::fetch_money_info(&drk).await?; // Decode the Money coin tree let tree_bytes = records.get(MONEY_CONTRACT_COIN_MERKLE_TREE).ok_or_else(|| { DarkFiError::Custom("Coin tree not found in contract state".to_string()) })?; let (_, rest) = tree_bytes.split_at_checked(4).ok_or_else(|| { DarkFiError::Custom("Malformed coin tree record: missing set size prefix".to_string()) })?; let tree: MerkleTree = deserialize(rest)?; let mut tree = MerkleTree::from_parts( tree.prior_bridges().to_vec(), tree.current_bridge().clone(), tree.marked_indices().clone(), tree.checkpoints().clone(), u32::MAX as usize, ) .map_err(|_| DarkFiError::Custom("Invalid coin tree record".to_string()))?; let root_bytes = records.get(MONEY_CONTRACT_LATEST_COIN_ROOT).ok_or_else(|| { DarkFiError::Custom("Latest coin root not found in contract state".to_string()) })?; let latest_root: MerkleNode = deserialize(root_bytes)?; if tree.root(0) != Some(latest_root) { return Err(DarkFiError::Custom( "Fetched coin tree root does not match the chain's latest coin root".to_string(), )) } // Re-read the tip in case the chain advanced between reads let (tip_height2, tip_hash2) = drk.get_last_confirmed_block().await?; if tip_height != tip_height2 || tip_hash != tip_hash2 { return Err(DarkFiError::Custom(format!( "Tip moved during sync ({tip_height} -> {tip_height2})" ))) } // Make the snapshot point rewindable by `reset_to_height` tree.checkpoint(tip_height as usize); let mut merkle_batch = Batch::new(); merkle_batch.insert(KVDB_MERKLE_TREES_MONEY, &serialize(&tree)); let mut scanned_batch = Batch::new(); scanned_batch .insert(&tip_height.to_be_bytes(), &serialize(&(tip_hash.clone(), "-".to_string()))); drk.cache.kvdb.atomic_write(&[ (&drk.cache.merkle_trees, &merkle_batch), (&drk.cache.scanned_blocks, &scanned_batch), ])?; drk.cache.kvdb.flush_default_mode()?; i!( "New wallet state initialized at height {tip_height}, scanning starts from {}", tip_height + 1 ); Ok(()) } /// Decode a single base64-encoded serialized block entry from darkfid. async fn decode_block_param(param: &JsonValue) -> DarkFiResult { let param_str = param.get::().ok_or_else(|| { DarkFiError::Custom("Malformed reply: block entry is not a string".to_string()) })?; let bytes = base64::decode(param_str).ok_or_else(|| { DarkFiError::Custom("Malformed reply: block entry is not valid base64".to_string()) })?; let block = deserialize_async(&bytes).await?; Ok(block) } /// Get a batch of blocks from darkfid starting at `height` using the provided client. async fn fetch_blocks(rpc_client: &RpcClient, height: u32) -> DarkFiResult> { let req = JsonRequest::new( "blockchain.get_blocks", JsonValue::Array(vec![JsonValue::Number(height as f64)]), ); let rep = rpc_client.request(req).await?; let params_array = rep.get::>().ok_or_else(|| { DarkFiError::Custom("Malformed reply: get_blocks result is not an array".to_string()) })?; let mut blocks = Vec::with_capacity(params_array.len()); for param in params_array { blocks.push(Self::decode_block_param(param).await?); } Ok(blocks) } /// Fetch and scan blocks starting from the last scanned block. async fn scan_blocks(&self) -> DarkFiResult<()> { let drk = self.drk.read().await; // Grab last scanned block height let (mut height, mut hash) = drk .get_last_scanned_block() .map_err(|e| DarkFiError::Custom(format!("Could not get last scanned block: {e}")))?; // Never resume past a height that has no tree checkpoint let anchor = drk.get_money_tree().await?.checkpoints().back().map(|c| *c.id() as u32).unwrap_or(0); if height > anchor { let mut batch = Batch::new(); for h in anchor + 1..=height { batch.remove(&h.to_be_bytes()); } drk.cache.kvdb.atomic_write(&[ (&drk.cache.scanned_blocks, &batch), (&drk.cache.state_inverse_diff, &batch), ])?; height = anchor; hash = drk .get_scanned_block(&height) .map_err(|e| DarkFiError::Custom(format!("Could not get scanned block: {e}")))? .0; } // Grab our last scanned block from darkfid let block = match drk.get_block_by_height(height).await { Ok(b) => Some(b), Err(darkfi::Error::JsonRpcError((-32121, _))) => None, Err(e) => return Err(e), }; // Check if a reorg has happened if !block.as_ref().is_some_and(|b| b.hash().to_string() == hash) { match self .handle_reorg(&drk, height) .await .map_err(|e| DarkFiError::Custom(format!("Could not find common ancestor: {e}")))? { // The wallet was reset and re-initialized None => return Ok(()), Some(h) => height = h, } } // If last scanned block is genesis(0) we reset, // otherwise continue with the next block height. if height == 0 { drk.reset(&mut vec![]) .await .map_err(|e| DarkFiError::Custom(format!("Wallet state reset failed: {e}")))?; self.emit_balances_updated().await; } else { height += 1; } // The expected previous block hash let mut expected_prev = if height > 0 { Some( drk.get_scanned_block(&(height - 1)) .map_err(|e| DarkFiError::Custom(format!("Could not get scanned block: {e}")))? .0, ) } else { None }; // Grab last confirmed block height let (mut last_height, _) = drk.get_last_confirmed_block().await?; drop(drk); // Already scanned last confirmed block if height > last_height { return Ok(()) } // Generate a new scan cache let mut cache = self.drk.read().await.scan_cache(false).await?; // Save starting height to report progress let start_height = height; // Create RPC client for block fetching let endpoint = Self::endpoint(&self.sg_root, &self.net_transport); let rpc_client = match RpcClient::new(endpoint, self.ex.clone()).await { Ok(client) => client, Err(e) => return Err(DarkFiError::Custom(format!("Failed to create RPC client: {e}"))), }; let rpc_client = Arc::new(rpc_client); let rpc_client_ = rpc_client.clone(); // Bounded channel limits block fetches let (block_tx, block_rx) = smol::channel::bounded(BLOCK_BATCHES_BUFFER); // Fetcher task: fetches blocks continuously, blocks when channel is full let fetcher_task = StoppableTask::new(); let fetcher_task_ = fetcher_task.clone(); fetcher_task.start( async move { let mut fetch_height = height; while fetch_height <= last_height { match Self::fetch_blocks(&rpc_client_, fetch_height).await { Ok(blocks) => { let len = blocks.len() as u32; if block_tx.send(blocks).await.is_err() { // Channel closed, receiver is done break; } fetch_height += len; } Err(e) => { e!("Error fetching blocks: {e}"); break; } } } Ok(()) }, |_result| async move { // Cleanup: stop the RPC client rpc_client.stop().await; }, DarkFiError::DetachedTaskStopped, self.ex.clone(), ); // Update scan progress self.emit_progress_update(height - start_height, last_height - start_height).await; // Process blocks from the channel until it closes let original_last_height = last_height; let mut processing_error = None; loop { let blocks = match block_rx.recv().await { Ok(b) => b, Err(e) => { // Channel closed: the fetcher finished or died early if height < original_last_height && processing_error.is_none() { processing_error = Some(DarkFiError::Custom(format!( "Block fetcher ended early at height {height} (expected up to {original_last_height}): {e}" ))); } break; } }; if blocks.is_empty() { processing_error = Some(DarkFiError::Custom( "Received empty block batch from fetcher".to_string(), )); break; } let old_nullifiers = cache.owncoins_nullifiers.clone(); // Scan each block, verifying the batch chains to our scan state for block in blocks { let block_height = block.header.height; if let Some(prev) = &expected_prev { if &block.header.previous.to_string() != prev { processing_error = Some(DarkFiError::Custom(format!( "Non-contiguous block batch: block {block_height} does not chain to {prev}" ))); break; } } expected_prev = Some(block.header.hash().to_string()); let drk = self.drk.read().await; if let Err(e) = drk.scan_block(&mut cache, &block).await { processing_error = Some(e); break; } height += 1; } if height > last_height { last_height = height; } // Update balances if they changed if cache.owncoins_nullifiers != old_nullifiers { self.emit_balances_updated().await; } // Update scan progress self.emit_progress_update(height - start_height, last_height - start_height).await; // Break on processing error if processing_error.is_some() { break; } } // Stop the fetcher gracefully before dropping the receiver fetcher_task_.stop().await; // Drop the receiver (now that fetcher is stopped) drop(block_rx); // Return any processing error that occurred if let Some(e) = processing_error { return Err(e); } Ok(()) } /// Handle a subscribed block. Returns `Ok(false)` when a below-floor /// reorg reset the wallet: the caller must abort the subscription. async fn handle_subscribed_block( &self, last_scanned_height: &mut u32, block: BlockInfo, ) -> DarkFiResult { i!("Received block {}", block.header.height); let drk = self.drk.read().await; if block.header.height <= *last_scanned_height { let reset_height = block.header.height.saturating_sub(1); // Can't handle a reorg to below the earliest wallet state if self.scanned_floor(&drk)?.is_some_and(|floor| reset_height < floor) { drop(drk); self.handle_full_reset().await; return Ok(false) } if let Err(e) = drk.reset_to_height(reset_height, &mut vec![]).await { return Err(DarkFiError::Custom(format!("Wallet state reset failed: {e}"))) } // Scan genesis again if needed if reset_height == 0 { let genesis = match drk.get_block_by_height(reset_height).await { Ok(b) => b, Err(e) => { return Err(DarkFiError::Custom(format!("RPC client request failed: {e}"))) } }; let mut scan_cache = drk.scan_cache(false).await?; if let Err(e) = drk.scan_block(&mut scan_cache, &genesis).await { return Err(DarkFiError::Custom(format!("Scanning block failed: {e}"))) }; } } // The subscribed block must chain to our scanned state if block.header.height > 0 { let (prev_scanned_hash, _) = drk.get_scanned_block(&(block.header.height - 1)).map_err(|e| { DarkFiError::Custom(format!( "Missing scanned block record at height {}: {e}", block.header.height - 1 )) })?; if prev_scanned_hash != block.header.previous.to_string() { return Err(DarkFiError::Custom(format!( "Subscribed block {} does not chain to scanned block {}", block.header.height, block.header.height - 1 ))) } } let mut scan_cache = drk.scan_cache(false).await?; let old_nullifiers = scan_cache.owncoins_nullifiers.clone(); if let Err(e) = drk.scan_block(&mut scan_cache, &block).await { return Err(DarkFiError::Custom(format!("Scanning block failed: {e}"))) } if scan_cache.owncoins_nullifiers != old_nullifiers { self.emit_balances_updated().await; } // Set new last scanned block height *last_scanned_height = block.header.height; Ok(true) } /// Scan until the scanned tip matches darkfid's confirmed tip. /// Returns the scanned tip, used as the subscription baseline. async fn catch_up_to_tip(&self) -> DarkFiResult { loop { // First we do a clean scan if let Err(e) = self.scan_blocks().await { return Err(DarkFiError::Custom(format!("Failed during scanning: {e}"))) } // Grab last confirmed block let drk = self.drk.read().await; let (last_confirmed_height, last_confirmed_hash) = drk.get_last_confirmed_block().await?; // Grab last scanned block let (last_scanned_height, last_scanned_hash) = match drk.get_last_scanned_block() { Ok(last) => last, Err(e) => { return Err(DarkFiError::Custom(format!( "Retrieving last scanned block failed: {e}" ))) } }; drop(drk); // The wallet was fully reset if last_scanned_height == 0 && last_scanned_hash == "-" { return Err(DarkFiError::Custom("Wallet was reset during catch-up".to_string())) } // Rescan if other blocks have been created while we were scanning if last_confirmed_height != last_scanned_height || last_confirmed_hash != last_scanned_hash { continue } return Ok(last_scanned_height) } } /// Call catch_up_to_tip() then subscribe to blocks from darkfid. async fn subscribe_blocks(&self, rpc_task: StoppableTaskPtr) -> DarkFiResult<()> { // Brand-new wallets (nothing scanned, no coins) are inited by // fetching the current money tree from darkfid. let drk = self.drk.read().await; let (last_height, last_hash) = drk .get_last_scanned_block() .map_err(|e| DarkFiError::Custom(format!("Could not get last scanned block: {e}")))?; let is_new_wallet = last_height == 0 && last_hash == "-" && drk.get_coins(true).await?.is_empty(); drop(drk); if is_new_wallet { i!("New wallet detected, initializing state at the chain tip..."); loop { if let Err(e) = self.new_wallet_init().await { e!("New wallet init attempt failed: {e}"); i!("Retrying new wallet init in {NEW_WALLET_INIT_RETRY_TIME} seconds..."); sleep(NEW_WALLET_INIT_RETRY_TIME).await; continue } break } } let mut last_scanned_height = self.catch_up_to_tip().await?; let publisher = Publisher::new(); let subscription = publisher.clone().subscribe().await; let _publisher = publisher.clone(); let rpc_client = Arc::new( RpcClient::new(Self::endpoint(&self.sg_root, &self.net_transport), self.ex.clone()) .await?, ); let rpc_client_ = rpc_client.clone(); rpc_task.clone().start( async move { let req = JsonRequest::new("blockchain.subscribe_blocks", JsonValue::Array(vec![])); rpc_client_.subscribe(req, _publisher).await }, |res| async move { rpc_client.stop().await; match res { Ok(()) | Err(DarkFiError::DetachedTaskStopped) | Err(DarkFiError::RpcServerStopped) => { /* Do nothing */ } Err(e) => { e!("JSON-RPC server error: {e}"); publisher .notify(JsonResult::Error(JsonError::new( ErrorCode::InternalError, None, 0, ))) .await; } } }, DarkFiError::RpcServerStopped, self.ex.clone(), ); // Set the blockchain indicator to fully scanned status and remove the progress text if let Some(node) = self.node.upgrade() { let _ = node.trigger("connect", serialize(&(3u8, String::new()))).await; } // Wait for blocks from darkfid i!("Blockchain is fully scanned, waiting for blocks..."); let e = 'outer: loop { match subscription.receive().await { JsonResult::Notification(n) => { if n.method != "blockchain.subscribe_blocks" { break DarkFiError::UnexpectedJsonRpc(format!( "Got foreign notification from darkfid: {}", n.method )) } // Verify parameters if !n.params.is_array() { break DarkFiError::UnexpectedJsonRpc( "Received notification params are not an array".to_string(), ) } let Some(params) = n.params.get::>() else { break DarkFiError::UnexpectedJsonRpc( "Notification parameters are not a JSON array".to_string(), ) }; if params.is_empty() { break DarkFiError::UnexpectedJsonRpc( "Notification parameters are empty".to_string(), ) } for param in params { let block = match Self::decode_block_param(param).await { Ok(b) => b, Err(e) => break 'outer e, }; match self.handle_subscribed_block(&mut last_scanned_height, block).await { Ok(true) => continue, // Below-floor reorg reset the wallet Ok(false) => { rpc_task.stop().await; return Ok(()) } Err(e) => break 'outer e, } } } JsonResult::Error(e) => { // Some error happened in the transmission break DarkFiError::UnexpectedJsonRpc(format!("Got error from JSON-RPC: {e:?}")) } x => { // And this is weird break DarkFiError::UnexpectedJsonRpc(format!( "Got unexpected data from JSON-RPC: {x:?}" )) } } }; rpc_task.stop().await; Err(e) } /// Lowest height whose wallet state we possess: the earliest scanned /// block record. `None` when nothing was ever scanned. fn scanned_floor(&self, drk: &Drk) -> DarkFiResult> { let records = drk.get_scanned_block_records().map_err(|e| { DarkFiError::Custom(format!("Could not get scanned block records: {e}")) })?; Ok(records.iter().map(|(height, _, _)| *height).min()) } /// Full reset after a reorg below the earliest wallet state: wipe /// everything and re-initialize at the current tip. async fn handle_full_reset(&self) { e!("Reorg reached below the earliest wallet state, resetting and re-initializing"); loop { let drk = self.drk.read().await; let result = drk.reset(&mut vec![]).await; drop(drk); self.emit_balances_updated().await; match result { Ok(()) => break, Err(e) => { e!("Wallet reset attempt failed: {e}"); i!("Retrying wallet reset in {NEW_WALLET_INIT_RETRY_TIME} seconds..."); sleep(NEW_WALLET_INIT_RETRY_TIME).await; } } } loop { if let Err(e) = self.new_wallet_init().await { e!("New wallet init attempt failed: {e}"); i!("Retrying new wallet init in {NEW_WALLET_INIT_RETRY_TIME} seconds..."); sleep(NEW_WALLET_INIT_RETRY_TIME).await; continue } break } } /// Find the exact block height the reorg happened. If it happened below /// the earliest wallet state, the wallet is fully reset and re-initialized /// at the current tip, and `Ok(None)` is returned. async fn handle_reorg(&self, drk: &Drk, mut height: u32) -> DarkFiResult> { i!("Reorg detected, finding common ancestor..."); let floor = self.scanned_floor(drk)?; let start_height = height; height = height.saturating_sub(1); while height != 0 { // Below the earliest wallet state if floor.is_some_and(|floor| height < floor) { self.handle_full_reset().await; return Ok(None) } // Grab our scanned block hash for that height let (scanned_block_hash, _) = drk.get_scanned_block(&height).map_err(|e| DarkFiError::Custom(e.to_string()))?; // Grab the block from darkfid for that height let block = match drk.get_block_by_height(height).await { Ok(b) => Some(b), // Check if block was found Err(DarkFiError::JsonRpcError((-32121, _))) => None, Err(e) => { e!("Error getting block {height} while finding common ancestor: {e}"); return Err(DarkFiError::Custom(e.to_string())) } }; // Continue to previous one if they don't match if !block.as_ref().is_some_and(|b| b.hash().to_string() == scanned_block_hash) { height = height.saturating_sub(1); continue } // Reset to its height drk.reset_to_height(height, &mut vec![]) .await .map_err(|e| DarkFiError::Custom(e.to_string()))?; self.emit_balances_updated().await; break } i!("Found common ancestor: {height} ({}-block reorg)", start_height - height); Ok(Some(height)) } /// Emit balances_updated signal with the balances encoded in the payload. /// Only emits when the encoded balances differ from the last emitted ones. async fn emit_balances_updated(&self) { let Some(node) = self.node.upgrade() else { return }; let balances = match self.get_balances().await { Ok(b) => b, Err(e) => { e!("Failed to get balances for balances_updated signal: {e}"); return } }; let mut data = vec![]; if let Err(e) = balances.encode(&mut data) { e!("Failed to encode balances for balances_updated signal: {e}"); return } let mut last = self.last_balances.lock(); if let Some(last) = &*last { if *last == balances { return } } *last = Some(balances); let _ = node.trigger("balances_updated", data).await; } /// Emit a progress update async fn emit_progress_update(&self, blocks_scanned: u32, total_blocks: u32) { let Some(node) = self.node.upgrade() else { return }; let percentage = if total_blocks > 0 { blocks_scanned * 100 / total_blocks } else { 0 }; let status = if percentage > 50 { 2u8 } else { 1u8 }; let _ = node .trigger( "connect", serialize(&(status, format!("{blocks_scanned}/{total_blocks} [{percentage}%]"))), ) .await; } /// Emit tx_updated signal async fn emit_tx_updated(&self, state: &TxState) { if let Some(node) = self.node.upgrade() { let mut data = vec![]; state.id.clone().encode(&mut data).unwrap(); Some(state.confirmed).encode(&mut data).unwrap(); state.amount.encode(&mut data).unwrap(); state.token_symbol.clone().encode(&mut data).unwrap(); state.recipient.map(|r| r.to_string()).encode(&mut data).unwrap(); let _ = node.trigger("tx_updated", data).await; } } async fn emit_tx_status_updated(&self, confirmed: bool) { if let Some(node) = self.node.upgrade() { let mut data = vec![]; None::.encode(&mut data).unwrap(); Some(confirmed).encode(&mut data).unwrap(); None::.encode(&mut data).unwrap(); None::.encode(&mut data).unwrap(); None::.encode(&mut data).unwrap(); let _ = node.trigger("tx_updated", data).await; } } /// Emit tx_built signal when transaction is built async fn emit_tx_built( &self, amount: String, token_symbol: String, recipient: Address, tx: Transaction, ) { if let Some(node) = self.node.upgrade() { let mut data = vec![]; amount.encode(&mut data).unwrap(); token_symbol.encode(&mut data).unwrap(); recipient.to_string().encode(&mut data).unwrap(); tx.encode(&mut data).unwrap(); let _ = node.trigger("tx_built", data).await; } } /// Emit tx_built_error signal when transaction building fails async fn emit_tx_built_error(&self, error: String) { if let Some(node) = self.node.upgrade() { let mut data = vec![]; error.encode(&mut data).unwrap(); let _ = node.trigger("tx_built_error", data).await; } } async fn process_get_default_address(me: &Weak, sub: &MethodCallSub) -> bool { let Ok(method_call) = sub.receive().await else { d!("get_default_address method closed"); return false }; t!("method called: get_default_address()"); let Some(self_) = me.upgrade() else { e!("drk plugin destroyed before get_default_address task was stopped!"); if let Some(send_res) = method_call.send_res { let _ = send_res.send(vec![]).await; } return false }; let address = match self_.get_default_address().await { Ok(addr) => addr, Err(e) => { e!("Failed to get default address: {e}"); if let Some(send_res) = method_call.send_res { let _ = send_res.send(vec![]).await; } return true } }; i!("Got default address: {address}"); if let Some(send_res) = method_call.send_res { let mut cur = Cursor::new(vec![]); if address.encode(&mut cur).is_ok() { let _ = send_res.send(cur.into_inner()).await; } else { e!("Failed to encode default address"); let _ = send_res.send(vec![]).await; } } else { e!("No send_res channel available"); } true } async fn process_get_balances(me: &Weak, sub: &MethodCallSub) -> bool { let Ok(method_call) = sub.receive().await else { d!("get_balances method closed"); return false }; t!("method called: get_balances()"); let Some(self_) = me.upgrade() else { e!("drk plugin destroyed before get_balances task was stopped!"); if let Some(send_res) = method_call.send_res { let _ = send_res.send(vec![]).await; } return false }; let balances = match self_.get_balances().await { Ok(b) => b, Err(e) => { e!("Failed to get balances: {e}"); if let Some(send_res) = method_call.send_res { let _ = send_res.send(vec![]).await; } return true } }; if let Some(send_res) = method_call.send_res { let mut cur = Cursor::new(vec![]); if balances.encode(&mut cur).is_ok() { let _ = send_res.send(cur.into_inner()).await; } else { e!("Failed to encode balances"); let _ = send_res.send(vec![]).await; } } else { e!("No send_res channel available"); } true } async fn process_get_tx_status(me: &Weak, sub: &MethodCallSub) -> bool { let Ok(method_call) = sub.receive().await else { d!("get_tx_status method closed"); return false }; t!("method called: get_tx_status()"); fn decode_data(data: &[u8]) -> std::io::Result { let mut cur = Cursor::new(&data); let tx_id = String::decode(&mut cur)?; Ok(tx_id) } let Ok(tx_id) = decode_data(&method_call.data) else { d!("get_tx_status() method invalid arg data"); return true }; let Some(self_) = me.upgrade() else { e!("drk plugin destroyed before get_tx_status task was stopped!"); if let Some(send_res) = method_call.send_res { let _ = send_res.send(vec![]).await; } return false }; let drk = self_.drk.read().await; let Ok((_, status, _block_height, _tx)) = drk.get_tx_history_record(&tx_id).await else { d!("get_tx_history() method failed to get tx history record"); return true }; if let Some(send_res) = method_call.send_res { let mut cur = Cursor::new(vec![]); let confirmed = status.as_str() == "Confirmed"; if confirmed.encode(&mut cur).is_ok() { let _ = send_res.send(cur.into_inner()).await; } else { e!("Failed to encode tx status"); let _ = send_res.send(vec![]).await; } } else { e!("No send_res channel available"); } true } /// Build a transaction without broadcasting it pub async fn build_tx( &self, amount: &str, token_id: TokenId, recipient: PublicKey, ) -> DarkFiResult { let drk = self.drk.read().await; drk.transfer(amount, token_id, recipient, None, None, false).await } /// Build a transaction from a BuildTxRequest (called by background task) async fn build_tx_request( &self, request: BuildTxRequest, ) -> DarkFiResult<(Transaction, String, Address, String)> { let drk = self.drk.read().await; let aliases = drk.get_aliases_mapped_by_token().await.unwrap_or_default(); let token_symbol = aliases.get(&request.token_id.to_string()).unwrap_or(&"UNKN".to_string()).to_string(); let recipient: Address = StandardAddress::from_public(drk.network, request.recipient).into(); let tx = self.build_tx(&request.amount, request.token_id, request.recipient).await?; Ok((tx, token_symbol, recipient, request.amount)) } async fn process_build_tx(me: &Weak, sub: &MethodCallSub) -> bool { let Ok(method_call) = sub.receive().await else { d!("build_tx method closed"); return false }; t!("method called: build_tx()"); // Send empty response immediately to unblock the caller if let Some(send_res) = method_call.send_res { let _ = send_res.send(vec![]).await; } fn decode_data(data: &[u8]) -> std::io::Result<(String, TokenId, PublicKey)> { let mut cur = Cursor::new(&data); let amount = String::decode(&mut cur)?; let token_id = TokenId::decode(&mut cur)?; let recipient = PublicKey::decode(&mut cur)?; Ok((amount, token_id, recipient)) } let Ok((amount, token_id, recipient)) = decode_data(&method_call.data) else { d!("build_tx() method invalid arg data"); return true }; let Some(self_) = me.upgrade() else { e!("drk plugin destroyed before build_tx task was stopped!"); return false }; // Send request to channel for background processing let request = BuildTxRequest { amount, token_id, recipient }; let _ = self_.build_tx_channel.send(request).await; true } /// Broadcast a transaction pub async fn broadcast_tx(&self, tx: Transaction) -> Result { let drk = self.drk.read().await; let tx_id = drk.broadcast_tx(&tx, &mut vec![]).await.map_err(|e| { e!("Failed to broadcast transaction: {e}"); Error::ServiceFailed })?; Ok(tx_id) } async fn process_broadcast_tx(me: &Weak, sub: &MethodCallSub) -> bool { let Ok(method_call) = sub.receive().await else { d!("broadcast_tx method closed"); return false }; t!("method called: broadcast_tx()"); let Some(send_res) = method_call.send_res else { return true }; // Send empty response immediately to unblock the caller let _ = send_res.send(vec![]).await; let Some(self_) = me.upgrade() else { e!("drk plugin destroyed before broadcast_tx task was stopped!"); return false }; let Ok(tx) = Transaction::decode(&mut Cursor::new(&method_call.data)) else { d!("broadcast_tx() method invalid arg data"); return true }; let drk = self_.drk.read().await; if let Err(e) = drk.mark_tx_spend(&tx, &mut vec![]).await { e!("Failed to mark transaction coins as spent: {e}"); self_.emit_tx_status_updated(false).await; return true }; let tx_id = match drk.broadcast_tx(&tx, &mut vec![]).await { Ok(t) => t, Err(e) => { e!("Failed to broadcast transaction: {e}"); self_.emit_tx_status_updated(false).await; return true } }; drop(drk); let state = TxState { id: Some(tx_id), confirmed: false, amount: None, token_symbol: None, recipient: None, }; self_.emit_tx_updated(&state).await; self_.emit_balances_updated().await; true } /// `net.transport` or one of the `wallet.*.rpc` endpoint settings was /// changed: cancel the scan/subscribe session, stop drk's RPC client, /// and swap it over to the recomputed endpoint. The subscribe loop in /// `Self::start()` sees the dead session and reconnects immediately. async fn restart_rpc_connection(&self) { let endpoint = Self::endpoint(&self.sg_root, &self.net_transport); i!("Restarting darkfid connection at {endpoint}"); // Cancel the scan/subscribe task let subscribe_task = self.subscribe_task.lock().take(); if let Some(subscribe_task) = &subscribe_task { subscribe_task.stop_nowait(); } // Stop drk's rpc client let old_client = self.drk.write().await.rpc_client.take(); if let Some(old_client) = old_client { old_client.read().await.stop().await; } // Swap drk's rpc client over to the new endpoint let new_client = DarkfidRpcClient::new(endpoint, self.ex.clone()).await; self.drk.write().await.rpc_client = Some(RwLock::new(new_client)); } async fn start(self: Arc, tasks: Vec>) { let me2 = Arc::downgrade(&self); let subscribe_task = self.ex.spawn(async move { loop { let Some(self2) = me2.upgrade() else { break }; let endpoint = Self::endpoint(&self2.sg_root, &self2.net_transport); i!("Attempting to connect to darkfid daemon at {}", endpoint); let subscribe_rpc_task = StoppableTask::new(); let subscribe_rpc_task_ = subscribe_rpc_task.clone(); if let Some(node) = self2.node.upgrade() { let _ = node.trigger("connect", serialize(&(0u8, String::new()))).await; } let task = StoppableTask::new(); *self2.subscribe_task.lock() = Some(task.clone()); let (task_res_tx, task_res_rx) = smol::channel::bounded(1); let self3 = self2.clone(); task.clone().start( async move { let res = self3.subscribe_blocks(subscribe_rpc_task).await; let _ = task_res_tx.send(res).await; Ok(()) }, |res| async move { match res { Ok(()) | Err(DarkFiError::DetachedTaskStopped) => {} Err(e) => { e!("Session task error: {e}"); } } }, DarkFiError::DetachedTaskStopped, self2.ex.clone(), ); let res = match task_res_rx.recv().await { Ok(res) => res, Err(_) => { i!("Subscribe task cancelled"); Err(DarkFiError::DetachedTaskStopped) } }; subscribe_rpc_task_.stop_nowait(); let reset = match res { Ok(()) => true, Err(e) => { e!("Failed during drk scanning: {e}"); false } }; *self2.subscribe_task.lock() = None; if let Some(node) = self2.node.upgrade() { let _ = node.trigger("connect", serialize(&(0u8, String::new()))).await; } // Endpoint changed while we were connected, no need to sleep if Self::endpoint(&self2.sg_root, &self2.net_transport) != endpoint { continue } if !reset { i!("Retrying connection to darkfid in {} seconds...", DARKFID_RETRY_TIME); sleep(DARKFID_RETRY_TIME).await; } } }); // Watch the settings feeding the darkfid endpoint. Changing any of // them restarts the RPC connection at the recomputed endpoint. let setting_node = self.sg_root.lookup_node("/setting").unwrap(); let watch_props = vec![ self.net_transport.prop(), setting_node.get_property("wallet.tcp.rpc").unwrap(), setting_node.get_property("wallet.tor.rpc").unwrap(), ]; let mut watch_tasks = vec![]; for prop in watch_props { let me = Arc::downgrade(&self); let name = prop.name.clone(); let sub = prop.subscribe_modify(); let task = self.ex.spawn(async move { while let Ok(_) = sub.receive().await { let Some(me2) = me.upgrade() else { break }; i!("Setting {name} changed, restarting darkfid connection"); me2.restart_rpc_connection().await; } }); watch_tasks.push(task); } let mut all_tasks = vec![subscribe_task]; all_tasks.extend(watch_tasks); all_tasks.extend(tasks); self.tasks.set(all_tasks).unwrap(); } }