Query outputs
In Cartesi Rollups, outputs are essential in interacting with the blockchain. The direct output types are notices, reports and vouchers.
The JSON-RPC server unifies Notice and Vouchers into a single query, therefore a single request returns all Notices and Vouchers generated by the application.
Notices: Off-chain events
A notice is a verifiable data declaration that attests to off-chain events or conditions and is accompanied by proof. Unlike vouchers, notices do not trigger direct interactions with other smart contracts.
They serve as a means for application to notify the blockchain about particular events.
How Notices Work
-
The application backend creates a notice containing relevant off-chain data.
-
The notice is submitted to the Rollup Server as evidence of the off-chain event.
-
Notices are validated on-chain using the
validateOutput()function of theApplicationcontract.
Send a notice
Let's examine how an Application has its Advance request calculating and returning the first five multiples of a given number.
We will send the output to the rollup server as a notice.
- JavaScript
- Python
- Rust
- Go
- C++
function calculateMultiples(num) {
let multiples = "";
for (let i = 1; i <= 5; i++) {
multiples += (num * i).toString();
if (i < 5) {
multiples += ", ";
}
}
return multiples;
}
async function handle_advance(data) {
console.log("Received advance request data " + JSON.stringify(data));
const numberHex = data["payload"];
const number = parseInt(viem.hexToString(numberHex));
try {
const multiples = calculateMultiples(number);
console.log(`Adding notice with value ${multiples}`);
const hexresult = viem.stringToHex(multiples);
await fetch(rollup_server + "/notice", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ payload: hexresult }),
});
} catch (error) {
//Do something when there is an error
}
return "accept";
}
def hex2str(hex):
"""
Decodes a hex string into a regular string
"""
return bytes.fromhex(hex[2:]).decode("utf-8")
def str2hex(str):
"""
Encodes a string as a hex string
"""
return "0x" + str.encode("utf-8").hex()
def calculate_multiples(num):
multiples = ""
for i in range(1, 6):
multiples += str(num * i)
if i < 5:
multiples += ", "
return multiples
def handle_advance(data):
logger.info(f"Received advance request data {data}")
status = "accept"
try:
input = hex2str(data["payload"])
logger.info(f"Received input: {input}")
# Evaluates expression
multiples = calculate_multiples(int(input))
# Emits notice with result of calculation
logger.info(f"Adding notice with payload: '{multiples}'")
response = requests.post(
rollup_server + "/notice", json={"payload": str2hex(str(multiples))}
)
logger.info(
f"Received notice status {response.status_code} body {response.content}"
)
except Exception as e:
# Emits report with error message here
return status
fn calculate_multiples(num: i64) -> String {
let mut multiples = String::new();
for i in 1..=5 {
multiples.push_str(&(num * i).to_string());
if i < 5 {
multiples.push_str(", ");
}
}
multiples
}
fn hex_to_string(hex: &str) -> Result<String, Box<dyn std::error::Error>> {
let hexstr = hex.strip_prefix("0x").unwrap_or(hex);
let bytes = hex::decode(hexstr).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
let s = String::from_utf8(bytes).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
Ok(s)
}
async fn emit_notice( payload: String) -> Option<bool> {
let hex_string = {
let s = payload.strip_prefix("0x").unwrap_or(payload.as_str());
hex::encode(s.as_bytes())
};
let server_addr = env::var("ROLLUP_HTTP_SERVER_URL").expect("ROLLUP_HTTP_SERVER_URL not set");
let client = hyper::Client::new();
let response = object! {
"payload" => format!("0x{}", hex_string),
};
let request = hyper::Request::builder()
.method(hyper::Method::POST)
.header(hyper::header::CONTENT_TYPE, "application/json")
.uri(format!("{}/notice", server_addr))
.body(hyper::Body::from(response.dump()))
.ok()?;
let response = client.request(request).await;
match response {
Ok(_) => {
println!("Notice generation successful");
return Some(true);
}
Err(e) => {
println!("Notice request failed {}", e);
None
}
}
}
pub async fn handle_advance(
_client: &hyper::Client<hyper::client::HttpConnector>,
_server_addr: &str,
request: JsonValue,
) -> Result<&'static str, Box<dyn std::error::Error>> {
println!("Received advance request data {}", &request);
let payload = request["data"]["payload"]
.as_str()
.ok_or("Missing payload")?;
let number: i64 = hex_to_string(payload)?.parse()?;
let multiples = calculate_multiples(number);
println!("ADDING NOTICE WITH VALUE {}", multiples);
emit_notice(multiples).await;
Ok("accept")
}
package main
import (
"dapp/rollups"
"fmt"
"strconv"
"strings"
)
// calculateMultiples returns the first 5 multiples as: "n, 2n, 3n, 4n, 5n".
func calculateMultiples(num int64) string {
parts := make([]string, 0, 5)
for i := int64(1); i <= 5; i++ {
parts = append(parts, strconv.FormatInt(num*i, 10))
}
return strings.Join(parts, ", ")
}
// HandleAdvance parses an integer input and emits a notice with its first 5 multiples.
func HandleAdvance(data *rollups.AdvanceResponse) error {
// Payload is hex-encoded text, expected to contain a decimal integer (example: "7").
input, err := rollups.Hex2Str(data.Payload)
if err != nil {
return fmt.Errorf("HandleAdvance: invalid payload encoding: %w", err)
}
input = strings.TrimSpace(input)
value, err := strconv.ParseInt(input, 10, 64)
if err != nil {
return fmt.Errorf("HandleAdvance: invalid integer payload %q: %w", input, err)
}
// Compute result and publish as a notice payload.
result := calculateMultiples(value)
notice := rollups.NoticeRequest{Payload: rollups.Str2Hex(result)}
if _, err = rollups.SendNotice(¬ice); err != nil {
return fmt.Errorf("HandleAdvance: failed sending notice: %w", err)
}
return nil
}
#include <sstream>
#include <string>
#include "3rdparty/cpp-httplib/httplib.h"
#include "3rdparty/picojson/picojson.h"
// Build a comma-separated string with the first 5 multiples of num.
std::string calculate_multiples(long long num)
{
std::ostringstream result;
for (int i = 1; i <= 5; ++i)
{
result << (num * i);
if (i < 5)
{
result << ", ";
}
}
return result.str();
}
std::string handle_advance(httplib::Client &cli, picojson::value data)
{
std::cout << "Received advance request data " << data << std::endl;
try
{
// Use your template helper to decode hex payload into UTF-8 text.
// Example helper in Cartesi templates: hex_to_string("0x32") -> "2"
const std::string payload_hex = data.get("payload").get<std::string>();
const std::string input = hex_to_string(payload_hex);
// 2) Parse number, compute multiples.
const long long value = std::stoll(input);
const std::string multiples = calculate_multiples(value);
std::cout << "Adding notice with value " << multiples << std::endl;
// 3) Build and emit notice to the rollup server.
picojson::object notice;
// Use your template helper to encode UTF-8 text into hex.
// Example helper: string_to_hex("2, 4, 6, 8, 10")
notice["payload"] = picojson::value(string_to_hex(multiples));
const std::string body = picojson::value(notice).serialize();
auto response = cli.Post("/notice", body, "application/json");
if (!response || response->status >= 400)
{
std::cerr << "Failed to send notice" << std::endl;
}
}
catch (const std::exception &e)
{
std::cerr << "Error in handle_advance: " << e.what() << std::endl;
}
return "accept";
}
// ... rest of the Cartesi app code (finish loop, routing, and startup)
For example, sending an input payload of “2” to the application using Cast or cartesi send generic will log:
Received finish status 200
Received advance request data {"metadata":{"chain_id":31337,"app_contract":"0xef34611773387750985673f94067ea22db406f72","msg_sender":"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266","block_number":188,"block_timestamp":1741291197,"prev_randao":"0xa8b0097138c68949870aabe7aa4dc62a91b6dd0bc2b4582eac2be9eaee280032","input_index":0},"payload":"0x32"}
Adding notice with values 2, 4, 6, 8, 10
Vouchers: On-chain actions
A voucher in Cartesi Rollups is a mechanism for executing on-chain actions from an application.
Think of it as a digital authorization ticket that enables an application to perform specific actions on the blockchain, such as transferring assets or approving transactions.
How Vouchers Work
-
The application backend creates a voucher while executing in the Cartesi Machine.
-
The voucher specifies the action, such as a token swap, and is sent to the blockchain.
-
The
Applicationcontract executes the voucher using theexecuteOutput()function. -
The result is recorded on the base layer through claims submitted by a consensus contract.
create a voucherRefer to the documentation here for asset handling and creating vouchers in your application.
These notices and vouchers can be validated and queried by any interested party.
Query all notices and vouchers
Frontend clients can use a JSON-RPC API exposed by the Cartesi Nodes to query the state of a Cartesi Rollups instance.
The below snippet uses a frontend client to request for a list of all vouchers and notices from an application running in a local environment:
async function fetchOutputs() {
try {
const response = await fetch("http://127.0.0.1:6751/rpc", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
method: "cartesi_listOutputs",
params: {
application: "multiples-of-a-number", // APPLICATION NAME
limit: 10,
offset: 0
},
id: 1
})
});
const result = await response.json();
// Extract the array that lives in result.result.data
const outputs = result?.result?.data ?? [];
for (const output of outputs) {
const decoded = output.decoded_data;
if (!decoded) continue;
// Handle Notice
if (decoded.type === "Notice") {
const payload = decoded.payload; // hex string
console.log("Notice payload:", payload);
// Do something with the payload
}
// Handle Voucher
if (decoded.type === "Voucher") {
const { destination, value, payload } = decoded;
console.log("Voucher →", { destination, value, payload });
// Do something with the payload
}
}
} catch (error) {
console.error("Error fetching notices:", error);
}
}
fetchOutputs();
This can also be achieved via the terminal by running the below curl command:
curl -X POST http://127.0.0.1:6751/rpc \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "cartesi_listOutputs",
"params": {
"application": "multiples-of-a-number",
"limit": 10,
"offset": 0
},
"id": 1
}'
Query a single notice or voucher
You can retrieve detailed information about a single notice, including its proof information:
Here is the query which takes the variables: output_index and application then returns the details of a notice.
async function getOutput() {
try {
const response = await fetch("http://127.0.0.1:6751/rpc", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
method: "cartesi_getOutput",
params: {
application: "multiples-of-a-number", // APPLICATION NAME
output_index: "0x0" // INDEX OF THE OUTPUT (VOUCHER OR NOTICE)
},
id: 1
})
});
const result = await response.json();
console.log(result)
// Do something with the JSON result
} catch (error) {
console.error("Error getting output:", error);
}
}
getOutput();
The curl equivalent of this function would be:
curl -X POST http://127.0.0.1:6751/rpc \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "cartesi_getOutput",
"params": {
"application": "multiples-of-a-number",
"output_index": "0x0"
},
"id": 1
}'
Reports: Stateless logs
Reports serve as stateless logs, providing read-only information without affecting the state. They are commonly used for logging and diagnostics within the application.
Here is how you can write your application to send reports to the rollup server:
- JavaScript
- Python
- Rust
- Go
- C++
async function handle_advance(data) {
console.log("Received advance request data " + JSON.stringify(data));
try {
// something here
} catch (e) {
//Send a report when there is an error
const error = viem.stringToHex(`Error:${e}`);
await fetch(rollup_server + "/report", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ payload: error }),
});
return "reject";
}
return "accept";
}
def handle_advance(data):
logger.info(f"Received advance request data {data}")
status = "accept"
try:
# Do something here to emit a notice
except Exception as e:
# Emits report with error message here
status = "reject"
msg = f"Error processing data {data}\n{traceback.format_exc()}"
logger.error(msg)
response = requests.post(
rollup_server + "/report", json={"payload": msg}
)
logger.info(
f"Received report status {response.status_code} body {response.content}"
)
return status
pub async fn handle_advance(
_client: &hyper::Client<hyper::client::HttpConnector>,
_server_addr: &str,
request: JsonValue,
) -> Result<&'static str, Box<dyn std::error::Error>> {
println!("Received advance request data {}", &request);
let payload = request["data"]["payload"]
.as_str()
.ok_or("Missing payload")?;
let payload_string = hex_to_string(payload)?;
emit_report(payload_string).await;
Ok("accept")
}
fn hex_to_string(hex: &str) -> Result<String, Box<dyn std::error::Error>> {
let hexstr = hex.strip_prefix("0x").unwrap_or(hex);
let bytes = hex::decode(hexstr).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
let s = String::from_utf8(bytes).map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
Ok(s)
}
async fn emit_report( payload: String) -> Option<bool> {
let hex_string = {
let s = payload.strip_prefix("0x").unwrap_or(payload.as_str());
hex::encode(s.as_bytes())
};
let server_addr = env::var("ROLLUP_HTTP_SERVER_URL").expect("ROLLUP_HTTP_SERVER_URL not set");
let client = hyper::Client::new();
let response = object! {
"payload" => format!("0x{}", hex_string),
};
let request = hyper::Request::builder()
.method(hyper::Method::POST)
.header(hyper::header::CONTENT_TYPE, "application/json")
.uri(format!("{}/report", server_addr))
.body(hyper::Body::from(response.dump()))
.ok()?;
let response = client.request(request).await;
match response {
Ok(_) => {
println!("Report generation successful");
return Some(true);
}
Err(e) => {
println!("Report request failed {}", e);
None
}
}
}
package main
import (
"dapp/rollups"
"fmt"
)
// HandleAdvance demonstrates minimal report emission on error.
func HandleAdvance(data *rollups.AdvanceResponse) error {
// Example operation that may fail: decode payload.
if _, err := rollups.Hex2Str(data.Payload); err != nil {
// Keep report payload hex-encoded.
report := rollups.ReportRequest{
Payload: rollups.Str2Hex(fmt.Sprintf("Error: %v", err)),
}
if _, sendErr := rollups.SendReport(&report); sendErr != nil {
return fmt.Errorf("HandleAdvance: failed sending report: %w", sendErr)
}
return fmt.Errorf("HandleAdvance: rejected due to app error: %w", err)
}
return nil
}
#include <iostream>
#include <string>
#include "3rdparty/cpp-httplib/httplib.h"
#include "3rdparty/picojson/picojson.h"
// Minimal helper to emit a report payload.
static bool emit_report(httplib::Client &cli, const std::string &message)
{
picojson::object report;
// Keep report payload hex-encoded using your template helper.
report["payload"] = picojson::value(string_to_hex(message));
const std::string body = picojson::value(report).serialize();
auto response = cli.Post("/report", body, "application/json");
return response && response->status < 400;
}
std::string handle_advance(httplib::Client &cli, picojson::value data)
{
try
{
// Example operation that may fail: decode payload.
const std::string payload_hex = data.get("payload").get<std::string>();
(void)hex_to_string(payload_hex);
}
catch (const std::exception &e)
{
emit_report(cli, std::string("Error: ") + e.what());
return "reject";
}
return "accept";
}
You can use the exposed JSON-RPC API to query all reports from an application.
Query all reports
Frontend clients can also use the JSON-RPC API exposed by the Cartesi Nodes to query the state of a Cartesi Rollups instance as regards to emitted reports.
The below command is a function that calls the JSON-RPC API to request for all reports emitted by an application running locally:
async function fetchReports() {
try {
const response = await fetch("http://127.0.0.1:6751/rpc", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
"jsonrpc": "2.0",
"method": "cartesi_listReports",
"params": {
"application": "multiples-of-a-number",
"limit": 10,
"offset": 0
},
"id": 1
})
});
const result = await response.json();
// Extract the array that lives in result.result.data
const reports = result?.result?.data ?? [];
for (const report of reports) {
console.log(report);
// Do something with the REPORT
}
} catch (error) {
console.error("Error fetching report:", error);
}
}
await fetchReports();
This can also be achieved via the terminal by running the below curl command:
curl -X POST http://127.0.0.1:6751/rpc \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "cartesi_listReports",
"params": {
"application": "multiples-of-a-number",
"limit": 10,
"offset": 0
},
"id": 1
}'
Query a single report
Similar to Notice and Vouchers, you can retrieve a single report using its report_index. Though unlike Vouchers and Notices, reports are stateless.
async function getOutput() {
try {
const response = await fetch("http://127.0.0.1:6751/rpc", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
method: "cartesi_getReport",
params: {
application: "multiples-of-a-number", // APPLICATION NAME
report_index: "0x0" // INDEX OF THE REPORT
},
id: 1
})
});
const result = await response.json();
console.log(result)
// Do something with the JSON result
} catch (error) {
console.error("Error getting report:", error);
}
}
getOutput();
The curl equivalent of this function would be:
curl -X POST http://127.0.0.1:6751/rpc \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "cartesi_getReport",
"params": {
"application": "multiples-of-a-number",
"report_index": "0x0"
},
"id": 1
}'