• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stacks-network / stacks-core / 25903914664-1

15 May 2026 06:28AM UTC coverage: 47.122% (-38.8%) from 85.959%
25903914664-1

Pull #7199

github

94e391
web-flow
Merge 109f2828c into 1c7b8e6ac
Pull Request #7199: Feat: L1 and L2 early unlocks, updating signer

103343 of 219309 relevant lines covered (47.12%)

12880462.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.04
/stackslib/src/net/api/callreadonly.rs
1
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
2
// Copyright (C) 2020-2026 Stacks Open Internet Foundation
3
//
4
// This program is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// This program is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

17
use clarity::vm::analysis::RuntimeCheckErrorKind;
18
use clarity::vm::ast::parser::v1::CLARITY_NAME_REGEX;
19
use clarity::vm::clarity::ClarityConnection;
20
use clarity::vm::costs::{ExecutionCost, LimitedCostTracker};
21
use clarity::vm::errors::ClarityEvalError;
22
use clarity::vm::errors::VmExecutionError::{self, RuntimeCheck};
23
use clarity::vm::representations::{CONTRACT_NAME_REGEX_STRING, STANDARD_PRINCIPAL_REGEX_STRING};
24
use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier};
25
use clarity::vm::{ClarityName, ContractName, SymbolicExpression, Value};
26
use regex::{Captures, Regex};
27
use stacks_common::types::chainstate::StacksAddress;
28
use stacks_common::types::net::PeerHost;
29

30
use crate::net::http::{
31
    parse_json, Error, HttpContentType, HttpNotFound, HttpRequest, HttpRequestContents,
32
    HttpRequestPreamble, HttpResponse, HttpResponseContents, HttpResponsePayload,
33
    HttpResponsePreamble,
34
};
35
use crate::net::httpcore::{
36
    request, HttpRequestContentsExtensions as _, RPCRequestHandler, StacksHttpRequest,
37
    StacksHttpResponse,
38
};
39
use crate::net::{Error as NetError, StacksNodeState, TipRequest};
40

41
#[derive(Clone, Serialize, Deserialize)]
42
pub struct CallReadOnlyRequestBody {
43
    pub sender: String,
44
    #[serde(skip_serializing_if = "Option::is_none")]
45
    pub sponsor: Option<String>,
46
    pub arguments: Vec<String>,
47
}
48

49
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
50
pub struct CallReadOnlyResponse {
51
    pub okay: bool,
52
    #[serde(default)]
53
    #[serde(skip_serializing_if = "Option::is_none")]
54
    pub result: Option<String>,
55
    #[serde(default)]
56
    #[serde(skip_serializing_if = "Option::is_none")]
57
    pub cause: Option<String>,
58
}
59

60
#[derive(Clone)]
61
pub struct RPCCallReadOnlyRequestHandler {
62
    pub maximum_call_argument_size: u32,
63
    read_only_call_limit: ExecutionCost,
64

65
    /// Runtime fields
66
    pub contract_identifier: Option<QualifiedContractIdentifier>,
67
    pub function: Option<ClarityName>,
68
    pub sender: Option<PrincipalData>,
69
    pub sponsor: Option<PrincipalData>,
70
    pub arguments: Option<Vec<Value>>,
71
}
72

73
impl RPCCallReadOnlyRequestHandler {
74
    pub fn new(maximum_call_argument_size: u32, read_only_call_limit: ExecutionCost) -> Self {
2,118,118✔
75
        Self {
2,118,118✔
76
            maximum_call_argument_size,
2,118,118✔
77
            read_only_call_limit,
2,118,118✔
78
            contract_identifier: None,
2,118,118✔
79
            function: None,
2,118,118✔
80
            sender: None,
2,118,118✔
81
            sponsor: None,
2,118,118✔
82
            arguments: None,
2,118,118✔
83
        }
2,118,118✔
84
    }
2,118,118✔
85
}
86

87
/// Decode the HTTP request
88
impl HttpRequest for RPCCallReadOnlyRequestHandler {
89
    fn verb(&self) -> &'static str {
1,059,059✔
90
        "POST"
1,059,059✔
91
    }
1,059,059✔
92

