diff --git a/CHANGELOG.md b/CHANGELOG.md index 0985d33202..b080d761b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE ### Added - In the `/v3/transaction/{txid}` RPC endpoint, added `block_height` and `is_canonical` to the response. +- Added the `events` array to readonly endpoints output. ## [3.3.0.0.2] diff --git a/clarity/src/vm/contexts.rs b/clarity/src/vm/contexts.rs index b2ea58e5f0..9962d2b8d0 100644 --- a/clarity/src/vm/contexts.rs +++ b/clarity/src/vm/contexts.rs @@ -1124,6 +1124,16 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { &self.global_context.epoch_id } + pub fn execute_contract_with_events( + &mut self, + contract: &QualifiedContractIdentifier, + tx_name: &str, + args: &[SymbolicExpression], + read_only: bool, + ) -> Result<(Value, Vec), VmExecutionError> { + self.inner_execute_contract(contract, tx_name, args, read_only, false) + } + pub fn execute_contract( &mut self, contract: &QualifiedContractIdentifier, @@ -1132,6 +1142,7 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { read_only: bool, ) -> Result { self.inner_execute_contract(contract, tx_name, args, read_only, false) + .map(|(value, _)| value) } /// This method is exposed for callers that need to invoke a private method directly. @@ -1145,6 +1156,7 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { read_only: bool, ) -> Result { self.inner_execute_contract(contract, tx_name, args, read_only, true) + .map(|(value, _)| value) } /// This method handles actual execution of contract-calls on a contract. @@ -1160,7 +1172,7 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { args: &[SymbolicExpression], read_only: bool, allow_private: bool, - ) -> Result { + ) -> Result<(Value, Vec), VmExecutionError> { let contract_size = self .global_context .database @@ -1208,11 +1220,11 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { return Err(CheckErrorKind::CircularReference(vec![func_identifier.to_string()]).into()) } self.call_stack.insert(&func_identifier, true); - let res = self.execute_function_as_transaction(&func, &args, Some(&contract.contract_context), allow_private); + let res = self.execute_function_as_transaction_and_events(&func, &args, Some(&contract.contract_context), allow_private); self.call_stack.remove(&func_identifier, true)?; match res { - Ok(value) => { + Ok((value, events)) => { if let Some(handler) = self.global_context.database.get_cc_special_cases_handler() { handler( self.global_context, @@ -1224,7 +1236,7 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { &value )?; } - Ok(value) + Ok((value, events)) }, Err(e) => Err(e) } @@ -1238,6 +1250,23 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { next_contract_context: Option<&ContractContext>, allow_private: bool, ) -> Result { + let (value, _events) = self.execute_function_as_transaction_and_events( + function, + args, + next_contract_context, + allow_private, + )?; + + Ok(value) + } + + pub fn execute_function_as_transaction_and_events( + &mut self, + function: &DefinedFunction, + args: &[Value], + next_contract_context: Option<&ContractContext>, + allow_private: bool, + ) -> Result<(Value, Vec), VmExecutionError> { let make_read_only = function.is_read_only(); if make_read_only { @@ -1261,11 +1290,25 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> { function.execute_apply(args, &mut nested_env) }; - if make_read_only { + // retrieve all the events + let mut events = vec![]; + self.global_context + .event_batches + .iter() + .for_each(|event_batch| { + events.extend(event_batch.events.clone()); + }); + + let result = if make_read_only { self.global_context.roll_back()?; result } else { self.global_context.handle_tx_result(result, allow_private) + }; + + match result { + Ok(result) => Ok((result, events)), + Err(e) => Err(e), } } diff --git a/docs/rpc/components/schemas/read-only-function-result.schema.yaml b/docs/rpc/components/schemas/read-only-function-result.schema.yaml index 890a90e305..0c1d6591fe 100644 --- a/docs/rpc/components/schemas/read-only-function-result.schema.yaml +++ b/docs/rpc/components/schemas/read-only-function-result.schema.yaml @@ -24,3 +24,18 @@ oneOf: cause: type: string description: A string representing the cause of the error. + events: + type: array + description: Contract events generated by the call + items: + type: object + properties: + sender: + type: string + description: The contract address generating the event. + key: + type: string + description: Who generated the event (generally "print"). + value: + type: string + description: Hex-encoded Clarity value of the event. diff --git a/stackslib/src/net/api/callreadonly.rs b/stackslib/src/net/api/callreadonly.rs index 3f62d0ac21..13dceaad96 100644 --- a/stackslib/src/net/api/callreadonly.rs +++ b/stackslib/src/net/api/callreadonly.rs @@ -19,17 +19,18 @@ use clarity::vm::ast::parser::v1::CLARITY_NAME_REGEX; use clarity::vm::clarity::ClarityConnection; use clarity::vm::costs::{ExecutionCost, LimitedCostTracker}; use clarity::vm::errors::VmExecutionError::Unchecked; +use clarity::vm::events::StacksTransactionEvent; use clarity::vm::representations::{CONTRACT_NAME_REGEX_STRING, STANDARD_PRINCIPAL_REGEX_STRING}; use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier}; -use clarity::vm::{ClarityName, ContractName, SymbolicExpression, Value}; +use clarity::vm::{ClarityName, ContractName, Environment, SymbolicExpression, Value}; use regex::{Captures, Regex}; use stacks_common::types::chainstate::StacksAddress; use stacks_common::types::net::PeerHost; use crate::net::http::{ parse_json, Error, HttpContentType, HttpNotFound, HttpRequest, HttpRequestContents, - HttpRequestPreamble, HttpResponse, HttpResponseContents, HttpResponsePayload, - HttpResponsePreamble, + HttpRequestPreamble, HttpRequestTimeout, HttpResponse, HttpResponseContents, + HttpResponsePayload, HttpResponsePreamble, }; use crate::net::httpcore::{ request, HttpRequestContentsExtensions as _, RPCRequestHandler, StacksHttpRequest, @@ -45,6 +46,13 @@ pub struct CallReadOnlyRequestBody { pub arguments: Vec, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CallReadOnlyEvent { + pub sender: String, + pub key: String, + pub value: String, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct CallReadOnlyResponse { pub okay: bool, @@ -54,6 +62,8 @@ pub struct CallReadOnlyResponse { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub cause: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub events: Option>, } #[derive(Clone)] @@ -81,101 +91,18 @@ impl RPCCallReadOnlyRequestHandler { arguments: None, } } -} - -/// Decode the HTTP request -impl HttpRequest for RPCCallReadOnlyRequestHandler { - fn verb(&self) -> &'static str { - "POST" - } - - fn path_regex(&self) -> Regex { - Regex::new(&format!( - "^/v2/contracts/call-read/(?P
{})/(?P{})/(?P{})$", - *STANDARD_PRINCIPAL_REGEX_STRING, *CONTRACT_NAME_REGEX_STRING, *CLARITY_NAME_REGEX - )) - .unwrap() - } - - fn metrics_identifier(&self) -> &str { - "/v2/contracts/call-read/:principal/:contract_name/:func_name" - } - - /// Try to decode this request. - fn try_parse_request( - &mut self, - preamble: &HttpRequestPreamble, - captures: &Captures, - query: Option<&str>, - body: &[u8], - ) -> Result { - let content_len = preamble.get_content_length(); - if !(content_len > 0 && content_len < self.maximum_call_argument_size) { - return Err(Error::DecodeError(format!( - "Invalid Http request: invalid body length for CallReadOnly ({})", - content_len - ))); - } - - if preamble.content_type != Some(HttpContentType::JSON) { - return Err(Error::DecodeError( - "Invalid content-type: expected application/json".to_string(), - )); - } - - let contract_identifier = request::get_contract_address(captures, "address", "contract")?; - let function = request::get_clarity_name(captures, "function")?; - let body: CallReadOnlyRequestBody = serde_json::from_slice(body) - .map_err(|_e| Error::DecodeError("Failed to parse JSON body".into()))?; - - let sender = PrincipalData::parse(&body.sender) - .map_err(|_e| Error::DecodeError("Failed to parse sender principal".into()))?; - - let sponsor = if let Some(sponsor) = body.sponsor { - Some( - PrincipalData::parse(&sponsor) - .map_err(|_e| Error::DecodeError("Failed to parse sponsor principal".into()))?, - ) - } else { - None - }; - - // arguments must be valid Clarity values - let arguments = body - .arguments - .into_iter() - .map(|hex| Value::try_deserialize_hex_untyped(&hex).ok()) - .collect::>>() - .ok_or_else(|| Error::DecodeError("Failed to deserialize argument value".into()))?; - - self.contract_identifier = Some(contract_identifier); - self.function = Some(function); - self.sender = Some(sender); - self.sponsor = sponsor; - self.arguments = Some(arguments); - - Ok(HttpRequestContents::new().query_string(query)) - } -} - -/// Handle the HTTP request -impl RPCRequestHandler for RPCCallReadOnlyRequestHandler { - /// Reset internal state - fn restart(&mut self) { - self.contract_identifier = None; - self.function = None; - self.sender = None; - self.sponsor = None; - self.arguments = None; - } - /// Make the response - fn try_handle_request( + pub fn execute_contract_function( &mut self, preamble: HttpRequestPreamble, contents: HttpRequestContents, node: &mut StacksNodeState, - ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> { + override_cost_tracker: Option, + to_do: F, + ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> + where + F: FnOnce(&mut Environment), + { let tip = match node.load_stacks_chain_tip(&preamble, &contents) { Ok(tip) => tip, Err(error_resp) => { @@ -221,9 +148,13 @@ impl RPCRequestHandler for RPCCallReadOnlyRequestHandler { |clarity_tx| { let epoch = clarity_tx.get_epoch(); let cost_track = clarity_tx.with_clarity_db_readonly(|clarity_db| { - LimitedCostTracker::new_mid_block( - mainnet, chain_id, cost_limit, clarity_db, epoch, - ) + if let Some(cost_tracker) = override_cost_tracker { + Ok(cost_tracker) + } else { + LimitedCostTracker::new_mid_block( + mainnet, chain_id, cost_limit, clarity_db, epoch, + ) + } })?; clarity_tx.with_readonly_clarity_env( @@ -233,13 +164,15 @@ impl RPCRequestHandler for RPCCallReadOnlyRequestHandler { sponsor, cost_track, |env| { + to_do(env); + // we want to execute any function as long as no actual writes are made as // opposed to be limited to purely calling `define-read-only` functions, // so use `read_only = false`. This broadens the number of functions that // can be called, and also circumvents limitations on `define-read-only` // functions that can not use `contrac-call?`, even when calling other // read-only functions - env.execute_contract( + env.execute_contract_with_events( &contract_identifier, function.as_str(), &args, @@ -251,17 +184,43 @@ impl RPCRequestHandler for RPCCallReadOnlyRequestHandler { ) }); - // decode the response + // decode the response (and serialize the events) let data_resp = match data_resp { - Ok(Some(Ok(data))) => { + Ok(Some(Ok((data, events)))) => { let hex_result = data .serialize_to_hex() .map_err(|e| NetError::SerializeError(format!("{:?}", &e)))?; + let events = { + let events: Vec = events + .iter() + .filter_map(|event| match event { + StacksTransactionEvent::SmartContractEvent(event_data) => { + if let Ok(event_hex) = event_data.value.serialize_to_hex() { + Some(CallReadOnlyEvent { + sender: event_data.key.0.to_string(), + key: event_data.key.1.clone(), + value: event_hex, + }) + } else { + None + } + } + _ => None, + }) + .collect(); + if events.is_empty() { + None + } else { + Some(events) + } + }; + CallReadOnlyResponse { okay: true, result: Some(format!("0x{}", hex_result)), cause: None, + events, } } Ok(Some(Err(e))) => match e { @@ -272,12 +231,22 @@ impl RPCRequestHandler for RPCCallReadOnlyRequestHandler { okay: false, result: None, cause: Some("NotReadOnly".to_string()), + events: None, } } + Unchecked(CheckErrorKind::ExecutionTimeExpired) => { + return StacksHttpResponse::new_error( + &preamble, + &HttpRequestTimeout::new("ExecutionTime expired".to_string()), + ) + .try_into_contents() + .map_err(NetError::from) + } _ => CallReadOnlyResponse { okay: false, result: None, cause: Some(e.to_string()), + events: None, }, }, Ok(None) | Err(_) => { @@ -296,6 +265,103 @@ impl RPCRequestHandler for RPCCallReadOnlyRequestHandler { } } +/// Decode the HTTP request +impl HttpRequest for RPCCallReadOnlyRequestHandler { + fn verb(&self) -> &'static str { + "POST" + } + + fn path_regex(&self) -> Regex { + Regex::new(&format!( + "^/v2/contracts/call-read/(?P
{})/(?P{})/(?P{})$", + *STANDARD_PRINCIPAL_REGEX_STRING, *CONTRACT_NAME_REGEX_STRING, *CLARITY_NAME_REGEX + )) + .unwrap() + } + + fn metrics_identifier(&self) -> &str { + "/v2/contracts/call-read/:principal/:contract_name/:func_name" + } + + /// Try to decode this request. + fn try_parse_request( + &mut self, + preamble: &HttpRequestPreamble, + captures: &Captures, + query: Option<&str>, + body: &[u8], + ) -> Result { + let content_len = preamble.get_content_length(); + if !(content_len > 0 && content_len < self.maximum_call_argument_size) { + return Err(Error::DecodeError(format!( + "Invalid Http request: invalid body length for CallReadOnly ({})", + content_len + ))); + } + + if preamble.content_type != Some(HttpContentType::JSON) { + return Err(Error::DecodeError( + "Invalid content-type: expected application/json".to_string(), + )); + } + + let contract_identifier = request::get_contract_address(captures, "address", "contract")?; + let function = request::get_clarity_name(captures, "function")?; + let body: CallReadOnlyRequestBody = serde_json::from_slice(body) + .map_err(|_e| Error::DecodeError("Failed to parse JSON body".into()))?; + + let sender = PrincipalData::parse(&body.sender) + .map_err(|_e| Error::DecodeError("Failed to parse sender principal".into()))?; + + let sponsor = if let Some(sponsor) = body.sponsor { + Some( + PrincipalData::parse(&sponsor) + .map_err(|_e| Error::DecodeError("Failed to parse sponsor principal".into()))?, + ) + } else { + None + }; + + // arguments must be valid Clarity values + let arguments = body + .arguments + .into_iter() + .map(|hex| Value::try_deserialize_hex_untyped(&hex).ok()) + .collect::>>() + .ok_or_else(|| Error::DecodeError("Failed to deserialize argument value".into()))?; + + self.contract_identifier = Some(contract_identifier); + self.function = Some(function); + self.sender = Some(sender); + self.sponsor = sponsor; + self.arguments = Some(arguments); + + Ok(HttpRequestContents::new().query_string(query)) + } +} + +/// Handle the HTTP request +impl RPCRequestHandler for RPCCallReadOnlyRequestHandler { + /// Reset internal state + fn restart(&mut self) { + self.contract_identifier = None; + self.function = None; + self.sender = None; + self.sponsor = None; + self.arguments = None; + } + + /// Make the response + fn try_handle_request( + &mut self, + preamble: HttpRequestPreamble, + contents: HttpRequestContents, + node: &mut StacksNodeState, + ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> { + self.execute_contract_function(preamble, contents, node, None, |_| {}) + } +} + /// Decode the HTTP response impl HttpResponse for RPCCallReadOnlyRequestHandler { fn try_parse_response( diff --git a/stackslib/src/net/api/fastcallreadonly.rs b/stackslib/src/net/api/fastcallreadonly.rs index 67ca10529e..0ca186a7c9 100644 --- a/stackslib/src/net/api/fastcallreadonly.rs +++ b/stackslib/src/net/api/fastcallreadonly.rs @@ -15,14 +15,11 @@ use std::time::Duration; -use clarity::vm::analysis::CheckErrorKind; use clarity::vm::ast::parser::v1::CLARITY_NAME_REGEX; -use clarity::vm::clarity::ClarityConnection; use clarity::vm::costs::{ExecutionCost, LimitedCostTracker}; -use clarity::vm::errors::VmExecutionError::Unchecked; use clarity::vm::representations::{CONTRACT_NAME_REGEX_STRING, STANDARD_PRINCIPAL_REGEX_STRING}; use clarity::vm::types::PrincipalData; -use clarity::vm::{ClarityName, ContractName, SymbolicExpression, Value}; +use clarity::vm::{ClarityName, ContractName, Value}; use regex::{Captures, Regex}; use stacks_common::types::chainstate::StacksAddress; use stacks_common::types::net::PeerHost; @@ -31,13 +28,11 @@ use crate::net::api::callreadonly::{ CallReadOnlyRequestBody, CallReadOnlyResponse, RPCCallReadOnlyRequestHandler, }; use crate::net::http::{ - parse_json, Error, HttpContentType, HttpNotFound, HttpRequest, HttpRequestContents, - HttpRequestPreamble, HttpRequestTimeout, HttpResponse, HttpResponseContents, - HttpResponsePayload, HttpResponsePreamble, + parse_json, Error, HttpContentType, HttpRequest, HttpRequestContents, HttpRequestPreamble, + HttpResponse, HttpResponseContents, HttpResponsePayload, HttpResponsePreamble, }; use crate::net::httpcore::{ request, HttpRequestContentsExtensions as _, RPCRequestHandler, StacksHttpRequest, - StacksHttpResponse, }; use crate::net::{Error as NetError, StacksNodeState, TipRequest}; @@ -177,131 +172,17 @@ impl RPCRequestHandler for RPCFastCallReadOnlyRequestHandler { contents: HttpRequestContents, node: &mut StacksNodeState, ) -> Result<(HttpResponsePreamble, HttpResponseContents), NetError> { - 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 contract_identifier = self - .call_read_only_handler - .contract_identifier - .take() - .ok_or(NetError::SendError("Missing `contract_identifier`".into()))?; - let function = self - .call_read_only_handler - .function - .take() - .ok_or(NetError::SendError("Missing `function`".into()))?; - let sender = self - .call_read_only_handler - .sender - .take() - .ok_or(NetError::SendError("Missing `sender`".into()))?; - let sponsor = self.call_read_only_handler.sponsor.clone(); - let arguments = self - .call_read_only_handler - .arguments - .take() - .ok_or(NetError::SendError("Missing `arguments`".into()))?; - - // run the read-only call - let data_resp = - node.with_node_state(|_network, sortdb, chainstate, _mempool, _rpc_args| { - let args: Vec<_> = arguments - .iter() - .map(|x| SymbolicExpression::atom_value(x.clone())) - .collect(); - - let mainnet = chainstate.mainnet; - let chain_id = chainstate.chain_id; - - chainstate.maybe_read_only_clarity_tx( - &sortdb.index_handle_at_block(chainstate, &tip)?, - &tip, - |clarity_tx| { - clarity_tx.with_readonly_clarity_env( - mainnet, - chain_id, - sender, - sponsor, - LimitedCostTracker::new_free(), - |env| { - // cost tracking in read only calls is meamingful mainly from a security point of view - // for this reason we enforce max_execution_time when cost tracking is disabled/free - - env.global_context - .set_max_execution_time(self.read_only_max_execution_time); - - // we want to execute any function as long as no actual writes are made as - // opposed to be limited to purely calling `define-read-only` functions, - // so use `read_only = false`. This broadens the number of functions that - // can be called, and also circumvents limitations on `define-read-only` - // functions that can not use `contrac-call?`, even when calling other - // read-only functions - env.execute_contract( - &contract_identifier, - function.as_str(), - &args, - false, - ) - }, - ) - }, - ) - }); - - // decode the response - let data_resp = match data_resp { - Ok(Some(Ok(data))) => { - let hex_result = data - .serialize_to_hex() - .map_err(|e| NetError::SerializeError(format!("{:?}", &e)))?; - - CallReadOnlyResponse { - okay: true, - result: Some(format!("0x{}", hex_result)), - cause: None, - } - } - Ok(Some(Err(e))) => match e { - Unchecked(CheckErrorKind::CostBalanceExceeded(actual_cost, _)) - if actual_cost.write_count > 0 => - { - CallReadOnlyResponse { - okay: false, - result: None, - cause: Some("NotReadOnly".to_string()), - } - } - Unchecked(CheckErrorKind::ExecutionTimeExpired) => { - return StacksHttpResponse::new_error( - &preamble, - &HttpRequestTimeout::new("ExecutionTime expired".to_string()), - ) - .try_into_contents() - .map_err(NetError::from) - } - _ => CallReadOnlyResponse { - okay: false, - result: None, - cause: Some(e.to_string()), - }, + // + self.call_read_only_handler.execute_contract_function( + preamble, + contents, + node, + Some(LimitedCostTracker::new_free()), + |env| { + env.global_context + .set_max_execution_time(self.read_only_max_execution_time); }, - Ok(None) | Err(_) => { - return StacksHttpResponse::new_error( - &preamble, - &HttpNotFound::new("Chain tip not found".to_string()), - ) - .try_into_contents() - .map_err(NetError::from); - } - }; - - let preamble = HttpResponsePreamble::ok_json(&preamble); - let body = HttpResponseContents::try_from_json(&data_resp)?; - Ok((preamble, body)) + ) } } diff --git a/stackslib/src/net/api/tests/callreadonly.rs b/stackslib/src/net/api/tests/callreadonly.rs index 1ad5c149cd..0f4864880a 100644 --- a/stackslib/src/net/api/tests/callreadonly.rs +++ b/stackslib/src/net/api/tests/callreadonly.rs @@ -1,5 +1,5 @@ // Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2023 Stacks Open Internet Foundation +// Copyright (C) 2020-2025 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 @@ -181,6 +181,21 @@ fn test_try_make_response() { ); requests.push(request); + // call function generating events + let request = StacksHttpRequest::new_callreadonlyfunction( + addr.into(), + StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R").unwrap(), + "hello-world".try_into().unwrap(), + StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R") + .unwrap() + .to_account_principal(), + None, + "printer".try_into().unwrap(), + vec![], + TipRequest::UseLatestAnchoredTip, + ); + requests.push(request); + let mut responses = test_rpc(function_name!(), requests); // confirmed tip @@ -257,4 +272,69 @@ fn test_try_make_response() { let (preamble, payload) = response.destruct(); assert_eq!(preamble.status_code, 404); + + // generated events + let response = responses.remove(0); + debug!( + "Response:\n{}\n", + std::str::from_utf8(&response.try_serialize().unwrap()).unwrap() + ); + + let resp = response.decode_call_readonly_response().unwrap(); + + assert!(resp.okay); + assert!(resp.result.is_some()); + assert!(resp.cause.is_none()); + + // Ok(u1) + assert_eq!( + resp.result.unwrap(), + "0x070100000000000000000000000000000001" + ); + + let events = resp.events.unwrap(); + + assert_eq!(events.len(), 7); + assert_eq!( + events[0].sender, + "ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world" + ); + assert_eq!(events[0].key, "print"); + assert_eq!(events[0].value, "0000000000000000000000000000000064"); // 100 + assert_eq!( + events[1].sender, + "ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world" + ); + assert_eq!(events[1].key, "print"); + assert_eq!(events[1].value, "01000000000000000000000000000003e8"); // u1000 + assert_eq!( + events[2].sender, + "ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world" + ); + assert_eq!(events[2].key, "print"); + assert_eq!(events[2].value, "0d0000000474657374"); // "test" + assert_eq!( + events[3].sender, + "ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world" + ); + assert_eq!(events[3].key, "print"); + assert_eq!(events[3].value, "03"); // true + assert_eq!( + events[4].sender, + "ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world" + ); + assert_eq!(events[4].key, "print"); + assert_eq!(events[4].value, "0d0000000578797a7a79"); // "xyzzy" + assert_eq!( + events[5].sender, + "ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world" + ); + assert_eq!(events[5].key, "print"); + assert_eq!(events[5].value, "0d0000000578797a7a77"); // "xyzzw" + assert_eq!( + events[6].sender, + "ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world" + ); + assert_eq!(events[6].key, "print"); + assert_eq!(events[6].value, "0d0000000471757578"); // "quux" } diff --git a/stackslib/src/net/api/tests/fastcallreadonly.rs b/stackslib/src/net/api/tests/fastcallreadonly.rs index cd6c6814ec..b2a7111a89 100644 --- a/stackslib/src/net/api/tests/fastcallreadonly.rs +++ b/stackslib/src/net/api/tests/fastcallreadonly.rs @@ -201,6 +201,21 @@ fn test_try_make_response() { requests.push(request); + // call function generating events + let request = StacksHttpRequest::new_callreadonlyfunction( + addr.into(), + StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R").unwrap(), + "hello-world".try_into().unwrap(), + StacksAddress::from_string("ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R") + .unwrap() + .to_account_principal(), + None, + "printer".try_into().unwrap(), + vec![], + TipRequest::UseLatestAnchoredTip, + ); + requests.push(request); + let mut responses = test_rpc(function_name!(), requests); // confirmed tip @@ -277,6 +292,71 @@ fn test_try_make_response() { let (preamble, payload) = response.destruct(); assert_eq!(preamble.status_code, 404); + + // generated events + let response = responses.remove(0); + debug!( + "Response:\n{}\n", + std::str::from_utf8(&response.try_serialize().unwrap()).unwrap() + ); + + let resp = response.decode_call_readonly_response().unwrap(); + + assert!(resp.okay); + assert!(resp.result.is_some()); + assert!(resp.cause.is_none()); + + // Ok(u1) + assert_eq!( + resp.result.unwrap(), + "0x070100000000000000000000000000000001" + ); + + let events = resp.events.unwrap(); + + assert_eq!(events.len(), 7); + assert_eq!( + events[0].sender, + "ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world" + ); + assert_eq!(events[0].key, "print"); + assert_eq!(events[0].value, "0000000000000000000000000000000064"); // 100 + assert_eq!( + events[1].sender, + "ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world" + ); + assert_eq!(events[1].key, "print"); + assert_eq!(events[1].value, "01000000000000000000000000000003e8"); // u1000 + assert_eq!( + events[2].sender, + "ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world" + ); + assert_eq!(events[2].key, "print"); + assert_eq!(events[2].value, "0d0000000474657374"); // "test" + assert_eq!( + events[3].sender, + "ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world" + ); + assert_eq!(events[3].key, "print"); + assert_eq!(events[3].value, "03"); // true + assert_eq!( + events[4].sender, + "ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world" + ); + assert_eq!(events[4].key, "print"); + assert_eq!(events[4].value, "0d0000000578797a7a79"); // "xyzzy" + assert_eq!( + events[5].sender, + "ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world" + ); + assert_eq!(events[5].key, "print"); + assert_eq!(events[5].value, "0d0000000578797a7a77"); // "xyzzw" + assert_eq!( + events[6].sender, + "ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R.hello-world" + ); + assert_eq!(events[6].key, "print"); + assert_eq!(events[6].value, "0d0000000471757578"); // "quux" } #[test] diff --git a/stackslib/src/net/api/tests/getclaritymetadata.rs b/stackslib/src/net/api/tests/getclaritymetadata.rs index 649ba5aa71..fe6e2d63c5 100644 --- a/stackslib/src/net/api/tests/getclaritymetadata.rs +++ b/stackslib/src/net/api/tests/getclaritymetadata.rs @@ -309,7 +309,7 @@ fn test_try_make_response() { // contract size metadata let response = responses.remove(0); let resp = response.decode_clarity_metadata_response().unwrap(); - assert_eq!(resp.data, "1432"); + assert_eq!(resp.data, "1786"); // data map metadata let response = responses.remove(0); @@ -352,7 +352,7 @@ fn test_try_make_response() { // contract size metadata let response = responses.remove(0); let resp = response.decode_clarity_metadata_response().unwrap(); - assert_eq!(resp.data, "1432"); + assert_eq!(resp.data, "1786"); // unknwnon data var let response = responses.remove(0); diff --git a/stackslib/src/net/api/tests/mod.rs b/stackslib/src/net/api/tests/mod.rs index aacc86a0fc..9ed6573208 100644 --- a/stackslib/src/net/api/tests/mod.rs +++ b/stackslib/src/net/api/tests/mod.rs @@ -160,6 +160,21 @@ const TEST_CONTRACT: &str = " max-neighbors: u32, hint-replicas: (list ) })) + + (define-public (quux) + (begin (print \"quux\") (ok u1))) + (define-public (xyzzw) + (begin (print \"xyzzw\") (quux))) + (define-public (xyzzy) + (begin (print \"xyzzy\") (xyzzw))) + + (define-read-only (printer) + (begin + (print 100) + (print u1000) + (print \"test\") + (print true) + (xyzzy))) "; const TEST_CONTRACT_UNCONFIRMED: &str = "