explorerd/rpc/
contracts.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2025 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 std::str::FromStr;
20
21use tinyjson::JsonValue;
22
23use darkfi::{
24    rpc::jsonrpc::{parse_json_array_string, validate_empty_params},
25    Result,
26};
27use darkfi_sdk::crypto::ContractId;
28
29use crate::{error::ExplorerdError, Explorerd};
30
31impl Explorerd {
32    // RPCAPI:
33    // Retrieves the native contracts deployed in the DarkFi network.
34    // Returns a JSON array containing Contract IDs along with their associated metadata upon success.
35    //
36    // **Params:**
37    // * `None`
38    //
39    // **Returns:**
40    // * Array of `ContractRecord`s encoded into a JSON.
41    //
42    // **Example API Usage:**
43    // --> {"jsonrpc": "2.0", "method": "contracts.get_native_contracts", "params": ["5cc...2f9"], "id": 1}
44    // <-- {"jsonrpc": "2.0", "result": ["BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o", "Money Contract", "The money contract..."], "id": 1}
45    pub async fn contracts_get_native_contracts(&self, params: &JsonValue) -> Result<JsonValue> {
46        // Validate that no parameters are provided
47        validate_empty_params(params)?;
48
49        // Retrieve native contracts
50        let contract_records = self.service.get_native_contracts()?;
51
52        // Transform contract records into a JSON array and return result
53        if contract_records.is_empty() {
54            Ok(JsonValue::Array(vec![]))
55        } else {
56            let json_blocks: Vec<JsonValue> = contract_records
57                .into_iter()
58                .map(|contract_record| contract_record.to_json_array())
59                .collect();
60            Ok(JsonValue::Array(json_blocks))
61        }
62    }
63
64    // RPCAPI:
65    // Retrieves the source code paths for the contract associated with the specified Contract ID.
66    // Returns a JSON array containing the source code paths upon success.
67    //
68    // **Params:**
69    // * `array[0]`: `String` Contract ID
70    //
71    // **Returns:**
72    // * `JsonArray` containing source code paths for the specified Contract ID.
73    //
74    // **Example API Usage:**
75    // --> {"jsonrpc": "2.0", "method": "contracts.get_contract_source_code_paths", "params": ["BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o"], "id": 1}
76    // <-- {"jsonrpc": "2.0", "result": ["path/to/source1.rs", "path/to/source2.rs"], "id": 1}
77    pub async fn contracts_get_contract_source_code_paths(
78        &self,
79        params: &JsonValue,
80    ) -> Result<JsonValue> {
81        // Extract contract ID
82        let contact_id_str = parse_json_array_string("contract_id", 0, params)?;
83
84        // Convert the contract string to a `ContractId` instance
85        let contract_id = ContractId::from_str(&contact_id_str)
86            .map_err(|_| ExplorerdError::InvalidContractId(contact_id_str))?;
87
88        // Retrieve source code paths for the contract
89        let paths = self.service.get_contract_source_paths(&contract_id)?;
90
91        // Tranform found paths into `JsonValues`
92        let json_value_paths = paths.iter().map(|path| JsonValue::String(path.clone())).collect();
93
94        Ok(JsonValue::Array(json_value_paths))
95    }
96
97    // RPCAPI:
98    // Retrieves contract source code content using the provided Contract ID and source path.
99    // Returns the source code content as a JSON string upon success.
100    //
101    // **Params:**
102    // * `array[0]`: `String` Contract ID
103    // * `array[1]`: `String` Source path
104    //
105    // **Returns:**
106    // * `String` containing the content of the contract source file.
107    //
108    // **Example API Usage:**
109    // --> {"jsonrpc": "2.0", "method": "contracts.get_contract_source", "params": ["BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o", "client/lib.rs"], "id": 1}
110    // <-- {"jsonrpc": "2.0", "result": "/* This file is ...", "id": 1}
111    pub async fn contracts_get_contract_source(&self, params: &JsonValue) -> Result<JsonValue> {
112        // Extract the contract ID
113        let contact_id_str = parse_json_array_string("contract_id", 0, params)?;
114
115        // Convert the contract string to a `ContractId` instance
116        let contract_id = ContractId::from_str(&contact_id_str)
117            .map_err(|_| ExplorerdError::InvalidContractId(contact_id_str))?;
118
119        // Extract the source path
120        let source_path = parse_json_array_string("source_path", 1, params)?;
121
122        // Retrieve the contract source code, transform it into a `JsonValue`, and return the result
123        match self.service.get_contract_source_content(&contract_id, &source_path)? {
124            Some(source_file) => Ok(JsonValue::String(source_file)),
125            None => Ok(JsonValue::from(std::collections::HashMap::<String, JsonValue>::new())),
126        }
127    }
128}
129
130#[cfg(test)]
131/// Test module for validating the functionality of RPC methods related to explorer contracts.
132/// Focuses on ensuring proper error handling for invalid parameters across several use cases,
133/// including cases with missing values, unsupported types, and unparsable inputs.
134mod tests {
135
136    use tinyjson::JsonValue;
137
138    use darkfi::rpc::jsonrpc::ErrorCode;
139
140    use crate::test_utils::{
141        setup, validate_empty_rpc_parameters, validate_invalid_rpc_contract_id,
142        validate_invalid_rpc_parameter,
143    };
144
145    #[test]
146    /// Tests the `contracts.get_native_contracts` method to ensure it correctly handles cases where
147    /// empty parameters are supplied, returning an expected result or error response.
148    fn test_contracts_get_native_contracts_empty_params() {
149        smol::block_on(async {
150            validate_empty_rpc_parameters(&setup(), "contracts.get_native_contracts").await;
151        });
152    }
153
154    #[test]
155    /// Tests the `contracts.get_contract_source_code_paths` method to ensure it correctly handles cases
156    /// with invalid or missing `contract_id` parameters, returning appropriate error responses.
157    fn test_contracts_get_contract_source_code_paths_invalid_params() {
158        validate_invalid_rpc_contract_id(&setup(), "contracts.get_contract_source_code_paths");
159    }
160
161    #[test]
162    /// Tests the `contracts.get_contract_source` method to ensure it correctly handles cases
163    /// with invalid or missing parameters, returning appropriate error responses.
164    fn test_contracts_get_contract_source_invalid_params() {
165        let test_method = "contracts.get_contract_source";
166        let parameter_name = "source_path";
167
168        smol::block_on(async {
169            // Set up the explorerd instance
170            let explorerd = setup();
171
172            validate_invalid_rpc_contract_id(&explorerd, test_method);
173
174            // Test for missing `source_path` parameter
175            validate_invalid_rpc_parameter(
176                &explorerd,
177                test_method,
178                &[JsonValue::String("BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o".to_string())],
179                ErrorCode::InvalidParams.code(),
180                &format!("Parameter '{parameter_name}' at index 1 is missing"),
181            )
182            .await;
183
184            // Test for invalid `source_path` parameter
185            validate_invalid_rpc_parameter(
186                &explorerd,
187                test_method,
188                &[
189                    JsonValue::String("BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o".to_string()),
190                    JsonValue::Number(123.0), // Invalid `source_path` type
191                ],
192                ErrorCode::InvalidParams.code(),
193                &format!("Parameter '{parameter_name}' is not a valid string"),
194            )
195            .await;
196        });
197    }
198}