/* 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 darkfi::{ system::{sleep, Publisher, PublisherPtr, StoppableTask}, tx::Transaction, Result as DarkFiResult, }; use darkfi_money_contract::model::TokenId; use darkfi_sdk::crypto::keypair::{Address, Network, PublicKey, StandardAddress}; use darkfi_serial::{serialize, Decodable, Encodable}; use drk::{rpc::subscribe_blocks, Drk}; use parking_lot::Mutex as SyncMutex; use smol::{channel::unbounded, lock::RwLock}; use std::{ io::Cursor, sync::{Arc, OnceLock, Weak}, }; use url::Url; use crate::{ error::{Error, Result}, prop::BatchGuardPtr, scene::{MethodCallSub, Pimpl, SceneNode, SceneNodePtr, SceneNodeType, SceneNodeWeak}, ExecutorPtr, }; // TODO: should be configurable at runtime //const DARKFID_ENDPOINT: &str = "tcp://127.0.0.1:18345"; const DARKFID_ENDPOINT: &str = "tcp+tls://node0.testnet.dark.fi:18345"; const DARKFID_RETRY_TIME: u64 = 20; #[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)] enum TxStatus { Confirming, Confirmed, Error(String), } impl TxStatus { fn text(&self) -> String { match self { TxStatus::Confirming => "Confirming transaction...".to_string(), TxStatus::Confirmed => "Transaction confirmed".to_string(), TxStatus::Error(ref err) => format!("Error sending transaction: {err}"), } } } #[derive(Debug, Clone)] struct TxState { id: Option, status: TxStatus, 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>>, scan_progress_pub: PublisherPtr<(u32, u32)>, drk: Arc>, build_tx_channel: smol::channel::Sender, last_balances: SyncMutex>>, } impl DrkPlugin { pub async fn new(node: SceneNodeWeak, sg_root: SceneNodePtr, ex: ExecutorPtr) -> Result { let node_ref = node.upgrade().unwrap(); let setting_root = Arc::new(SceneNode::new("setting", SceneNodeType::SettingRoot)); node_ref.link(setting_root.clone()); let endpoint = Url::parse(DARKFID_ENDPOINT).unwrap(); 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), &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, scan_progress_pub: Publisher::new(), last_balances: SyncMutex::new(None), }); // 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(ex.clone(), tasks).await; Ok(Pimpl::Drk(self_)) } async fn apply_settings(_self: Arc, _batch: BatchGuardPtr) { // TODO } 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) } /// 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 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.status.text()).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, status: &TxStatus) { if let Some(node) = self.node.upgrade() { let mut data = vec![]; None::.encode(&mut data).unwrap(); Some(status.text()).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_history 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 status = match status.as_str() { "Broadcasted" => TxStatus::Confirming, "Confirmed" => TxStatus::Confirmed, _ => TxStatus::Error("unknown status".to_string()), }; if status.text().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 } /// 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(&TxStatus::Error( "failed to mark coins as spent".to_string(), )) .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(&TxStatus::Error("failed to broadcast".to_string())) .await; return true } }; drop(drk); let state = TxState { id: Some(tx_id), status: TxStatus::Confirming, amount: None, token_symbol: None, recipient: None, }; self_.emit_tx_updated(&state).await; self_.emit_balances_updated().await; true } async fn start(self: Arc, ex: ExecutorPtr, tasks: Vec>) { let endpoint = Url::parse(DARKFID_ENDPOINT).unwrap(); let self2 = self.clone(); let drk = self.drk.clone(); let (shell_sender, shell_receiver) = unbounded(); let ex_ = ex.clone(); let progress_sub = self.scan_progress_pub.clone().subscribe().await; let scan_progress_task = ex.spawn(async move { let mut first_height = None; loop { let (height, final_height) = progress_sub.receive().await; if first_height.is_none() { first_height = Some(height); } let progress: f64 = match final_height - first_height.unwrap() { 0 => 0., _ => { (height - first_height.unwrap()) as f64 / (final_height - first_height.unwrap()) as f64 } }; let status: u8 = if progress > 0.5 { 2 } else { 1 }; let Some(node) = self2.node.upgrade() else { continue }; let start_height = first_height.unwrap(); let blocks_scanned = height - start_height; let total_blocks = final_height - start_height; let percentage = if total_blocks > 0 { (blocks_scanned as f32 / total_blocks as f32 * 100.0) as u32 } else { 0 }; let desc = format!("{}/{} [{}%]", blocks_scanned, total_blocks, percentage); let _ = node.trigger("connect", serialize(&(status, desc))).await; } }); let self2 = self.clone(); // Task that handles the RPC subscription with retry logic let subscribe_task = ex.spawn(async move { loop { i!("Attempting to connect to darkfid daemon at {}", endpoint); let subscribe_rpc_task = StoppableTask::new(); let shell_sender = shell_sender.clone(); let drk = drk.clone(); let endpoint = endpoint.clone(); let ex = ex_.clone(); let progress_pub = self2.scan_progress_pub.clone(); let _ = self2 .node .upgrade() .unwrap() .trigger("connect", serialize(&(0u8, String::new()))) .await; if let Err(e) = drk .read() .await .scan_blocks(&mut vec![], Some(&shell_sender), &false, Some(progress_pub)) .await { e!("Failed during drk scanning: {e}"); let _ = self2 .node .upgrade() .unwrap() .trigger("connect", serialize(&(0u8, String::new()))) .await; // Wait before retrying i!("Retrying connection to darkfid in {} seconds...", DARKFID_RETRY_TIME); sleep(DARKFID_RETRY_TIME).await; continue } let _ = self2 .node .upgrade() .unwrap() .trigger("connect", serialize(&(3u8, String::new()))) .await; self2.emit_balances_updated().await; match subscribe_blocks( &drk, subscribe_rpc_task, shell_sender.clone(), endpoint, &ex, ) .await { Ok(()) => { i!("darkfid subscription closed normally (detached task stopped)"); } Err(e) => { e!("darkfid connection failed: {e}"); } } let _ = self2 .node .upgrade() .unwrap() .trigger("connect", serialize(&(0u8, String::new()))) .await; // Wait before retrying i!("Retrying connection to darkfid in {} seconds...", DARKFID_RETRY_TIME); sleep(DARKFID_RETRY_TIME).await; } }); let self2 = self.clone(); let subscribe_recv_task = ex.spawn(async move { loop { let recv = shell_receiver.recv().await; if let Ok(lines) = recv { self2.emit_balances_updated().await; for line in lines.iter() { i!(line); } } } }); let mut all_tasks = vec![scan_progress_task, subscribe_task, subscribe_recv_task]; all_tasks.extend(tasks); self.tasks.set(all_tasks).unwrap(); } }