diff --git a/docs/rpc-endpoints.md b/docs/rpc-endpoints.md
index 6163f27b75..117d74d0dc 100644
--- a/docs/rpc-endpoints.md
+++ b/docs/rpc-endpoints.md
@@ -172,6 +172,35 @@ Where data is the hex serialization of the variable value.
This endpoint also accepts a querystring parameter `?proof=` which when supplied `0`, will return the
JSON object _without_ the `proof` field.
+### GET /v2/clarity_marf_value/[Clarity MARF Key]
+Attempt to fetch the value of a MARF key. The key is identified with [Clarity MARF Key].
+
+Returns JSON data in the form:
+
+```json
+{
+ "data": "0x01ce...",
+ "proof": "0x01ab...",
+}
+```
+
+Where data is the hex serialization of the value.
+
+### GET /v2/clarity_metadata/[Stacks Address]/[Contract Name]/[Clarity Metadata Key]
+Attempt to fetch the metadata of a contract.
+ The contract is identified with [Stacks Address] and [Contract Name] in the URL path.
+ The metadata key is identified with [Clarity Metadata Key].
+
+Returns JSON data in the form:
+
+```json
+{
+ "data": "'{\"contract_identifier\":{...}'",
+}
+```
+
+Where data is the metadata formatted as a JSON string.
+
### GET /v2/constant_val/[Stacks Address]/[Contract Name]/[Constant Name]
Attempt to fetch a constant from a contract. The contract is identified with [Stacks Address] and
[Contract Name] in the URL path. The constant is identified with [Constant Name].
diff --git a/docs/rpc/api/core-node/get-clarity-marf-value.example.json b/docs/rpc/api/core-node/get-clarity-marf-value.example.json
new file mode 100644
index 0000000000..d0e233416f
--- /dev/null
+++ b/docs/rpc/api/core-node/get-clarity-marf-value.example.json
@@ -0,0 +1,4 @@
+{
+ "data": "0x0a0c000000010a6d6f6e737465722d69640100000000000000000000000000000001",
+ "proof": "0x123..."
+}
diff --git a/docs/rpc/api/core-node/get-clarity-marf-value.schema.json b/docs/rpc/api/core-node/get-clarity-marf-value.schema.json
new file mode 100644
index 0000000000..ea7e7894fb
--- /dev/null
+++ b/docs/rpc/api/core-node/get-clarity-marf-value.schema.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "description": "Response of get Clarity MARF value request",
+ "title": "ClarityMARFValueResponse",
+ "type": "object",
+ "required": ["data"],
+ "properties": {
+ "data": {
+ "type": "string",
+ "description": "Hex-encoded string"
+ },
+ "proof": {
+ "type": "string",
+ "description": "Hex-encoded string of the MARF proof for the data"
+ }
+ }
+}
diff --git a/docs/rpc/api/core-node/get-clarity-metadata.example.json b/docs/rpc/api/core-node/get-clarity-metadata.example.json
new file mode 100644
index 0000000000..5bb4bd5c47
--- /dev/null
+++ b/docs/rpc/api/core-node/get-clarity-metadata.example.json
@@ -0,0 +1,3 @@
+{
+ "data": "'{\"contract_identifier\":{...}, \"private_function_types\":{...}'"
+}
diff --git a/docs/rpc/api/core-node/get-clarity-metadata.schema.json b/docs/rpc/api/core-node/get-clarity-metadata.schema.json
new file mode 100644
index 0000000000..3c0104fa41
--- /dev/null
+++ b/docs/rpc/api/core-node/get-clarity-metadata.schema.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "description": "Response of get clarity metadata request",
+ "title": "ClarityMetadataResponse",
+ "type": "object",
+ "required": ["data"],
+ "properties": {
+ "data": {
+ "type": "string",
+ "description": "Metadata value formatted as a JSON string"
+ }
+ }
+}
diff --git a/docs/rpc/openapi.yaml b/docs/rpc/openapi.yaml
index f33e0dca73..e77fc31ade 100644
--- a/docs/rpc/openapi.yaml
+++ b/docs/rpc/openapi.yaml
@@ -519,7 +519,94 @@ paths:
description: |
The Stacks chain tip to query from.
If tip == "latest", the query will be run from the latest known tip (includes unconfirmed state).
- If the tip is left unspecified, the stacks chain tip will be selected (only includes confirmed state).
+ If the tip is left unspecified, the stacks chain tip will be selected (only includes confirmed state).
+
+ /v2/clarity_marf_value/{clarity_marf_key}:
+ post:
+ summary: Get the MARF value for a given key
+ tags:
+ - Smart Contracts
+ operationId: get_clarity_marf_value
+ description: |
+ Attempt to fetch the value of a MARF key. The key is identified with [Clarity MARF Key].
+
+ In the response, `data` is the hex serialization of the value.
+ responses:
+ 200:
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: ./api/core-node/get-clarity-marf-value.schema.json
+ example:
+ $ref: ./api/core-node/get-clarity-marf-value.example.json
+ 400:
+ description: Failed to retrieve MARF key
+ parameters:
+ - name: clarity_marf_key
+ in: path
+ required: true
+ description: MARF key
+ schema:
+ type: string
+ - name: proof
+ in: query
+ description: Returns object without the proof field when set to 0
+ schema:
+ type: integer
+ - name: tip
+ in: query
+ schema:
+ type: string
+ description: The Stacks chain tip to query from. If tip == latest, the query will be run from the latest
+ known tip (includes unconfirmed state).
+
+ /v2/clarity_metadata/{contract_address}/{contract_name}/{clarity_metadata_key}:
+ post:
+ summary: Get the contract metadata for the metadata key
+ tags:
+ - Smart Contracts
+ operationId: get_clarity_metadata_key
+ description: |
+ Attempt to fetch the metadata of a contract. The contract is identified with [Stacks Address] and [Contract Name] in the URL path. The metadata key is identified with [Clarity Metadata Key].
+
+ In the response, `data` is formatted as JSON.
+ responses:
+ 200:
+ description: Success
+ content:
+ application/json:
+ schema:
+ $ref: ./api/core-node/get-clarity-metadata.schema.json
+ example:
+ $ref: ./api/core-node/get-clarity-metadata.example.json
+ 400:
+ description: Failed to retrieve constant value from contract
+ parameters:
+ - name: contract_address
+ in: path
+ required: true
+ description: Stacks address
+ schema:
+ type: string
+ - name: contract_name
+ in: path
+ required: true
+ description: Contract name
+ schema:
+ type: string
+ - name: clarity_metadata_key
+ in: path
+ required: true
+ description: Metadata key
+ schema:
+ type: string
+ - name: tip
+ in: query
+ schema:
+ type: string
+ description: The Stacks chain tip to query from. If tip == latest, the query will be run from the latest
+ known tip (includes unconfirmed state).
/v2/constant_val/{contract_address}/{contract_name}/{constant_name}:
post:
@@ -633,7 +720,7 @@ paths:
/v3/blocks/{block_id}:
get:
- summary: Fetch a Nakamoto block
+ summary: Fetch a Nakamoto block
tags:
- Blocks
operationId: get_block_v3
@@ -674,7 +761,7 @@ paths:
application/json:
example:
$ref: ./api/core-node/get_tenure_info.json
-
+
/v3/tenures/{block_id}:
get:
summary: Fetch a sequence of Nakamoto blocks in a tenure
diff --git a/stackslib/src/net/api/getclaritymarfvalue.rs b/stackslib/src/net/api/getclaritymarfvalue.rs
new file mode 100644
index 0000000000..ff584a0ccf
--- /dev/null
+++ b/stackslib/src/net/api/getclaritymarfvalue.rs
@@ -0,0 +1,232 @@
+// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
+// Copyright (C) 2020-2024 Stacks Open Internet Foundation
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+use clarity::vm::clarity::ClarityConnection;
+use clarity::vm::representations::CONTRACT_PRINCIPAL_REGEX_STRING;
+use lazy_static::lazy_static;
+use regex::{Captures, Regex};
+use stacks_common::types::net::PeerHost;
+use stacks_common::util::hash::to_hex;
+
+use crate::net::http::{
+ parse_json, Error, HttpNotFound, HttpRequest, HttpRequestContents, HttpRequestPreamble,
+ HttpResponse, HttpResponseContents, HttpResponsePayload, HttpResponsePreamble,
+};
+use crate::net::httpcore::{
+ request, HttpPreambleExtensions, HttpRequestContentsExtensions, RPCRequestHandler,
+ StacksHttpRequest, StacksHttpResponse,
+};
+use crate::net::{Error as NetError, StacksNodeState, TipRequest};
+
+lazy_static! {
+ static ref CLARITY_NAME_NO_BOUNDARIES_REGEX_STRING: String =
+ "[a-zA-Z]([a-zA-Z0-9]|[-_!?+<>=/*])*|[-+=/*]|[<>]=?".into();
+ static ref MARF_KEY_FOR_TRIP_REGEX_STRING: String = format!(
+ r"vm::{}::\d+::({})",
+ *CONTRACT_PRINCIPAL_REGEX_STRING, *CLARITY_NAME_NO_BOUNDARIES_REGEX_STRING,
+ );
+ static ref MARF_KEY_FOR_QUAD_REGEX_STRING: String =
+ format!(r"{}::[0-9a-fA-F]+", *MARF_KEY_FOR_TRIP_REGEX_STRING,);
+}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct ClarityMarfResponse {
+ pub data: String,
+ #[serde(rename = "proof")]
+ #[serde(default)]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub marf_proof: Option,
+}
+
+#[derive(Clone)]
+pub struct RPCGetClarityMarfRequestHandler {
+ pub clarity_marf_key: Option,
+}
+impl RPCGetClarityMarfRequestHandler {
+ pub fn new() -> Self {
+ Self {
+ clarity_marf_key: None,
+ }
+ }
+}
+
+/// Decode the HTTP request
+impl HttpRequest for RPCGetClarityMarfRequestHandler {
+ fn verb(&self) -> &'static str {
+ "GET"
+ }
+
+ fn path_regex(&self) -> Regex {
+ Regex::new(&format!(
+ r"^/v2/clarity/marf/(?P(vm-epoch::epoch-version)|({})|({}))$",
+ *MARF_KEY_FOR_TRIP_REGEX_STRING, *MARF_KEY_FOR_QUAD_REGEX_STRING
+ ))
+ .unwrap()
+ }
+
+ fn metrics_identifier(&self) -> &str {
+ "/v2/clarity/marf/:clarity_marf_key"
+ }
+
+ /// Try to decode this request.
+ /// There's nothing to load here, so just make sure the request is well-formed.
+ fn try_parse_request(
+ &mut self,
+ preamble: &HttpRequestPreamble,
+ captures: &Captures,
+ query: Option<&str>,
+ _body: &[u8],
+ ) -> Result {
+ if preamble.get_content_length() != 0 {
+ return Err(Error::DecodeError(
+ "Invalid Http request: expected 0-length body".to_string(),
+ ));
+ }
+
+ let marf_key = if let Some(key_str) = captures.name("clarity_marf_key") {
+ key_str.as_str().to_string()
+ } else {
+ return Err(Error::Http(404, "Missing `clarity_marf_key`".to_string()));
+ };
+
+ self.clarity_marf_key = Some(marf_key);
+
+ let contents = HttpRequestContents::new().query_string(query);
+ Ok(contents)
+ }
+}
+
+/// Handle the HTTP request
+impl RPCRequestHandler for RPCGetClarityMarfRequestHandler {
+ /// Reset internal state
+ fn restart(&mut self) {
+ self.clarity_marf_key = None;
+ }
+
+ /// Make the response
+ fn try_handle_request(
+ &mut self,
+ preamble: HttpRequestPreamble,
+ contents: HttpRequestContents,
+ node: &mut StacksNodeState,
+ ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> {
+ let clarity_marf_key = self.clarity_marf_key.take().ok_or(NetError::SendError(
+ "`clarity_marf_key` not set".to_string(),
+ ))?;
+
+ let tip = match node.load_stacks_chain_tip(&preamble, &contents) {
+ Ok(tip) => tip,
+ Err(error_resp) => {
+ return error_resp.try_into_contents().map_err(NetError::from);
+ }
+ };
+
+ let with_proof = contents.get_with_proof();
+
+ let data_opt = node.with_node_state(|_network, sortdb, chainstate, _mempool, _rpc_args| {
+ chainstate.maybe_read_only_clarity_tx(
+ &sortdb.index_handle_at_block(chainstate, &tip)?,
+ &tip,
+ |clarity_tx| {
+ clarity_tx.with_clarity_db_readonly(|clarity_db| {
+ let (value_hex, marf_proof): (String, _) = if with_proof {
+ clarity_db
+ .get_data_with_proof(&clarity_marf_key)
+ .ok()
+ .flatten()
+ .map(|(a, b)| (a, Some(format!("0x{}", to_hex(&b)))))?
+ } else {
+ clarity_db
+ .get_data(&clarity_marf_key)
+ .ok()
+ .flatten()
+ .map(|a| (a, None))?
+ };
+
+ let data = format!("0x{}", value_hex);
+ Some(ClarityMarfResponse { data, marf_proof })
+ })
+ },
+ )
+ });
+
+ let data_resp = match data_opt {
+ Ok(Some(Some(data))) => data,
+ Ok(Some(None)) => {
+ return StacksHttpResponse::new_error(
+ &preamble,
+ &HttpNotFound::new("Marf key not found".to_string()),
+ )
+ .try_into_contents()
+ .map_err(NetError::from);
+ }
+ Ok(None) | Err(_) => {
+ return StacksHttpResponse::new_error(
+ &preamble,
+ &HttpNotFound::new("Chain tip not found".to_string()),
+ )
+ .try_into_contents()
+ .map_err(NetError::from);
+ }
+ };
+
+ let mut preamble = HttpResponsePreamble::ok_json(&preamble);
+ preamble.set_canonical_stacks_tip_height(Some(node.canonical_stacks_tip_height()));
+ let body = HttpResponseContents::try_from_json(&data_resp)?;
+ Ok((preamble, body))
+ }
+}
+
+/// Decode the HTTP response
+impl HttpResponse for RPCGetClarityMarfRequestHandler {
+ fn try_parse_response(
+ &self,
+ preamble: &HttpResponsePreamble,
+ body: &[u8],
+ ) -> Result {
+ let marf_value: ClarityMarfResponse = parse_json(preamble, body)?;
+ Ok(HttpResponsePayload::try_from_json(marf_value)?)
+ }
+}
+
+impl StacksHttpRequest {
+ pub fn new_getclaritymarf(
+ host: PeerHost,
+ clarity_marf_key: String,
+ tip_req: TipRequest,
+ with_proof: bool,
+ ) -> StacksHttpRequest {
+ StacksHttpRequest::new_for_peer(
+ host,
+ "GET".into(),
+ format!("/v2/clarity/marf/{}", &clarity_marf_key),
+ HttpRequestContents::new()
+ .for_tip(tip_req)
+ .query_arg("proof".into(), if with_proof { "1" } else { "0" }.into()),
+ )
+ .expect("FATAL: failed to construct request from infallible data")
+ }
+}
+
+impl StacksHttpResponse {
+ pub fn decode_clarity_marf_response(self) -> Result {
+ let contents = self.get_http_payload_ok()?;
+ let contents_json: serde_json::Value = contents.try_into()?;
+ let resp: ClarityMarfResponse = serde_json::from_value(contents_json)
+ .map_err(|_e| NetError::DeserializeError("Failed to load from JSON".to_string()))?;
+ Ok(resp)
+ }
+}
diff --git a/stackslib/src/net/api/getclaritymetadata.rs b/stackslib/src/net/api/getclaritymetadata.rs
new file mode 100644
index 0000000000..5ef3feee6e
--- /dev/null
+++ b/stackslib/src/net/api/getclaritymetadata.rs
@@ -0,0 +1,233 @@
+// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
+// Copyright (C) 2020-2024 Stacks Open Internet Foundation
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+use clarity::vm::clarity::ClarityConnection;
+use clarity::vm::representations::{CONTRACT_NAME_REGEX_STRING, STANDARD_PRINCIPAL_REGEX_STRING};
+use clarity::vm::types::QualifiedContractIdentifier;
+use clarity::vm::ContractName;
+use lazy_static::lazy_static;
+use regex::{Captures, Regex};
+use stacks_common::types::chainstate::StacksAddress;
+use stacks_common::types::net::PeerHost;
+
+use crate::net::http::{
+ parse_json, Error, HttpNotFound, HttpRequest, HttpRequestContents, HttpRequestPreamble,
+ HttpResponse, HttpResponseContents, HttpResponsePayload, HttpResponsePreamble,
+};
+use crate::net::httpcore::{
+ request, HttpPreambleExtensions, HttpRequestContentsExtensions, RPCRequestHandler,
+ StacksHttpRequest, StacksHttpResponse,
+};
+use crate::net::{Error as NetError, StacksNodeState, TipRequest};
+
+lazy_static! {
+ static ref CLARITY_NAME_NO_BOUNDARIES_REGEX_STRING: String =
+ "[a-zA-Z]([a-zA-Z0-9]|[-_!?+<>=/*])*|[-+=/*]|[<>]=?".into();
+ static ref METADATA_KEY_REGEX_STRING: String = format!(
+ r"vm-metadata::\d+::(contract|contract-size|contract-src|contract-data-size|({}))",
+ *CLARITY_NAME_NO_BOUNDARIES_REGEX_STRING,
+ );
+}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct ClarityMetadataResponse {
+ pub data: String,
+}
+
+#[derive(Clone)]
+pub struct RPCGetClarityMetadataRequestHandler {
+ pub clarity_metadata_key: Option,
+ pub contract_identifier: Option,
+}
+impl RPCGetClarityMetadataRequestHandler {
+ pub fn new() -> Self {
+ Self {
+ clarity_metadata_key: None,
+ contract_identifier: None,
+ }
+ }
+}
+
+/// Decode the HTTP request
+impl HttpRequest for RPCGetClarityMetadataRequestHandler {
+ fn verb(&self) -> &'static str {
+ "GET"
+ }
+
+ fn path_regex(&self) -> Regex {
+ Regex::new(&format!(
+ r"^/v2/clarity/metadata/(?P{})/(?P{})/(?P(analysis)|({}))$",
+ *STANDARD_PRINCIPAL_REGEX_STRING,
+ *CONTRACT_NAME_REGEX_STRING,
+ *METADATA_KEY_REGEX_STRING
+ ))
+ .unwrap()
+ }
+
+ fn metrics_identifier(&self) -> &str {
+ "/v2/clarity/metadata/:principal/:contract_name/:clarity_metadata_key"
+ }
+
+ /// Try to decode this request.
+ /// There's nothing to load here, so just make sure the request is well-formed.
+ fn try_parse_request(
+ &mut self,
+ preamble: &HttpRequestPreamble,
+ captures: &Captures,
+ query: Option<&str>,
+ _body: &[u8],
+ ) -> Result {
+ if preamble.get_content_length() != 0 {
+ return Err(Error::DecodeError(
+ "Invalid Http request: expected 0-length body".to_string(),
+ ));
+ }
+
+ let contract_identifier = request::get_contract_address(captures, "address", "contract")?;
+
+ let metadata_key = if let Some(key_str) = captures.name("clarity_metadata_key") {
+ key_str.as_str().to_string()
+ } else {
+ return Err(Error::Http(
+ 404,
+ "Missing `clarity_metadata_key`".to_string(),
+ ));
+ };
+
+ self.contract_identifier = Some(contract_identifier);
+ self.clarity_metadata_key = Some(metadata_key);
+
+ let contents = HttpRequestContents::new().query_string(query);
+ Ok(contents)
+ }
+}
+
+/// Handle the HTTP request
+impl RPCRequestHandler for RPCGetClarityMetadataRequestHandler {
+ /// Reset internal state
+ fn restart(&mut self) {
+ self.contract_identifier = None;
+ self.clarity_metadata_key = None;
+ }
+
+ /// Make the response
+ fn try_handle_request(
+ &mut self,
+ preamble: HttpRequestPreamble,
+ contents: HttpRequestContents,
+ node: &mut StacksNodeState,
+ ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> {
+ let contract_identifier = self.contract_identifier.take().ok_or(NetError::SendError(
+ "`contract_identifier` not set".to_string(),
+ ))?;
+ let clarity_metadata_key = self.clarity_metadata_key.take().ok_or(NetError::SendError(
+ "`clarity_metadata_key` not set".to_string(),
+ ))?;
+
+ let tip = match node.load_stacks_chain_tip(&preamble, &contents) {
+ Ok(tip) => tip,
+ Err(error_resp) => {
+ return error_resp.try_into_contents().map_err(NetError::from);
+ }
+ };
+
+ let data_opt = node.with_node_state(|_network, sortdb, chainstate, _mempool, _rpc_args| {
+ chainstate.maybe_read_only_clarity_tx(
+ &sortdb.index_handle_at_block(chainstate, &tip)?,
+ &tip,
+ |clarity_tx| {
+ clarity_tx.with_clarity_db_readonly(|clarity_db| {
+ let data = clarity_db
+ .store
+ .get_metadata(&contract_identifier, &clarity_metadata_key)
+ .ok()
+ .flatten()?;
+
+ Some(ClarityMetadataResponse { data })
+ })
+ },
+ )
+ });
+
+ let data_resp = match data_opt {
+ Ok(Some(Some(data))) => data,
+ Ok(Some(None)) => {
+ return StacksHttpResponse::new_error(
+ &preamble,
+ &HttpNotFound::new("Metadata not found".to_string()),
+ )
+ .try_into_contents()
+ .map_err(NetError::from);
+ }
+ Ok(None) | Err(_) => {
+ return StacksHttpResponse::new_error(
+ &preamble,
+ &HttpNotFound::new("Chain tip not found".to_string()),
+ )
+ .try_into_contents()
+ .map_err(NetError::from);
+ }
+ };
+
+ let mut preamble = HttpResponsePreamble::ok_json(&preamble);
+ preamble.set_canonical_stacks_tip_height(Some(node.canonical_stacks_tip_height()));
+ let body = HttpResponseContents::try_from_json(&data_resp)?;
+ Ok((preamble, body))
+ }
+}
+
+/// Decode the HTTP response
+impl HttpResponse for RPCGetClarityMetadataRequestHandler {
+ fn try_parse_response(
+ &self,
+ preamble: &HttpResponsePreamble,
+ body: &[u8],
+ ) -> Result {
+ let metadata: ClarityMetadataResponse = parse_json(preamble, body)?;
+ Ok(HttpResponsePayload::try_from_json(metadata)?)
+ }
+}
+
+impl StacksHttpRequest {
+ pub fn new_getclaritymetadata(
+ host: PeerHost,
+ contract_addr: StacksAddress,
+ contract_name: ContractName,
+ clarity_metadata_key: String,
+ tip_req: TipRequest,
+ ) -> StacksHttpRequest {
+ StacksHttpRequest::new_for_peer(
+ host,
+ "GET".into(),
+ format!(
+ "/v2/clarity/metadata/{}/{}/{}",
+ &contract_addr, &contract_name, &clarity_metadata_key
+ ),
+ HttpRequestContents::new().for_tip(tip_req),
+ )
+ .expect("FATAL: failed to construct request from infallible data")
+ }
+}
+
+impl StacksHttpResponse {
+ pub fn decode_clarity_metadata_response(self) -> Result {
+ let contents = self.get_http_payload_ok()?;
+ let contents_json: serde_json::Value = contents.try_into()?;
+ let resp: ClarityMetadataResponse = serde_json::from_value(contents_json)
+ .map_err(|_e| NetError::DeserializeError("Failed to load from JSON".to_string()))?;
+ Ok(resp)
+ }
+}
diff --git a/stackslib/src/net/api/mod.rs b/stackslib/src/net/api/mod.rs
index d256c15b97..0057f9047d 100644
--- a/stackslib/src/net/api/mod.rs
+++ b/stackslib/src/net/api/mod.rs
@@ -42,6 +42,8 @@ pub mod getattachment;
pub mod getattachmentsinv;
pub mod getblock;
pub mod getblock_v3;
+pub mod getclaritymarfvalue;
+pub mod getclaritymetadata;
pub mod getconstantval;
pub mod getcontractabi;
pub mod getcontractsrc;
@@ -90,6 +92,8 @@ impl StacksHttp {
self.register_rpc_endpoint(getattachmentsinv::RPCGetAttachmentsInvRequestHandler::new());
self.register_rpc_endpoint(getblock::RPCBlocksRequestHandler::new());
self.register_rpc_endpoint(getblock_v3::RPCNakamotoBlockRequestHandler::new());
+ self.register_rpc_endpoint(getclaritymarfvalue::RPCGetClarityMarfRequestHandler::new());
+ self.register_rpc_endpoint(getclaritymetadata::RPCGetClarityMetadataRequestHandler::new());
self.register_rpc_endpoint(getconstantval::RPCGetConstantValRequestHandler::new());
self.register_rpc_endpoint(getcontractabi::RPCGetContractAbiRequestHandler::new());
self.register_rpc_endpoint(getcontractsrc::RPCGetContractSrcRequestHandler::new());
diff --git a/stackslib/src/net/api/tests/getclaritymarfvalue.rs b/stackslib/src/net/api/tests/getclaritymarfvalue.rs
new file mode 100644
index 0000000000..ce342b7442
--- /dev/null
+++ b/stackslib/src/net/api/tests/getclaritymarfvalue.rs
@@ -0,0 +1,180 @@
+// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
+// Copyright (C) 2020-2024 Stacks Open Internet Foundation
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+use std::net::{IpAddr, Ipv4Addr, SocketAddr};
+
+use clarity::vm::types::{QualifiedContractIdentifier, StacksAddressExtensions};
+use clarity::vm::{ClarityName, ContractName};
+use stacks_common::codec::StacksMessageCodec;
+use stacks_common::types::chainstate::StacksAddress;
+use stacks_common::types::net::PeerHost;
+use stacks_common::types::Address;
+
+use super::test_rpc;
+use crate::net::api::*;
+use crate::net::connection::ConnectionOptions;
+use crate::net::httpcore::{
+ HttpPreambleExtensions, HttpRequestContentsExtensions, RPCRequestHandler, StacksHttp,
+ StacksHttpRequest,
+};
+use crate::net::{ProtocolFamily, TipRequest};
+
+#[test]
+fn test_try_parse_request() {
+ let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333);
+ let mut http = StacksHttp::new(addr.clone(), &ConnectionOptions::default());
+
+ let vm_key_epoch = "vm-epoch::epoch-version";
+ let vm_key_trip = "vm::ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5.counter::1::count";
+ let vm_key_quad = "vm::ST1SJ3DTE5DN7X54YDH5D64R3BCB6A2AG2ZQ8YPD5.counter::0::data::1234";
+ let valid_keys = [vm_key_epoch, vm_key_trip, vm_key_quad];
+
+ for key in valid_keys {
+ let request = StacksHttpRequest::new_getclaritymarf(
+ addr.into(),
+ key.to_string(),
+ TipRequest::SpecificTip(StacksBlockId([0x22; 32])),
+ true,
+ );
+ assert_eq!(
+ request.contents().tip_request(),
+ TipRequest::SpecificTip(StacksBlockId([0x22; 32]))
+ );
+ assert_eq!(request.contents().get_with_proof(), true);
+
+ let bytes = request.try_serialize().unwrap();
+
+ let (parsed_preamble, offset) = http.read_preamble(&bytes).unwrap();
+ let mut handler = getclaritymarfvalue::RPCGetClarityMarfRequestHandler::new();
+ let mut parsed_request = http
+ .handle_try_parse_request(
+ &mut handler,
+ &parsed_preamble.expect_request(),
+ &bytes[offset..],
+ )
+ .unwrap();
+
+ // parsed request consumes headers that would not be in a constructed request
+ parsed_request.clear_headers();
+ let (preamble, contents) = parsed_request.destruct();
+
+ // consumed path args
+ assert_eq!(handler.clarity_marf_key, Some(key.to_string()));
+
+ assert_eq!(&preamble, request.preamble());
+
+ handler.restart();
+ assert!(handler.clarity_marf_key.is_none());
+ }
+}
+
+#[test]
+fn test_try_make_response() {
+ let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333);
+
+ let mut requests = vec![];
+
+ // query existing
+ let request = StacksHttpRequest::new_getclaritymarf(
+ addr.into(),
+ "vm::ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world::1::bar".to_string(),
+ TipRequest::UseLatestAnchoredTip,
+ true,
+ );
+ requests.push(request);
+
+ // query existing unconfirmed
+ let request = StacksHttpRequest::new_getclaritymarf(
+ addr.into(),
+ "vm::ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world-unconfirmed::1::bar-unconfirmed"
+ .to_string(),
+ TipRequest::UseLatestUnconfirmedTip,
+ true,
+ );
+ requests.push(request);
+
+ // query non-existant var
+ let request = StacksHttpRequest::new_getclaritymarf(
+ addr.into(),
+ "vm::ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world::1::does-not-exist".to_string(),
+ TipRequest::UseLatestAnchoredTip,
+ true,
+ );
+ requests.push(request);
+
+ // query non-existant contract
+ let request = StacksHttpRequest::new_getclaritymarf(
+ addr.into(),
+ "vm::ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.does-not-exist::1::bar".to_string(),
+ TipRequest::UseLatestAnchoredTip,
+ true,
+ );
+ requests.push(request);
+
+ let mut responses = test_rpc(function_name!(), requests);
+
+ // existing data
+ let response = responses.remove(0);
+ debug!(
+ "Response:\n{}\n",
+ std::str::from_utf8(&response.try_serialize().unwrap()).unwrap()
+ );
+
+ assert_eq!(
+ response.preamble().get_canonical_stacks_tip_height(),
+ Some(1)
+ );
+
+ let resp = response.decode_data_var_response().unwrap();
+ assert_eq!(resp.data, "0x0000000000000000000000000000000000");
+ assert!(resp.marf_proof.is_some());
+
+ // unconfirmed data
+ let response = responses.remove(0);
+ debug!(
+ "Response:\n{}\n",
+ std::str::from_utf8(&response.try_serialize().unwrap()).unwrap()
+ );
+
+ assert_eq!(
+ response.preamble().get_canonical_stacks_tip_height(),
+ Some(1)
+ );
+
+ let resp = response.decode_data_var_response().unwrap();
+ assert_eq!(resp.data, "0x0100000000000000000000000000000001");
+ assert!(resp.marf_proof.is_some());
+
+ // no such var
+ let response = responses.remove(0);
+ debug!(
+ "Response:\n{}\n",
+ std::str::from_utf8(&response.try_serialize().unwrap()).unwrap()
+ );
+
+ let (preamble, body) = response.destruct();
+ assert_eq!(preamble.status_code, 404);
+
+ // no such contract
+ let response = responses.remove(0);
+ debug!(
+ "Response:\n{}\n",
+ std::str::from_utf8(&response.try_serialize().unwrap()).unwrap()
+ );
+
+ let (preamble, body) = response.destruct();
+ assert_eq!(preamble.status_code, 404);
+}
diff --git a/stackslib/src/net/api/tests/getclaritymetadata.rs b/stackslib/src/net/api/tests/getclaritymetadata.rs
new file mode 100644
index 0000000000..3de5949a87
--- /dev/null
+++ b/stackslib/src/net/api/tests/getclaritymetadata.rs
@@ -0,0 +1,166 @@
+// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
+// Copyright (C) 2020-2024 Stacks Open Internet Foundation
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+use std::net::{IpAddr, Ipv4Addr, SocketAddr};
+
+use clarity::vm::types::{QualifiedContractIdentifier, StacksAddressExtensions};
+use clarity::vm::{ClarityName, ContractName};
+use stacks_common::codec::StacksMessageCodec;
+use stacks_common::types::chainstate::StacksAddress;
+use stacks_common::types::net::PeerHost;
+use stacks_common::types::Address;
+
+use super::test_rpc;
+use crate::net::api::*;
+use crate::net::connection::ConnectionOptions;
+use crate::net::httpcore::{
+ HttpPreambleExtensions, HttpRequestContentsExtensions, RPCRequestHandler, StacksHttp,
+ StacksHttpRequest,
+};
+use crate::net::{ProtocolFamily, TipRequest};
+
+#[test]
+fn test_try_parse_request() {
+ let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333);
+ let mut http = StacksHttp::new(addr.clone(), &ConnectionOptions::default());
+
+ let request = StacksHttpRequest::new_getclaritymetadata(
+ addr.into(),
+ StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R").unwrap(),
+ "hello-world".try_into().unwrap(),
+ "vm-metadata::9::contract-size".to_string(),
+ TipRequest::SpecificTip(StacksBlockId([0x22; 32])),
+ );
+ assert_eq!(
+ request.contents().tip_request(),
+ TipRequest::SpecificTip(StacksBlockId([0x22; 32]))
+ );
+ let bytes = request.try_serialize().unwrap();
+
+ let (parsed_preamble, offset) = http.read_preamble(&bytes).unwrap();
+ let mut handler = getclaritymetadata::RPCGetClarityMetadataRequestHandler::new();
+ let mut parsed_request = http
+ .handle_try_parse_request(
+ &mut handler,
+ &parsed_preamble.expect_request(),
+ &bytes[offset..],
+ )
+ .unwrap();
+
+ // parsed request consumes headers that would not be in a constructed request
+ parsed_request.clear_headers();
+ let (preamble, contents) = parsed_request.destruct();
+
+ // consumed path args
+ assert_eq!(
+ handler.clarity_metadata_key,
+ Some("vm-metadata::9::contract-size".to_string())
+ );
+ assert_eq!(
+ handler.contract_identifier,
+ Some(
+ QualifiedContractIdentifier::parse(
+ "ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world"
+ )
+ .unwrap()
+ )
+ );
+
+ assert_eq!(&preamble, request.preamble());
+
+ handler.restart();
+ assert!(handler.clarity_metadata_key.is_none());
+}
+
+#[test]
+fn test_try_parse_request_for_analysis() {
+ let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333);
+ let mut http = StacksHttp::new(addr.clone(), &ConnectionOptions::default());
+
+ let request = StacksHttpRequest::new_getclaritymetadata(
+ addr.into(),
+ StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R").unwrap(),
+ "hello-world".try_into().unwrap(),
+ "analysis".to_string(),
+ TipRequest::SpecificTip(StacksBlockId([0x22; 32])),
+ );
+ assert_eq!(
+ request.contents().tip_request(),
+ TipRequest::SpecificTip(StacksBlockId([0x22; 32]))
+ );
+ let bytes = request.try_serialize().unwrap();
+
+ let (parsed_preamble, offset) = http.read_preamble(&bytes).unwrap();
+ let mut handler = getclaritymetadata::RPCGetClarityMetadataRequestHandler::new();
+ let mut parsed_request = http
+ .handle_try_parse_request(
+ &mut handler,
+ &parsed_preamble.expect_request(),
+ &bytes[offset..],
+ )
+ .unwrap();
+
+ // parsed request consumes headers that would not be in a constructed request
+ parsed_request.clear_headers();
+ let (preamble, contents) = parsed_request.destruct();
+
+ // consumed path args
+ assert_eq!(handler.clarity_metadata_key, Some("analysis".to_string()));
+ assert_eq!(
+ handler.contract_identifier,
+ Some(
+ QualifiedContractIdentifier::parse(
+ "ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world"
+ )
+ .unwrap()
+ )
+ );
+
+ assert_eq!(&preamble, request.preamble());
+
+ handler.restart();
+ assert!(handler.clarity_metadata_key.is_none());
+}
+
+#[test]
+fn test_try_make_response() {
+ let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333);
+
+ let mut requests = vec![];
+
+ // query existing
+ let request = StacksHttpRequest::new_getclaritymetadata(
+ addr.into(),
+ StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R").unwrap(),
+ "hello-world".try_into().unwrap(),
+ "vm-metadata::9::contract-size".to_string(),
+ TipRequest::UseLatestAnchoredTip,
+ );
+ requests.push(request);
+
+ let mut responses = test_rpc(function_name!(), requests);
+
+ // latest data
+ let response = responses.remove(0);
+
+ assert_eq!(
+ response.preamble().get_canonical_stacks_tip_height(),
+ Some(1)
+ );
+
+ let resp = response.decode_clarity_metadata_response().unwrap();
+ assert_eq!(resp.data, "1432");
+}
diff --git a/stackslib/src/net/api/tests/mod.rs b/stackslib/src/net/api/tests/mod.rs
index f0a537d045..72800164f3 100644
--- a/stackslib/src/net/api/tests/mod.rs
+++ b/stackslib/src/net/api/tests/mod.rs
@@ -60,6 +60,8 @@ mod getattachment;
mod getattachmentsinv;
mod getblock;
mod getblock_v3;
+mod getclaritymarfvalue;
+mod getclaritymetadata;
mod getconstantval;
mod getcontractabi;
mod getcontractsrc;
@@ -117,7 +119,7 @@ const TEST_CONTRACT: &'static str = "
(ok 1)))
(begin
(map-set unit-map { account: 'ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R } { units: 123 }))
-
+
(define-read-only (ro-confirmed) u1)
(define-public (do-test) (ok u0))