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        let mut validator = self.validator.write().await;
52        if !validator.synced {
53            error!(target: "darkfid::rpc::tx_simulate", "Blockchain is not synced");
54            return server_error(RpcError::NotSynced, id, None)
55        }
56
57        // Try to deserialize the transaction
58        let tx_enc = params[0].get::<String>().unwrap().trim();
59        let tx_bytes = match base64::decode(tx_enc) {
60            Some(v) => v,
61            None => {
62                error!(target: "darkfid::rpc::tx_simulate", "Failed decoding base64 transaction");
63                return server_error(RpcError::ParseError, id, None)
64            }
65        };
66
67        let tx: Transaction = match deserialize_async(&tx_bytes).await {
68            Ok(v) => v,
69            Err(e) => {
70                error!(target: "darkfid::rpc::tx_simulate", "Failed deserializing bytes into Transaction: {e}");
71                return server_error(RpcError::ParseError, id, None)
72            }
73        };
74
75        // Simulate state transition
76        if let Err(e) = validator.append_tx(&tx, false).await {
77            error!(target: "darkfid::rpc::tx_simulate", "Failed to validate state transition: {e}");
78            return server_error(RpcError::TxSimulationFail, id, None)
79        };
80
81        JsonResponse::new(JsonValue::Boolean(true), id).into()
82    }
83
84    // RPCAPI:
85    // Append a given transaction to the mempool and broadcast it to
86    // the P2P network. The function will first simulate the state
87    // transition in order to see if the transaction is actually valid,
88    // and in turn it will return an error if this is the case.
89    // Otherwise, a transaction ID will be returned.
90    //
91    // --> {"jsonrpc": "2.0", "method": "tx.broadcast", "params": ["base64encodedTX"], "id": 1}
92    // <-- {"jsonrpc": "2.0", "result": "txID...", "id": 1}
93    pub async fn tx_broadcast(&self, id: u16, params: JsonValue) -> JsonResult {
94        let Some(params) = params.get::<Vec<JsonValue>>() else {
95            return JsonError::new(InvalidParams, None, id).into()
96        };
97        if params.len() != 1 || !params[0].is_string() {
98            return JsonError::new(InvalidParams, None, id).into()
99        }
100
101        let mut validator = self.validator.write().await;
102        if !validator.synced {
103            error!(target: "darkfid::rpc::tx_broadcast", "Blockchain is not synced");
104            return server_error(RpcError::NotSynced, id, None)
105        }
106
107        // Try to deserialize the transaction
108        let tx_enc = params[0].get::<String>().unwrap().trim();
109        let tx_bytes = match base64::decode(tx_enc) {
110            Some(v) => v,
111            None => {
112                error!(target: "darkfid::rpc::tx_broadcast", "Failed decoding base64 transaction");
113                return server_error(RpcError::ParseError, id, None)
114            }
115        };
116
117        let tx: Transaction = match deserialize_async(&tx_bytes).await {
118            Ok(v) => v,
119            Err(e) => {
120                error!(target: "darkfid::rpc::tx_broadcast", "Failed deserializing bytes into Transaction: {e}");
121                return server_error(RpcError::ParseError, id, None)
122            }
123        };
124
125        // We'll perform the state transition check here.
126        if let Err(e) = validator.append_tx(&tx, true).await {
127            error!(target: "darkfid::rpc::tx_broadcast", "Failed to append transaction to mempool: {e}");
128            return server_error(RpcError::TxSimulationFail, id, None)
129        };
130
131        self.p2p_handler.p2p.broadcast(&tx).await;
132        if !self.p2p_handler.p2p.is_connected() {
133            warn!(target: "darkfid::rpc::tx_broadcast", "No connected channels to broadcast tx");
134        }
135
136        let tx_hash = tx.hash().to_string();
137        JsonResponse::new(JsonValue::String(tx_hash), id).into()
138    }
139
140    // RPCAPI:
141    // Queries the node pending transactions store to retrieve all transactions.
142    // Returns a vector of hex-encoded transaction hashes.
143    //
144    // --> {"jsonrpc": "2.0", "method": "tx.pending", "params": [], "id": 1}
145    // <-- {"jsonrpc": "2.0", "result": ["TxHash" , "..."], "id": 1}
146    pub async fn tx_pending(&self, id: u16, params: JsonValue) -> JsonResult {
147        let Some(params) = params.get::<Vec<JsonValue>>() else {
148            return JsonError::new(InvalidParams, None, id).into()
149        };
150        if !params.is_empty() {
151            return JsonError::new(InvalidParams, None, id).into()
152        }
153
154        let validator = self.validator.read().await;
155        if !validator.synced {
156            error!(target: "darkfid::rpc::tx_pending", "Blockchain is not synced");
157            return server_error(RpcError::NotSynced, id, None)
158        }
159
160        let pending_txs = match validator.blockchain.get_pending_txs() {
161            Ok(v) => v,
162            Err(e) => {
163                error!(target: "darkfid::rpc::tx_pending", "Failed fetching pending txs: {e}");
164                return JsonError::new(InternalError, None, id).into()
165            }
166        };
167
168        let pending_txs: Vec<JsonValue> =
169            pending_txs.iter().map(|x| JsonValue::String(x.hash().to_string())).collect();
170
171        JsonResponse::new(JsonValue::Array(pending_txs), id).into()
172    }
173
174    // RPCAPI:
175    // Queries the node pending transactions store to reset all
176    // transactions. Unproposed transactions are removed.
177    // Returns `true` if the operation was successful, otherwise, a
178    // corresponding error.
179    //
180    // --> {"jsonrpc": "2.0", "method": "tx.clean_pending", "params": [], "id": 1}
181    // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
182    pub async fn tx_clean_pending(&self, id: u16, params: JsonValue) -> JsonResult {
183        let Some(params) = params.get::<Vec<JsonValue>>() else {
184            return JsonError::new(InvalidParams, None, id).into()
185        };
186        if !params.is_empty() {
187            return JsonError::new(InvalidParams, None, id).into()
188        }
189
190        let mut validator = self.validator.write().await;
191        if !validator.synced {
192            error!(target: "darkfid::rpc::tx_clean_pending", "Blockchain is not synced");
193            return server_error(RpcError::NotSynced, id, None)
194        }
195
196        // Retrieve registry transactions
197        let registry_txs = self.registry.state.read().await.proposed_transactions();
198
199        // Purge all unproposed pending transactions from the database
200        if let Err(e) = validator.consensus.purge_unproposed_pending_txs(registry_txs).await {
201            error!(target: "darkfid::rpc::tx_clean_pending", "Failed removing pending txs: {e}");
202            return JsonError::new(InternalError, None, id).into()
203        };
204
205        JsonResponse::new(JsonValue::Boolean(true), id).into()
206    }
207
208    // RPCAPI:
209    // Compute provided transaction's total gas, against current best fork.
210    // Returns the gas value if the transaction is valid, otherwise, a corresponding
211    // error.
212    //
213    // --> {"jsonrpc": "2.0", "method": "tx.calculate_fee", "params": ["base64encodedTX", "include_fee"], "id": 1}
214    // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
215    pub async fn tx_calculate_fee(&self, id: u16, params: JsonValue) -> JsonResult {
216        let Some(params) = params.get::<Vec<JsonValue>>() else {
217            return JsonError::new(InvalidParams, None, id).into()
218        };
219        if params.len() != 2 || !params[0].is_string() || !params[1].is_bool() {
220            return JsonError::new(InvalidParams, None, id).into()
221        }
222
223        let validator = self.validator.read().await;
224        if !validator.synced {
225            error!(target: "darkfid::rpc::tx_calculate_fee", "Blockchain is not synced");
226            return server_error(RpcError::NotSynced, id, None)
227        }
228
229        // Try to deserialize the transaction
230        let tx_enc = params[0].get::<String>().unwrap().trim();
231        let tx_bytes = match base64::decode(tx_enc) {
232            Some(v) => v,
233            None => {
234                error!(target: "darkfid::rpc::tx_calculate_fee", "Failed decoding base64 transaction");
235                return server_error(RpcError::ParseError, id, None)
236            }
237        };
238
239        let tx: Transaction = match deserialize_async(&tx_bytes).await {
240            Ok(v) => v,
241            Err(e) => {
242                error!(target: "darkfid::rpc::tx_calculate_fee", "Failed deserializing bytes into Transaction: {e}");
243                return server_error(RpcError::ParseError, id, None)
244            }
245        };
246
247        // Parse the include fee flag
248        let include_fee = params[1].get::<bool>().unwrap();
249
250        // Simulate state transition
251        let result = validator.calculate_fee(&tx, *include_fee).await;
252        if result.is_err() {
253            error!(
254                target: "darkfid::rpc::tx_calculate_fee", "Failed to validate state transition: {}",
255                result.err().unwrap()
256            );
257            return server_error(RpcError::TxGasCalculationFail, id, None)
258        };
259
260        JsonResponse::new(JsonValue::Number(result.unwrap() as f64), id).into()
261    }
262}