darkfid/rpc/
tx.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2026 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19use darkfi_serial::deserialize_async;
20use tinyjson::JsonValue;
21use tracing::{error, warn};
22
23use darkfi::{
24    rpc::jsonrpc::{
25        ErrorCode::{InternalError, InvalidParams},
26        JsonError, JsonResponse, JsonResult,
27    },
28    tx::Transaction,
29    util::encoding::base64,
30};
31
32use super::DarkfiNode;
33use crate::{server_error, RpcError};
34
35impl DarkfiNode {
36    // RPCAPI:
37    // Simulate a network state transition with the given transaction.
38    // Returns `true` if the transaction is valid, otherwise, a corresponding
39    // error.
40    //
41    // --> {"jsonrpc": "2.0", "method": "tx.simulate", "params": ["base64encodedTX"], "id": 1}
42    // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
43    pub async fn tx_simulate(&self, id: u16, params: JsonValue) -> JsonResult {
44        let Some(params) = params.get::<Vec<JsonValue>>() else {
45            return JsonError::new(InvalidParams, None, id).into()
46        };
47        if params.len() != 1 || !params[0].is_string() {
48            return JsonError::new(InvalidParams, None, id).into()
49        }
50
51        if !*self.validator.synced.read().await {
52            error!(target: "darkfid::rpc::tx_simulate", "Blockchain is not synced");
53            return server_error(RpcError::NotSynced, id, None)
54        }
55
56        // Try to deserialize the transaction
57        let tx_enc = params[0].get::<String>().unwrap().trim();
58        let tx_bytes = match base64::decode(tx_enc) {
59            Some(v) => v,
60            None => {
61                error!(target: "darkfid::rpc::tx_simulate", "Failed decoding base64 transaction");
62                return server_error(RpcError::ParseError, id, None)
63            }
64        };
65
66        let tx: Transaction = match deserialize_async(&tx_bytes).await {
67            Ok(v) => v,
68            Err(e) => {
69                error!(target: "darkfid::rpc::tx_simulate", "Failed deserializing bytes into Transaction: {e}");
70                return server_error(RpcError::ParseError, id, None)
71            }
72        };
73
74        // Simulate state transition
75        let result = self.validator.append_tx(&tx, false).await;
76        if result.is_err() {
77            error!(
78                target: "darkfid::rpc::tx_simulate", "Failed to validate state transition: {}",
79                result.err().unwrap()
80            );
81            return server_error(RpcError::TxSimulationFail, id, None)
82        };
83
84        JsonResponse::new(JsonValue::Boolean(true), id).into()
85    }
86
87    // RPCAPI:
88    // Append a given transaction to the mempool and broadcast it to
89    // the P2P network. The function will first simulate the state
90    // transition in order to see if the transaction is actually valid,
91    // and in turn it will return an error if this is the case.
92    // Otherwise, a transaction ID will be returned.
93    //
94    // --> {"jsonrpc": "2.0", "method": "tx.broadcast", "params": ["base64encodedTX"], "id": 1}
95    // <-- {"jsonrpc": "2.0", "result": "txID...", "id": 1}
96    pub async fn tx_broadcast(&self, id: u16, params: JsonValue) -> JsonResult {
97        let Some(params) = params.get::<Vec<JsonValue>>() else {
98            return JsonError::new(InvalidParams, None, id).into()
99        };
100        if params.len() != 1 || !params[0].is_string() {
101            return JsonError::new(InvalidParams, None, id).into()
102        }
103
104        if !*self.validator.synced.read().await {
105            error!(target: "darkfid::rpc::tx_broadcast", "Blockchain is not synced");
106            return server_error(RpcError::NotSynced, id, None)
107        }
108
109        // Try to deserialize the transaction
110        let tx_enc = params[0].get::<String>().unwrap().trim();
111        let tx_bytes = match base64::decode(tx_enc) {
112            Some(v) => v,
113            None => {
114                error!(target: "darkfid::rpc::tx_broadcast", "Failed decoding base64 transaction");
115                return server_error(RpcError::ParseError, id, None)
116            }
117        };
118
119        let tx: Transaction = match deserialize_async(&tx_bytes).await {
120            Ok(v) => v,
121            Err(e) => {
122                error!(target: "darkfid::rpc::tx_broadcast", "Failed deserializing bytes into Transaction: {e}");
123                return server_error(RpcError::ParseError, id, None)
124            }
125        };
126
127        // We'll perform the state transition check here.
128        if let Err(e) = self.validator.append_tx(&tx, true).await {
129            error!(target: "darkfid::rpc::tx_broadcast", "Failed to append transaction to mempool: {e}");
130            return server_error(RpcError::TxSimulationFail, id, None)
131        };
132
133        self.p2p_handler.p2p.broadcast(&tx).await;
134        if !self.p2p_handler.p2p.is_connected() {
135            warn!(target: "darkfid::rpc::tx_broadcast", "No connected channels to broadcast tx");
136        }
137
138        let tx_hash = tx.hash().to_string();
139        JsonResponse::new(JsonValue::String(tx_hash), id).into()
140    }
141
142    // RPCAPI:
143    // Queries the node pending transactions store to retrieve all transactions.
144    // Returns a vector of hex-encoded transaction hashes.
145    //
146    // --> {"jsonrpc": "2.0", "method": "tx.pending", "params": [], "id": 1}
147    // <-- {"jsonrpc": "2.0", "result": ["TxHash" , "..."], "id": 1}
148    pub async fn tx_pending(&self, id: u16, params: JsonValue) -> JsonResult {
149        let Some(params) = params.get::<Vec<JsonValue>>() else {
150            return JsonError::new(InvalidParams, None, id).into()
151        };
152        if !params.is_empty() {
153            return JsonError::new(InvalidParams, None, id).into()
154        }
155
156        if !*self.validator.synced.read().await {
157            error!(target: "darkfid::rpc::tx_pending", "Blockchain is not synced");
158            return server_error(RpcError::NotSynced, id, None)
159        }
160
161        let pending_txs = match self.validator.blockchain.get_pending_txs() {
162            Ok(v) => v,
163            Err(e) => {
164                error!(target: "darkfid::rpc::tx_pending", "Failed fetching pending txs: {e}");
165                return JsonError::new(InternalError, None, id).into()
166            }
167        };
168
169        let pending_txs: Vec<JsonValue> =
170            pending_txs.iter().map(|x| JsonValue::String(x.hash().to_string())).collect();
171
172        JsonResponse::new(JsonValue::Array(pending_txs), id).into()
173    }
174
175    // RPCAPI:
176    // Queries the node pending transactions store to reset all
177    // transactions. Unproposed transactions are removed.
178    // Returns `true` if the operation was successful, otherwise, a
179    // corresponding error.
180    //
181    // --> {"jsonrpc": "2.0", "method": "tx.clean_pending", "params": [], "id": 1}
182    // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
183    pub async fn tx_clean_pending(&self, id: u16, params: JsonValue) -> JsonResult {
184        let Some(params) = params.get::<Vec<JsonValue>>() else {
185            return JsonError::new(InvalidParams, None, id).into()
186        };
187        if !params.is_empty() {
188            return JsonError::new(InvalidParams, None, id).into()
189        }
190
191        if !*self.validator.synced.read().await {
192            error!(target: "darkfid::rpc::tx_clean_pending", "Blockchain is not synced");
193            return server_error(RpcError::NotSynced, id, None)
194        }
195
196        // Grab node registry locks
197        let submit_lock = self.registry.submit_lock.write().await;
198        let block_templates = self.registry.block_templates.write().await;
199        let jobs = self.registry.jobs.write().await;
200        let mm_jobs = self.registry.mm_jobs.write().await;
201
202        // Purge all unproposed pending transactions from the database
203        let result = self
204            .validator
205            .consensus
206            .purge_unproposed_pending_txs(self.registry.proposed_transactions(&block_templates))
207            .await;
208
209        // Release registry locks
210        drop(block_templates);
211        drop(jobs);
212        drop(mm_jobs);
213        drop(submit_lock);
214
215        // Check result
216        if let Err(e) = result {
217            error!(target: "darkfid::rpc::tx_clean_pending", "Failed removing pending txs: {e}");
218            return JsonError::new(InternalError, None, id).into()
219        };
220
221        JsonResponse::new(JsonValue::Boolean(true), id).into()
222    }
223
224    // RPCAPI:
225    // Compute provided transaction's total gas, against current best fork.
226    // Returns the gas value if the transaction is valid, otherwise, a corresponding
227    // error.
228    //
229    // --> {"jsonrpc": "2.0", "method": "tx.calculate_fee", "params": ["base64encodedTX", "include_fee"], "id": 1}
230    // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
231    pub async fn tx_calculate_fee(&self, id: u16, params: JsonValue) -> JsonResult {
232        let Some(params) = params.get::<Vec<JsonValue>>() else {
233            return JsonError::new(InvalidParams, None, id).into()
234        };
235        if params.len() != 2 || !params[0].is_string() || !params[1].is_bool() {
236            return JsonError::new(InvalidParams, None, id).into()
237        }
238
239        if !*self.validator.synced.read().await {
240            error!(target: "darkfid::rpc::tx_calculate_fee", "Blockchain is not synced");
241            return server_error(RpcError::NotSynced, id, None)
242        }
243
244        // Try to deserialize the transaction
245        let tx_enc = params[0].get::<String>().unwrap().trim();
246        let tx_bytes = match base64::decode(tx_enc) {
247            Some(v) => v,
248            None => {
249                error!(target: "darkfid::rpc::tx_calculate_fee", "Failed decoding base64 transaction");
250                return server_error(RpcError::ParseError, id, None)
251            }
252        };
253
254        let tx: Transaction = match deserialize_async(&tx_bytes).await {
255            Ok(v) => v,
256            Err(e) => {
257                error!(target: "darkfid::rpc::tx_calculate_fee", "Failed deserializing bytes into Transaction: {e}");
258                return server_error(RpcError::ParseError, id, None)
259            }
260        };
261
262        // Parse the include fee flag
263        let include_fee = params[1].get::<bool>().unwrap();
264
265        // Simulate state transition
266        let result = self.validator.calculate_fee(&tx, *include_fee).await;
267        if result.is_err() {
268            error!(
269                target: "darkfid::rpc::tx_calculate_fee", "Failed to validate state transition: {}",
270                result.err().unwrap()
271            );
272            return server_error(RpcError::TxGasCalculationFail, id, None)
273        };
274
275        JsonResponse::new(JsonValue::Number(result.unwrap() as f64), id).into()
276    }
277}