93
    fn path_regex(&self) -> Regex {
2,118,118✔
94
        Regex::new(&format!(
2,118,118✔
95
            "^/v2/contracts/call-read/(?P<address>{})/(?P<contract>{})/(?P<function>{})$",
2,118,118✔
96
            *STANDARD_PRINCIPAL_REGEX_STRING, *CONTRACT_NAME_REGEX_STRING, *CLARITY_NAME_REGEX
2,118,118✔
97
        ))
2,118,118✔
98
        .unwrap()
2,118,118✔
99
    }
2,118,118✔
100

101
    fn metrics_identifier(&self) -> &str {
20,295✔
102
        "/v2/contracts/call-read/:principal/:contract_name/:func_name"
20,295✔
103
    }
20,295✔
104

105
    /// Try to decode this request.
106
    fn try_parse_request(
20,295✔
107
        &mut self,
20,295✔
108
        preamble: &HttpRequestPreamble,
20,295✔
109
        captures: &Captures,
20,295✔
110
        query: Option<&str>,
20,295✔
111
        body: &[u8],
20,295✔
112
    ) -> Result<HttpRequestContents, Error> {
20,295✔
113
        let content_len = preamble.get_content_length();
20,295✔
114
        if !(content_len > 0 && content_len < self.maximum_call_argument_size) {
20,295✔
115
            return Err(Error::DecodeError(format!(
×
116
                "Invalid Http request: invalid body length for CallReadOnly ({})",
×
117
                content_len
×
118
            )));
×
119
        }
20,295✔
120

121
        if preamble.content_type != Some(HttpContentType::JSON) {
20,295✔
122
            return Err(Error::DecodeError(
×
123
                "Invalid content-type: expected application/json".to_string(),
×
124
            ));
×
125
        }
20,295✔
126

127
        let contract_identifier = request::get_contract_address(captures, "address", "contract")?;
20,295✔
128
        let function = request::get_clarity_name(captures, "function")?;
20,295✔
129
        let body: CallReadOnlyRequestBody = serde_json::from_slice(body)
20,295✔
130
            .map_err(|_e| Error::DecodeError("Failed to parse JSON body".into()))?;
20,295✔
131

132
        let sender = PrincipalData::parse(&body.sender)
20,295✔
133
            .map_err(|_e| Error::DecodeError("Failed to parse sender principal".into()))?;
20,295✔
134

135
        let sponsor = if let Some(sponsor) = body.sponsor {
20,295✔
136
            Some(
137
                PrincipalData::parse(&sponsor)
×
138
                    .map_err(|_e| Error::DecodeError("Failed to parse sponsor principal".into()))?,
×
139
            )
140
        } else {
141
            None
20,295✔
142
        };
143

144
        // arguments must be valid Clarity values
145
        let arguments = body
20,295✔
146
            .arguments
20,295✔
147
            .into_iter()
20,295✔
148
            .map(|hex| Value::try_deserialize_hex_untyped(&hex).ok())
20,295✔
149
            .collect::<Option<Vec<Value>>>()
20,295✔
150
            .ok_or_else(|| Error::DecodeError("Failed to deserialize argument value".into()))?;
20,295✔
151

152
        self.contract_identifier = Some(contract_identifier);
20,295✔
153
        self.function = Some(function);
20,295✔
154
        self.sender = Some(sender);
20,295✔
155
        self.sponsor = sponsor;
20,295✔
156
        self.arguments = Some(arguments);
20,295✔
157

158
        Ok(HttpRequestContents::new().query_string(query))
20,295✔
159
    }
20,295✔
160
}
161

