Integrating Cartesi Rollups with Chronicle Oracles
Co-Author Michael Asiedu
Blockchain development often requires more than just smart contracts; one exciting point is integrating data from the real world. Oracles generally serve as that bridge, delivering reliable external data to your smart contracts.
In this guide, we’ll walk you through integrating your Cartesi Rollups application with Chronicle Oracles and building a decentralized application (dApp) that fetches data from a Chronicle oracle and processes it using Cartesi’s powerful computational layer.
Whether you’re a seasoned blockchain developer or someone new to the space, this article will help you understand the intricacies of building your Cartesi Rollups dApps combined with Chronicle oracles. By the end of this tutorial, you’ll have a working dApp that showcases the power of off-chain computations integrated with real-world data.
What can you do with all this? Imagine you can have a solution with reliable data being processed in a verifiable and robust machine-learning model. The possibilities are endless.
About Cartesi Rollups and Chronicle Oracles
Before we dig into the code, let`s talk a little bit about the main parts of this solution and why them. With Cartesi Rollups, you can run complex operations off-chain within a Linux environment, making your dApp cost-effective and scalable. Remember that verifiable machine learning model? With Cartesi Rollups, it is an easy task. While it isn’t feasible on the Ethereum mainnet, you can run this heavy processing off-chain, all while maintaining the security of the blockchain.
But “Why oracles, you might be wondering?”. Think of an oracle as the postal service of blockchain. It takes information from the outside world (off-chain) and delivers it securely to your smart contract (on-chain). However, like any postal service, the reliability of the delivery is key.
Chronicle oracles ensure that this data delivery is secure and immutable, which is crucial for applications like DeFi, where inaccurate data can lead to significant financial loss. It ensures that your dApp can trust the data it’s using. It is a blockchain-agnostic protocol that can be deployed quickly to almost any chain, enhancing the synergy with Cartesi Rollups even more, which is also EVM compatible.
In this guide, we’ll use Chronicle oracles to fetch data and feed it into Cartesi Rollups for processing. We created a simple template that you can reuse freely to create your application using these two main parts. Let`s get started!
The solution
In this dApp, we’ll be deploying a smart contract on the Sepolia Testnet that integrates with Chronicle’s oracles, enabling your application to fetch and process real-time data. This contract, OracleCartesiReader.sol
, will be responsible for collecting data from the Chronicle Oracle and feeding it into Cartesi's InputBox for further processing. Below, we can check what the solution architecture looks like.
Let`s split the practical part into three main parts: We will create and deploy the new contract called OracleCartesiReader.sol
. After that, we will create a simple backend code for Cartesi Rollups that gives the information we collected on the Oracle back to the user. And at the end, we will see how we utilize the frontend we are providing.
OracleCartesiReader
In Chronicle`s developer documentation, we have an easy example of how to consume Oracle information. It provides us an OracleReader.sol
code example where we can simply deploy it through remix. They set the Oracle address for Oracle (in this case, ETH_USD price oracle) and the SelfKisser Address. For more information about those parts, refer to the Chronicle Documentation here.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;
/**
* @title OracleReader
* @notice A simple contract to read from Chronicle oracles
* @dev To see the full repository, visit https://github.com/chronicleprotocol/OracleReader-Example.
* @dev Addresses in this contract are hardcoded for the Sepolia testnet.
* For other supported networks, check the https://chroniclelabs.org/dashboard/oracles.
*/
contract OracleReader {
/**
* @notice The Chronicle oracle to read from.
* Chronicle_ETH_USD_3:0xdd6D76262Fd7BdDe428dcfCd94386EbAe0151603
* Network: Sepolia
*/
IChronicle public chronicle = IChronicle(address(0xdd6D76262Fd7BdDe428dcfCd94386EbAe0151603));
/**
* @notice The SelfKisser granting access to Chronicle oracles.
* SelfKisser_1:0x0Dcc19657007713483A5cA76e6A7bbe5f56EA37d
* Network: Sepolia
*/
ISelfKisser public selfKisser = ISelfKisser(address(0x0Dcc19657007713483A5cA76e6A7bbe5f56EA37d));
constructor() {
// Note to add address(this) to chronicle oracle's whitelist.
// This allows the contract to read from the chronicle oracle.
selfKisser.selfKiss(address(chronicle));
}
/**
* @notice Function to read the latest data from the Chronicle oracle.
* @return val The current value returned by the oracle.
* @return age The timestamp of the last update from the oracle.
*/
function read() external view returns (uint256 val, uint256 age) {
(val, age) = chronicle.readWithAge();
}
}
// Copied from [chronicle-std](https://github.com/chronicleprotocol/chronicle-std/blob/main/src/IChronicle.sol).
interface IChronicle {
/**
* @notice Returns the oracle's current value.
* @dev Reverts if no value set.
* @return value The oracle's current value.
*/
function read() external view returns (uint256 value);
/**
* @notice Returns the oracle's current value and its age.
* @dev Reverts if no value set.
* @return value The oracle's current value using 18 decimals places.
* @return age The value's age as a Unix Timestamp .
* */
function readWithAge() external view returns (uint256 value, uint256 age);
}
// Copied from [self-kisser](https://github.com/chronicleprotocol/self-kisser/blob/main/src/ISelfKisser.sol).
interface ISelfKisser {
/// @notice Kisses caller on oracle `oracle`.
function selfKiss(address oracle) external;
}
But for our solution, we need more than that. We should also integrate it with a call for the Cartesi InputBox Contract, which is responsible for receiving the inputs that will be processed by the Cartesi Rollups Node. Let`s start integrating an interface for this. Let`s also add an event emiter to be used after we send the data to the InputBox contract.
interface IInputBox {
function addInput(address _dapp, bytes calldata _input) external returns (bytes32);
}
event PriceRelayed(address indexed dapp, uint256 price, uint256 age, bytes32 inputId);
We also have to change the Contract Constructor to also add the InputBox variable.
constructor(
address _inputBoxAddress,
address _chronicleAddress,
address _selfKisserAddress
) {
inputBox = IInputBox(_inputBoxAddress);
chronicle = IChronicle(_chronicleAddress);
selfKisser = ISelfKisser(_selfKisserAddress);
// Add this contract to the chronicle oracle's whitelist
selfKisser.selfKiss(address(chronicle));
}
Finally, we should send the data from the Oracle to the Cartesi InputBox through the addInput Function. For that, we will create a relayPrice function, which reads the Oracle Data and sends it to the InputBox. We added simple JSON formatting for easy receiving on the dApp's backend, which is unnecessary; you could convert the data on the backend side as well.
function relayPrice(address _dappAddress) external returns (bytes32) {
(uint256 price, uint256 age) = read();
// Construct the JSON string
string memory jsonString = string(abi.encodePacked(
'{"ethUsdPrice":',
Strings.toString(price),
',"timestamp":',
Strings.toString(age),
'}'
));
// Add the input to the InputBox
bytes32 inputId = inputBox.addInput(_dappAddress, bytes(jsonString));
emit PriceRelayed(_dappAddress, price, age, inputId);
return inputId;
}
Our OracleCartesiReader.sol
contract is complete. Let`s see the full code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;
import "@openzeppelin/contracts/utils/Strings.sol";
interface IInputBox {
function addInput(address _dapp, bytes calldata _input) external returns (bytes32);
}
interface IChronicle {
function readWithAge() external view returns (uint256 value, uint256 age);
}
interface ISelfKisser {
function selfKiss(address oracle) external;
}
/// @title OracleCartesiReader
/// @notice This contract relays ETH/USD price from a Chronicle oracle to an InputBox
/// @dev This contract combines the functionality of oracle reading and price relaying
contract OracleCartesiReader {
/// @notice The InputBox contract for adding inputs
IInputBox immutable public inputBox;
/// @notice The Chronicle oracle contract for reading ETH/USD price
IChronicle immutable public chronicle;
/// @notice The SelfKisser contract for whitelisting
ISelfKisser immutable public selfKisser;
event PriceRelayed(address indexed dapp, uint256 price, uint256 age, bytes32 inputId);
/// @notice Initializes the contract with necessary addresses
/// @param _inputBoxAddress Address of the InputBox contract
/// @param _chronicleAddress Address of the Chronicle oracle contract
/// @param _selfKisserAddress Address of the SelfKisser contract
constructor(
address _inputBoxAddress,
address _chronicleAddress,
address _selfKisserAddress
) {
inputBox = IInputBox(_inputBoxAddress);
chronicle = IChronicle(_chronicleAddress);
selfKisser = ISelfKisser(_selfKisserAddress);
// Add this contract to the chronicle oracle's whitelist
selfKisser.selfKiss(address(chronicle));
}
/// @notice Reads the current ETH/USD price and its age from the Chronicle oracle
/// @return val The current ETH/USD price
/// @return age The age of the price data
function read() public view returns (uint256 val, uint256 age) {
return chronicle.readWithAge();
}
/// @notice Relays the current ETH/USD price to the specified dApp address in JSON format
/// @param _dappAddress The address of the dApp to receive the price data
/// @return The bytes32 identifier of the added input
/// @dev This function reads the price, formats it as JSON, and sends it to the InputBox
function relayPrice(address _dappAddress) external returns (bytes32) {
(uint256 price, uint256 age) = read();
// Construct the JSON string
string memory jsonString = string(abi.encodePacked(
'{"ethUsdPrice":',
Strings.toString(price),
',"timestamp":',
Strings.toString(age),
'}'
));
// Add the input to the InputBox
bytes32 inputId = inputBox.addInput(_dappAddress, bytes(jsonString));
emit PriceRelayed(_dappAddress, price, age, inputId);
return inputId;
}
}
With that in place, you can deploy the contract on Remix.
Next, let’s deploy the smart contract that will interface with Chronicle Oracles and Cartesi Rollups.
- Open Remix IDE
Start by opening Remix IDE. - Create a New File
In Remix, create a new Solidity file namedOracleCartesiReader.sol
and paste the contract code from thecontracts
directory of the repository. - Compile the Contract
Select the appropriate Solidity compiler version (e.g., 0.8.18) and compile your contract. - Deploy to Sepolia
Connect your MetaMask wallet to the Sepolia testnet and deploy the contract using Remix’s Deploy & Run module.
In the deploy option, you must fill out the three addresses for the InputBox, Chronicle Oracle, and the SelfKisser. For the Chronicle Oracle and SelfKisser you can get it here. For the InputBox, you can run the cartesi address-book and get the InputBox address listed there.
5. Make Note of the Contract Address
After deployment, note down the contract address. You’ll need this address to configure the front end. The deployed contract will now fetch data from the Chronicle Oracle and send it to Cartesi Rollups via the InputBox.
Simple Cartesi Backend
Now, let`s mount a simple Cartesi Rollups backend. Following the main documentation here, you can set up everything needed to build and run Cartesi Applications.
Let`s create a backend running the command below:
cartesi create backend --template javascript
This command creates a backend
directory with essential files for your dApp development.
Dockerfile
: Contains configurations to build a complete Cartesi machine with your app's dependencies. Your backend code will run in this environment.README.md
: A markdown file with basic information and instructions about your dApp.src folder
: A common javascript folder with an index.js file where you can write your backend logic.
Let`s change the index.ts code to be like the below:
const { ethers } = require("ethers");
const rollup_server = process.env.ROLLUP_HTTP_SERVER_URL;
console.log("HTTP rollup_server url is " + rollup_server);
async function handle_advance(data) {
console.log("Received advance request data " + JSON.stringify(data));
try {
const response = await createNotice(data);
console.log(`Notice created successfully: ${JSON.stringify(data)}. Return code is: ${response.status}`);
return "accept";
} catch (error) {
console.log(`Notice was not created: ${error}`);
return "reject";
}
}
async function handle_inspect(data) {
console.log("Received inspect request data " + JSON.stringify(data));
return "accept";
}
async function createNotice(data) {
try {
const response = await fetch(rollup_server + '/notice', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ payload: data.payload })
});
return response;
} catch (error) {
throw error;
}
}
var handlers = {
advance_state: handle_advance,
inspect_state: handle_inspect,
};
var finish = { status: "accept" };
(async () => {
while (true) {
const finish_req = await fetch(rollup_server + "/finish", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ status: "accept" }),
});
console.log("Received finish status " + finish_req.status);
if (finish_req.status == 202) {
console.log("No pending rollup request, trying again");
} else {
const rollup_req = await finish_req.json();
var handler = handlers[rollup_req["request_type"]];
finish["status"] = await handler(rollup_req["data"]);
}
}
})();
We also code the AdvanceHandler to output a notice with whatever it receives so it can be read by the front end, namely the user. We can refer to the whole documentation here about how to build, create, and run Cartesi Applications. You could have chosen any language you wanted; in this case, we used Typescript.
Follow the steps here to deploy this dApp on the test net. In our case, we will be deploying on Sepolia. Following the steps in the main documentation is really straightforward. Again, the process also outputs the dApp address, which we will need to use on our front end to interact with it.
Run a node on your machine or in a cloud provider, as mentioned in the documentation.
Mounting the Frontend
Considering our main architecture, the front end is responsible for interacting with the deployed contract and also showing the user the information we processed in the backend. We created a small frontend where you can simply reuse for this. Also, all the files created above can also be found here. You are free to clone this GitHub repository:
git clone https://github.com/Mugen-Builders/cartesi-chronicle-integration/
With the backend and contracts in place, it’s time to set up the frontend, where users will interact with your dApp.
- Navigate to the Frontend Directory
The frontend of our dApp is a React application that communicates with the Cartesi Rollups and the deployed smart contract:
cd ../frontend
2. Install Dependencies
Before running the frontend, install all the necessary dependencies
npm install
3. Run the Frontend
Start the frontend application with the following command:
npm run dev
In the frontend we have here, which is a simple react-frontend you will find options to fill out the dApp address and the OracleCartesiReader address, which we have both after the deployment steps. You must log in with your metamask wallet into the Sepolia Testnet. The two action buttons are in place, one for sending the transaction and the other for fetching the recent data from the backend.
When filling out the addresses, press the Send Oracle Data To Cartesi
button to send the transaction. Your metamask will pop up with the transaction details to be confirmed. Then you can Press Fetch Data
Button to see all the Recent Oracle Data that is on the Cartesi Node:
Around this, we have deployed an integration example here with the simplest flow of getting data from Chronicle Oracle, send to the cartesi node, and getting this data back. You are free to check this out here :
Frontend — https://chronicle-cartesi-integration.vercel.app/
1. Fill in the Oracle address with: 0xb53ff09032d9144c8c3d6eae14427492cd6138cb
2. Fill the dApp address with: 0xe9e5BdE6F1051A0e36055dDE027E5764BE6bA104
Congratulations! You’ve successfully integrated Chronicle Oracles with Cartesi Rollups. This guide has walked you through the entire process, from understanding the importance of oracles to deploying a fully functional dApp that fetches and processes real-world data.
This integration opens up endless possibilities for your dApps, allowing you to create complex applications that rely on off-chain data while leveraging Cartesi’s computational power. For instance, instead of just giving back the data we read in the backend, we could process it in any way we wish. Whether you’re working on a DeFi project, a game, or any other blockchain application, this template will serve as a strong foundation.
I hope this tutorial was helpful and informative. If you have any questions or run into issues, feel free to reach out in the comments or connect with the Cartesi Community. I will be there and thrilled to answer any questions!
Happy coding!