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        let result = validator.append_tx(&tx, false).await;
77
78        // Purge all unreferenced contract trees from the database
79        if let Err(e) = validator
80            .consensus
81            .purge_unreferenced_trees(&mut self.registry.state.read().await.new_trees())
82            .await
83        {
84            error!(target: "darkfid::rpc::tx_simulate", "Purging unreferenced contract trees from the database failed: {e}");
85            return JsonError::new(InternalError, None, id).into()
86        }
87
88        // Handle result
89        if let Err(e) = result {
90            error!(target: "darkfid::rpc::tx_simulate", "Failed to validate state transition: {e}");
91            return server_error(RpcError::TxSimulationFail, id, None)
92        };
93
94        JsonResponse::new(JsonValue::Boolean(true), id).into()
95    }
96
97    // RPCAPI:
98    // Append a given transaction to the mempool and broadcast it to
99    // the P2P network. The function will first simulate the state
100    // transition in order to see if the transaction is actually valid,
101    // and in turn it will return an error if this is the case.
102    // Otherwise, a transaction ID will be returned.
103    //
104    // --> {"jsonrpc": "2.0", "method": "tx.broadcast", "params": ["base64encodedTX"], "id": 1}
105    // <-- {"jsonrpc": "2.0", "result": "txID...", "id": 1}
106    pub async fn tx_broadcast(&self, id: u16, params: JsonValue) -> JsonResult {
107        let Some(params) = params.get::<Vec<JsonValue>>() else {
108            return JsonError::new(InvalidParams, None, id).into()
109        };
110        if params.len() != 1 || !params[0].is_string() {
111            return JsonError::new(InvalidParams, None, id).into()
112        }
113
114        let mut validator = self.validator.write().await;
115        if !validator.synced {
116            error!(target: "darkfid::rpc::tx_broadcast", "Blockchain is not synced");
117            return server_error(RpcError::NotSynced, id, None)
118        }
119
120        // Try to deserialize the transaction
121        let tx_enc = params[0].get::<String>().unwrap().trim();
122        let tx_bytes = match base64::decode(tx_enc) {
123            Some(v) => v,
124            None => {
125                error!(target: "darkfid::rpc::tx_broadcast", "Failed decoding base64 transaction");
126                return server_error(RpcError::ParseError, id, None)
127            }
128        };
129
130        let tx: Transaction = match deserialize_async(&tx_bytes).await {
131            Ok(v) => v,
132            Err(e) => {
133                error!(target: "darkfid::rpc::tx_broadcast", "Failed deserializing bytes into Transaction: {e}");
134                return server_error(RpcError::ParseError, id, None)
135            }
136        };
137
138        // We'll perform the state transition check here.
139        let result = validator.append_tx(&tx, true).await;
140
141        // Purge all unreferenced contract trees from the database
142        if let Err(e) = validator
143            .consensus
144            .purge_unreferenced_trees(&mut self.registry.state.read().await.new_trees())
145            .await
146        {
147            error!(target: "darkfid::rpc::tx_broadcast", "Purging unreferenced contract trees from the database failed: {e}");
148            return JsonError::new(InternalError, None, id).into()
149        }
150
151        // Handle result
152        if let Err(e) = result {
153            error!(target: "darkfid::rpc::tx_broadcast", "Failed to append transaction to mempool: {e}");
154            return server_error(RpcError::TxSimulationFail, id, None)
155        };
156
157        self.p2p_handler.p2p.broadcast(&tx).await;
158        if !self.p2p_handler.p2p.is_connected() {
159            warn!(target: "darkfid::rpc::tx_broadcast", "No connected channels to broadcast tx");
160        }
161
162        let tx_hash = tx.hash().to_string();
163        JsonResponse::new(JsonValue::String(tx_hash), id).into()
164    }
165
166    // RPCAPI:
167    // Queries the node pending transactions store to retrieve all transactions.
168    // Returns a vector of hex-encoded transaction hashes.
169    //
170    // --> {"jsonrpc": "2.0", "method": "tx.pending", "params": [], "id": 1}
171    // <-- {"jsonrpc": "2.0", "result": ["TxHash" , "..."], "id": 1}
172    pub async fn tx_pending(&self, id: u16, params: JsonValue) -> JsonResult {
173        let Some(params) = params.get::<Vec<JsonValue>>() else {
174            return JsonError::new(InvalidParams, None, id).into()
175        };
176        if !params.is_empty() {
177            return JsonError::new(InvalidParams, None, id).into()
178        }
179
180        let validator = self.validator.read().await;
181        if !validator.synced {
182            error!(target: "darkfid::rpc::tx_pending", "Blockchain is not synced");
183            return server_error(RpcError::NotSynced, id, None)
184        }
185
186        let pending_txs = match validator.blockchain.get_pending_txs() {
187            Ok(v) => v,
188            Err(e) => {
189                error!(target: "darkfid::rpc::tx_pending", "Failed fetching pending txs: {e}");
190                return JsonError::new(InternalError, None, id).into()
191            }
192        };
193
194        let pending_txs: Vec<JsonValue> =
195            pending_txs.iter().map(|x| JsonValue::String(x.hash().to_string())).collect();
196
197        JsonResponse::new(JsonValue::Array(pending_txs), id).into()
198    }
199
200    // RPCAPI:
201    // Queries the node pending transactions store to rebroadcast all
202    // transactions.
203    // Returns `true` if the operation was successful, otherwise, a
204    // corresponding error.
205    //
206    // --> {"jsonrpc": "2.0", "method": "tx.rebroadcast_pending", "params": [], "id": 1}
207    // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
208    pub async fn tx_rebroadcast_pending(&self, id: u16, params: JsonValue) -> JsonResult {
209        let Some(params) = params.get::<Vec<JsonValue>>() else {
210            return JsonError::new(InvalidParams, None, id).into()
211        };
212        if !params.is_empty() {
213            return JsonError::new(InvalidParams, None, id).into()
214        }
215
216        let validator = self.validator.read().await;
217        if !validator.synced {
218            error!(target: "darkfid::rpc::tx_rebroadcast_pending", "Blockchain is not synced");
219            return server_error(RpcError::NotSynced, id, None)
220        }
221
222        // Grab an iterator over pending transactions so we don't hold
223        // the validator lock.
224        let pending = validator.blockchain.transactions.pending.iter();
225        drop(validator);
226
227        // Rebroadcast all pending transactions
228        for value in pending.values() {
229            let value = match value {
230                Ok(v) => v,
231                Err(e) => {
232                    error!(target: "darkfid::rpc::tx_rebroadcast_pending", "Failed retrieving pending tx: {e}");
233                    return JsonError::new(InternalError, None, id).into()
234                }
235            };
236            let tx = match deserialize_async::<Transaction>(&value).await {
237                Ok(tx) => tx,
238                Err(e) => {
239                    error!(target: "darkfid::rpc::tx_rebroadcast_pending", "Failed deserialized pending tx: {e}");
240                    return JsonError::new(InternalError, None, id).into()
241                }
242            };
243            self.p2p_handler.p2p.broadcast(&tx).await;
244        }
245
246        JsonResponse::new(JsonValue::Boolean(true), id).into()
247    }
248
249    // RPCAPI:
250    // Queries the node pending transactions store to remove all
251    // transactions.
252    // Returns `true` if the operation was successful, otherwise, a
253    // corresponding error.
254    //
255    // --> {"jsonrpc": "2.0", "method": "tx.clean_pending", "params": [], "id": 1}
256    // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
257    pub async fn tx_clean_pending(&self, id: u16, params: JsonValue) -> JsonResult {
258        let Some(params) = params.get::<Vec<JsonValue>>() else {
259            return JsonError::new(InvalidParams, None, id).into()
260        };
261        if !params.is_empty() {
262            return JsonError::new(InvalidParams, None, id).into()
263        }
264
265        let validator = self.validator.write().await;
266        if !validator.synced {
267            error!(target: "darkfid::rpc::tx_clean_pending", "Blockchain is not synced");
268            return server_error(RpcError::NotSynced, id, None)
269        }
270
271        // Purge all pending transactions from the database
272        if let Err(e) = validator.blockchain.transactions.pending.clear() {
273            error!(target: "darkfid::rpc::tx_clean_pending", "Failed cleaning pending txs: {e}");
274            return JsonError::new(InternalError, None, id).into()
275        };
276
277        JsonResponse::new(JsonValue::Boolean(true), id).into()
278    }
279
280    // RPCAPI:
281    // Compute provided transaction's total gas, against current best fork.
282    // Returns the gas value if the transaction is valid, otherwise, a corresponding
283    // error.
284    //
285    // --> {"jsonrpc": "2.0", "method": "tx.calculate_fee", "params": ["base64encodedTX", "include_fee"], "id": 1}
286    // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
287    pub async fn tx_calculate_fee(&self, id: u16, params: JsonValue) -> JsonResult {
288        let Some(params) = params.get::<Vec<JsonValue>>() else {
289            return JsonError::new(InvalidParams, None, id).into()
290        };
291        if params.len() != 2 || !params[0].is_string() || !params[1].is_bool() {
292            return JsonError::new(InvalidParams, None, id).into()
293        }
294
295        let validator = self.validator.read().await;
296        if !validator.synced {
297            error!(target: "darkfid::rpc::tx_calculate_fee", "Blockchain is not synced");
298            return server_error(RpcError::NotSynced, id, None)
299        }
300
301        // Try to deserialize the transaction
302        let tx_enc = params[0].get::<String>().unwrap().trim();
303        let tx_bytes = match base64::decode(tx_enc) {
304            Some(v) => v,
305            None => {
306                error!(target: "darkfid::rpc::tx_calculate_fee", "Failed decoding base64 transaction");
307                return server_error(RpcError::ParseError, id, None)
308            }
309        };
310
311        let tx: Transaction = match deserialize_async(&tx_bytes).await {
312            Ok(v) => v,
313            Err(e) => {
314                error!(target: "darkfid::rpc::tx_calculate_fee", "Failed deserializing bytes into Transaction: {e}");
315                return server_error(RpcError::ParseError, id, None)
316            }
317        };
318
319        // Parse the include fee flag
320        let include_fee = params[1].get::<bool>().unwrap();
321
322        // Simulate state transition
323        let result = validator.calculate_fee(&tx, *include_fee).await;
324
325        // Purge all unreferenced contract trees from the database
326        if let Err(e) = validator
327            .consensus
328            .purge_unreferenced_trees(&mut self.registry.state.read().await.new_trees())
329            .await
330        {
331            error!(target: "darkfid::rpc::tx_calculate_fee", "Purging unreferenced contract trees from the database failed: {e}");
332            return JsonError::new(InternalError, None, id).into()
333        }
334
335        // Handle result
336        if let Err(e) = result {
337            error!(target: "darkfid::rpc::tx_calculate_fee", "Failed to validate state transition: {e}");
338            return server_error(RpcError::TxGasCalculationFail, id, None)
339        };
340
341        JsonResponse::new(JsonValue::Number(result.unwrap() as f64), id).into()
342    }
343}