162
/// Handle the HTTP request
163
impl RPCRequestHandler for RPCCallReadOnlyRequestHandler {
164
    /// Reset internal state
165
    fn restart(&mut self) {
20,295✔
166
        self.contract_identifier = None;
20,295✔
167
        self.function = None;
20,295✔
168
        self.sender = None;
20,295✔
169
        self.sponsor = None;
20,295✔
170
        self.arguments = None;
20,295✔
171
    }
20,295✔
172

173
    /// Make the response
174
    fn try_handle_request(
20,295✔
175
        &mut self,
20,295✔
176
        preamble: HttpRequestPreamble,
20,295✔
177
        contents: HttpRequestContents,
20,295✔
178
        node: &mut StacksNodeState,
20,295✔
179
    ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> {
20,295✔
180
        let tip = match node.load_stacks_chain_tip(&preamble, &contents) {
20,295✔
181
            Ok(tip) => tip,
20,295✔
182
            Err(error_resp) => {
×
183
                return error_resp.try_into_contents().map_err(NetError::from);
×
184
            }
185
        };
186

187
        let contract_identifier = self
20,295✔
188
            .contract_identifier
20,295✔
189
            .take()
20,295✔
190
            .ok_or(NetError::SendError("Missing `contract_identifier`".into()))?;
20,295✔
191
        let function = self
20,295✔
192
            .function
20,295✔
193
            .take()
20,295✔
194
            .ok_or(NetError::SendError("Missing `function`".into()))?;
20,295✔
195
        let sender = self
20,295✔
196
            .sender
20,295✔
197
            .take()
20,295✔
198
            .ok_or(NetError::SendError("Missing `sender`".into()))?;
20,295✔
199
        let sponsor = self.sponsor.clone();
20,295✔
200
        let arguments = self
20,295✔
201
            .arguments
20,295✔
202
            .take()
20,295✔
203
            .ok_or(NetError::SendError("Missing `arguments`".into()))?;
20,295✔
204

205
        // run the read-only call
206
        let data_resp =
20,295✔
207
            node.with_node_state(|_network, sortdb, chainstate, _mempool, _rpc_args| {
20,295✔
208
                let args: Vec<_> = arguments
20,295✔
209
                    .iter()
20,295✔
210
                    .map(|x| SymbolicExpression::atom_value(x.clone()))
20,295✔
211
                    .collect();
20,295✔
212

213
                let mainnet = chainstate.mainnet;
20,295✔
214
                let chain_id = chainstate.chain_id;
20,295✔
215
                let mut cost_limit = self.read_only_call_limit.clone();
20,295✔
216
                cost_limit.write_length = 0;
20,295✔
217
                cost_limit.write_count = 0;
20,295✔
218

219
                chainstate.maybe_read_only_clarity_tx(
20,295✔
220
                    &sortdb.index_handle_at_block(chainstate, &tip)?,
20,295✔
221
                    &tip,
20,295✔
222
                    |clarity_tx| {
20,295✔
223
                        let epoch = clarity_tx.get_epoch();
20,295✔
224
                        let cost_track = clarity_tx
20,295✔
225
                            .with_clarity_db_readonly(|clarity_db| {
20,295✔
226
                                LimitedCostTracker::new_mid_block(
20,295✔
227
                                    mainnet, chain_id, cost_limit, clarity_db, epoch,
20,295✔
228
                                )
229
                            })
20,295✔
230
                            .map_err(VmExecutionError::from)?;
20,295✔
231

232
                        clarity_tx.with_readonly_clarity_env(
20,295✔
233
                            mainnet,
20,295✔
234
                            chain_id,
20,295✔
235
                            sender,
20,295✔
236
                            sponsor,
20,295✔
237
                            cost_track,
20,295✔
238
                            |exec_state, invoke_ctx| {
20,295✔
239
                                // we want to execute any function as long as no actual writes are made as
240
                                // opposed to be limited to purely calling `define-read-only` functions,
241
                                // so use `read_only = false`.  This broadens the number of functions that
242
                                // can be called, and also circumvents limitations on `define-read-only`
243
                                // functions that can not use `contrac-call?`, even when calling other
244
                                // read-only functions
245
                                exec_state
20,295✔
246
                                    .execute_contract(
20,295✔
247
                                        invoke_ctx,
20,295✔
248
                                        &contract_identifier,
20,295✔
249
                                        function.as_str(),
20,295✔
250
                                        &args,
20,295✔
251
                                        false,
252
                                    )
253
                                    .map_err(ClarityEvalError::from)
20,295✔
254
                            },
20,295✔
255
                        )
256
                    },
20,295✔
257
                )
258
            });
20,295✔
259

260
        // decode the response
261
        let data_resp = match data_resp {
20,295✔
262
            Ok(Some(Ok(data))) => {
20,259✔
263
                let hex_result = data
20,259✔
264
                    .serialize_to_hex()
20,259✔
265
                    .map_err(|e| NetError::SerializeError(format!("{:?}", &e)))?;
20,259✔
266

267
                CallReadOnlyResponse {
20,259✔
268
                    okay: true,
20,259✔
269
                    result: Some(format!("0x{}", hex_result)),
20,259✔
270
                    cause: None,
20,259✔
271
                }
20,259✔
272
            }
273
            Ok(Some(Err(e))) => match e {
36✔
274
                ClarityEvalError::Vm(RuntimeCheck(RuntimeCheckErrorKind::CostBalanceExceeded(
275
                    actual_cost,
18✔
276
                    _,
277
                ))) if actual_cost.write_count > 0 => CallReadOnlyResponse {
18✔
278
                    okay: false,
18✔
279
                    result: None,
18✔
280
                    cause: Some("NotReadOnly".to_string()),
18✔
281
                },
18✔
282
                _ => CallReadOnlyResponse {
18✔
283
                    okay: false,
18✔
284
                    result: None,
18✔
285
                    cause: Some(e.to_string()),
18✔
286
                },
18✔
287
            },
288
            Ok(None) | Err(_) => {
289
                return StacksHttpResponse::new_error(
×
290
                    &preamble,
×
291
                    &HttpNotFound::new("Chain tip not found".to_string()),
×
292
                )
293
                .try_into_contents()
×
294
                .map_err(NetError::from);
×
295
            }
296
        };
297

298
        let preamble = HttpResponsePreamble::ok_json(&preamble);
20,295✔
299
        let body = HttpResponseContents::try_from_json(&data_resp)?;
20,295✔
300
        Ok((preamble, body))
20,295✔
301
    }
20,295✔
302
}
303

304
/// Decode the HTTP response
305
impl HttpResponse for RPCCallReadOnlyRequestHandler {
306
    fn try_parse_response(
×
307
        &self,
×
308
        preamble: &HttpResponsePreamble,
×
309
        body: &[u8],
×
310
    ) -> Result<HttpResponsePayload, Error> {
×
311
        let map_entry: CallReadOnlyResponse = parse_json(preamble, body)?;
×
312
        Ok(HttpResponsePayload::try_from_json(map_entry)?)
×
313
    }
×
314
}
315

316
impl StacksHttpRequest {
317
    /// Make a new request to run a read-only function
318
    pub fn new_callreadonlyfunction(
×
319
        host: PeerHost,
×
320
        contract_addr: StacksAddress,
×
321
        contract_name: ContractName,
×
322
        sender: PrincipalData,
×
323
        sponsor: Option<PrincipalData>,
×
324
        function_name: ClarityName,
×
325
        function_args: Vec<Value>,
×
326
        tip_req: TipRequest,
×
327
    ) -> StacksHttpRequest {
×
328
        StacksHttpRequest::new_for_peer(
×
329
            host,
×
330
            "POST".into(),
×
331
            format!(
×
332
                "/v2/contracts/call-read/{}/{}/{}",
333
                &contract_addr, &contract_name, &function_name
×
334
            ),
335
            HttpRequestContents::new().for_tip(tip_req).payload_json(
×
336
                serde_json::to_value(CallReadOnlyRequestBody {
×
337
                    sender: sender.to_string(),
×
338
                    sponsor: sponsor.map(|s| s.to_string()),
×
339
                    arguments: function_args.into_iter().map(|v| v.to_string()).collect(),
×
340
                })
341
                .expect("FATAL: failed to encode infallible data"),
×
342
            ),
343
        )
344
        .expect("FATAL: failed to construct request from infallible data")
×
345
    }
×
346
}
347

348
impl StacksHttpResponse {
349
    pub fn decode_call_readonly_response(self) -> Result<CallReadOnlyResponse, NetError> {
×
350
        let contents = self.get_http_payload_ok()?;
×
351
        let contents_json: serde_json::Value = contents.try_into()?;
×
352
        let resp: CallReadOnlyResponse = serde_json::from_value(contents_json)
×
353
            .map_err(|_e| NetError::DeserializeError("Failed to load from JSON".to_string()))?;
×
354
        Ok(resp)
×
355
    }
×
356
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc