Getting started with Rooch
1. What is Rooch
Rooch (opens in a new tab) is a fast, modular, secure, developer-friendly infrastructure solution for building Web3-native applications.
Rooch released the first version on June 28, 2023, the version name is Sprouting, and the version number is v0.1
.
2. Install Rooch
2.1 Download
Prebuilt binary tarballs and corresponding source tarballs can be found on Rooch's GitHub releases page (opens in a new tab). If you want to experience the Git version, you can refer to this page to compile and install Rooch (opens in a new tab). The following guides you to install the Release version of Rooch.
wget https://github.com/rooch-network/rooch/releases/download/v0.1/rooch-ubuntu-latest.zip
Note: Please choose the version corresponding to your own system. I will use the Linux version to demonstrate here.
2.2 Decompress
unzip rooch-ubuntu-latest.zip
The decompressed files are stored in the rooch-artifacts
directory, and rooch
is our precompiled binary program.
rooch-artifacts
├── README.md
└── rooch
2.3 Run
Go to the unzipped folder rooch-artifacts
and test if the program works.
cd rooch-artifacts
./rooch
If you can see the output below, it means everything is working fine.
rooch 0.1.0
Rooch Contributors <opensource@rooch.network>
USAGE:
rooch <SUBCOMMAND>
OPTIONS:
-h, --help Print help information
-V, --version Print version information
SUBCOMMANDS:
account Tool for interacting with accounts
event Tool for interacting with event
help Print this message or the help of the given subcommand(s)
init Tool for init with rooch
move CLI frontend for the Move compiler and VM
object Get object by object id
resource Get account resource by tag
server Start Rooch network
state Get states by accessPath
transaction Tool for interacting with transaction
2.4 Add to PATH
For the convenience of subsequent use, it is recommended to put rooch
into a path that can be retrieved by the system environment variable PATH
, or export
the current decompressed directory to PATH
through export.
- Method 1, copy the
rooch
program to the/usr/local/bin
directory (recommended)
sudo cp rooch /usr/local/bin
- Method 2, export path (not recommended)
Use the following small script to add rooch
to the Bash shell's PATH
.
echo "export PATH=\$PATH:$PWD" >> ~/.bashrc
source ~/.bashrc
3. Initialize Rooch configuration
rooch init
After running the command to initialize the configuration, a .rooch
directory will be created in the user's home directory ($HOME
), and the relevant configuration information of the Rooch account will be generated.
4. Create a new Rooch project
This part will guide you how to create a blog contract application on Rooch, and realize the basic CRUD functions.
4.1 Create a Move project
Use the rooch
integration's move new
command to create a blog application called simple_blog.
rooch move new simple_blog
The generated Move project contains a configuration file Move.toml
and a sources
directory for storing Move source code.
simple_blog
├── Move.toml
└── sources
We can take a quick look at what the Move.toml
file contains.
[package]
name = "simple_blog"
version = "0.0.1"
[dependencies]
MoveStdlib = { git = "https://github.com/rooch-network/rooch.git", subdir = "moveos/moveos-stdlib/move-stdlib", rev = "main" }
MoveosStdlib = { git = "https://github.com/rooch-network/rooch.git", subdir = "moveos/moveos-stdlib/moveos-stdlib", rev = "main" }
RoochFramework = { git = "https://github.com/rooch-network/rooch.git", subdir = "crates/rooch-framework", rev = "main" }
[addresses]
simple_blog = "_"
std = "0x1"
moveos_std = "0x2"
rooch_framework = "0x3"
- There are three tables in the TOML file:
package
,dependencies
andaddresses
, which store some meta information required by the project. - The
package
table is used to store some description information of the project, which contains two key-value pairsname
andversion
to describe the project name and version number of the project. - The
dependencies
table is used to store the metadata that the project depends on. - The
addresses
table is used to store the address of the project and the addresses of the modules that the project depends on. The first address is the address generated in$HOME/.rooch/rooch_config/rooch.yaml
when initializing the Rooch configuration.
In order to facilitate the deployment of other developers, we replace the address of simple_blog
with _
, and then specify it through --named--addresses
when deploying.
4.2 Quick experience
In this section, I will guide you to write a blog initialization function and run it in Rooch to experience the basic process of writing -> compiling -> publishing -> calling
the contract.
We create a new blog.move
file in the sources
directory and start writing our blog contract.
4.2.1 Define the structure of the blog
Our blogging system allows everyone to create their own blog and keep their own list of blogs. First, we define a blog structure:
struct MyBlog has key {
name: String,
articles: vector<ObjectID>,
}
This structure contains two fields, one is the name of the blog, and the other is the list of blog posts. For the article list, we only save the ID of the article Object.
Then define a function to create a blog:
public fun create_blog(ctx: &mut Context, owner: &signer) {
let articles = vector::empty();
let myblog = MyBlog{
name: string::utf8(b"MyBlog"),
articles,
};
account_storage::global_move_to(ctx, owner, myblog);
}
public entry fun set_blog_name(ctx: &mut Context, owner: &signer, blog_name: String) {
assert!(std::string::length(&blog_name) <= 200, error::invalid_argument(EDATA_TOO_LONG));
let owner_address = signer::address_of(owner);
// if blog not exist, create it
if(!account_storage::global_exists<MyBlog>(ctx, owner_address)){
create_blog(ctx, owner);
};
let myblog = account_storage::global_borrow_mut<MyBlog>(ctx, owner_address);
myblog.name = blog_name;
}
Creating a blog is to initialize the MyBlog
data structure and save MyBlog
in the user's storage space. At the same time, an entry function for setting the blog name is provided. If the blog does not exist, the blog will be created first, and then the blog name will be set.
Then provide a contract initialization function, which will be automatically executed when the contract is published, and the blog will be automatically initialized for the user who publishes the contract.
/// This init function is called when the module is published
/// The owner is the address of the account that publishes the module
fun init(ctx: &mut Context, owner: &signer) {
// auto create blog for module publisher
create_blog(ctx, owner);
}
Then, provide a function to query the blog list and a function to add and delete articles. The whole code is as follows:
module simple_blog::blog {
use std::error;
use std::signer;
use std::string::{Self,String};
use std::vector;
use moveos_std::object_id::ObjectID;
use moveos_std::context::Context;
use moveos_std::account_storage;
const EDATA_TOO_LONG: u64 = 1;
const ENOT_FOUND: u64 = 2;
struct MyBlog has key {
name: String,
articles: vector<ObjectID>,
}
/// This init function is called when the module is published
/// The owner is the address of the account that publishes the module
fun init(ctx: &mut Context, owner: &signer) {
// auto create blog for module publisher
create_blog(ctx, owner);
}
public fun create_blog(ctx: &mut Context, owner: &signer) {
let articles = vector::empty();
let myblog = MyBlog{
name: string::utf8(b"MyBlog"),
articles,
};
account_storage::global_move_to(ctx, owner, myblog);
}
public entry fun set_blog_name(ctx: &mut Context, owner: &signer, blog_name: String) {
assert!(std::string::length(&blog_name) <= 200, error::invalid_argument(EDATA_TOO_LONG));
let owner_address = signer::address_of(owner);
// if blog not exist, create it
if(!account_storage::global_exists<MyBlog>(ctx, owner_address)){
create_blog(ctx, owner);
};
let myblog = account_storage::global_borrow_mut<MyBlog>(ctx, owner_address);
myblog.name = blog_name;
}
fun add_article_to_myblog(ctx: &mut Context, owner: &signer, article_id: ObjectID) {
let owner_address = signer::address_of(owner);
// if blog not exist, create it
if(!account_storage::global_exists<MyBlog>(ctx, owner_address)){
create_blog(ctx, owner);
};
let myblog = account_storage::global_borrow_mut<MyBlog>(ctx, owner_address);
vector::push_back(&mut myblog.articles, article_id);
}
fun delete_article_from_myblog(ctx: &mut Context, owner: &signer, article_id: ObjectID) {
let owner_address = signer::address_of(owner);
let myblog = account_storage::global_borrow_mut<MyBlog>(ctx, owner_address);
let (contains, index) = vector::index_of(&myblog.articles, &article_id);
assert!(contains, error::not_found(ENOT_FOUND));
vector::remove(&mut myblog.articles, index);
}
/// Get owner's blog's articles
public fun get_blog_articles(ctx: &Context, owner_address: address): vector<ObjectID> {
if(!account_storage::global_exists<MyBlog>(ctx, owner_address)){
vector::empty()
}else{
let myblog = account_storage::global_borrow<MyBlog>(ctx, owner_address);
myblog.articles
}
}
}
module simple_blog::blog
is used to declare which module our contract belongs to. Its syntax ismodule address::module_name
, and the logic (function) of the contract is written in curly braces{}
.- The
use
statement imports the libraries we need to depend on when writing contracts. const
defines the constants used in the contract, usually used to define some error codes.fun
is a keyword used to define a function, usually the function of the function is defined here. For safety, such functions are prohibited from being called directly on the command line, and the calling logic needs to be encapsulated in the entry function.entry fun
is used to define the entry function, and the function modified by theentry
modifier can be called directly on the command line like a script.
4.2.2 Compile the Move contract
Before publishing the contract, we need to compile our contract. Here, --named-addresses
is used to specify the address of the simple_blog
module as the default
address on the current device.
rooch move build --named-addresses simple_blog=default
After compiling, if there are no errors, you will see the message of Success
at the end.
UPDATING GIT DEPENDENCY https://github.com/rooch-network/rooch.git
UPDATING GIT DEPENDENCY https://github.com/rooch-network/rooch.git
UPDATING GIT DEPENDENCY https://github.com/rooch-network/rooch.git
UPDATING GIT DEPENDENCY https://github.com/rooch-network/rooch.git
UPDATING GIT DEPENDENCY https://github.com/rooch-network/rooch.git
UPDATING GIT DEPENDENCY https://github.com/rooch-network/rooch.git
INCLUDING DEPENDENCY MoveStdlib
INCLUDING DEPENDENCY MoveosStdlib
INCLUDING DEPENDENCY RoochFramework
BUILDING simple_blog
Success
At this time, there will be a build
directory in the project folder, which stores the contract bytecode file generated by the Move compiler and the complete code of the contract.
4.2.3 Running the Rooch server
Let's open another terminal and run the following command, the Rooch server will start the Rooch container service locally to process and respond to the relevant behavior of the contract.
rooch server start
After starting the Rooch service, you will see these two messages at the end, indicating that the Rooch service has been started normally.
2023-07-03T15:44:33.315228Z INFO rooch_rpc_server: JSON-RPC HTTP Server start listening 0.0.0.0:50051
2023-07-03T15:44:33.315256Z INFO rooch_rpc_server: Available JSON-RPC methods : ["wallet_accounts", "eth_blockNumber", "eth_getBalance", "eth_gasPrice", "net_version", "eth_getTransactionCount", "eth_sendTransaction", "rooch_sendRawTransaction", "rooch_getAnnotatedStates", "eth_sendRawTransaction", "rooch_getTransactions", "rooch_executeRawTransaction", "rooch_getEventsByEventHandle", "rooch_getTransactionByHash", "rooch_executeViewFunction", "eth_getBlockByNumber", "rooch_getEvents", "eth_feeHistory", "eth_getTransactionByHash", "eth_getBlockByHash", "eth_getTransactionReceipt", "rooch_getTransactionInfosByOrder", "eth_estimateGas", "eth_chainId", "rooch_getTransactionInfosByHash", "wallet_sign", "rooch_getStates"]
Tip: When we operate the contract-related logic (function) in the previous terminal window, we can observe the output of this terminal window.
4.2.4 Publish the Move contract
rooch move publish --named-addresses simple_blog=default
You can confirm that the publish operation was successfully executed when you see output similar to this (status
is executed
):
{
//...
"execution_info": {
//...
"status": {
"type": "executed"
}
},
//...
}
You can also see the processing information of the response in the terminal running the Rooch service:
2023-07-03T16:02:11.691028Z INFO connection{remote_addr=127.0.0.1:58770 conn_id=0}: jsonrpsee_server::server: Accepting new connection 1/100
2023-07-03T16:02:13.690733Z INFO rooch_proposer::actor::proposer: [ProposeBlock] block_number: 0, batch_size: 1
4.2.5 Call the Move contract
At this point, our blog contract has been released to the chain, and the blog has been initialized under the default account. We can use the status query command to check the blog Resource under the account:
rooch state --access-path /resource/{ACCOUNT_ADDRESS}/{RESOURCE_TYPE}
Among them, {ACCOUNT_ADDRESS}
is the account address, {RESOURCE_TYPE}
is the resource type, here is {MODULE_ADDRESS}::blog::MyBlog
. Here {ACCOUNT_ADDRESS}
and {MODULE_ADDRESS}
are the default account addresses of my machine.
We can check the value corresponding to the active_address
key in the $HOME/.rooch/rooch_config/rooch.yaml
file, which is the default account address of the operation contract.
My address is 0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01
, and I will continue to use this address to demonstrate related operations.
So the command I actually execute here should be:
rooch state --access-path /resource/0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01/0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::blog::MyBlog
return result:
[
{
"state": {
"value": "0x064d79426c6f6700",
"value_type": "0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::blog::MyBlog"
},
"move_value": {
"abilities": 8,
"type": "0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::blog::MyBlog",
"value": {
"articles": [],
"name": "MyBlog"
}
}
}
]
It can be seen that MyBlog
Resource already exists, the name is the default MyBlog
, and the article list is empty.
Then we set the blog name through the set_blog_name
function. The syntax for calling a contract entry function is:
rooch move run --function {ACCOUNT_ADDRESS}::{MODULE_NAME}::{FUNCTION_NAME} --sender-account {ACCOUNT_ADDRESS}
Run a function with the rooch move run
command. --function
Specify the function name, you need to pass a complete function name, that is, the_address_of_the_published_contract::module_name::function_name
, in order to accurately identify the function that needs to be called. --sender-account
specifies the address of the account that calls this function, that is, which account is used to call this function, and anyone can call the contract on the chain.
rooch move run --function 0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::blog::set_blog_name --sender-account default --args 'string:Rooch blog'
When this command is executed, a transaction will be sent to the chain, and the content of the transaction is to call the set_blog_name
function in the blog contract.
After the execution is successful, run the previous status query command again, and view the blog Resource:
[
{
"state": {
"value": "0x0a526f6f636820626c6f6700",
"value_type": "0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::blog::MyBlog"
},
"move_value": {
"abilities": 8,
"type": "0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::blog::MyBlog",
"value": {
"articles": [],
"name": "Rooch blog"
}
}
}
]
As you can see, the blog name has been modified successfully. So far, we have experienced the zero-to-one installation in Rooch, initial configuration, project creation, writing contracts, compiling contracts, publishing contracts, and calling contracts.
4.3 Improve the blog contract
Next, we will continue to improve the blog contract and increase the function of CRUD blog posts.
4.3.1 Create blog post contract
We create another article.move
file in the sources
directory, which stores the data type of the article and the definition of related events for CRUD operations on the article.
Define the article data type, and the article event type:
struct Article has key {
version: u64,
title: String,
body: String,
}
struct ArticleCreatedEvent has copy,store {
id: ObjectID,
}
struct ArticleUpdatedEvent has copy,store {
id: ObjectID,
version: u64,
}
struct ArticleDeletedEvent has copy,store {
id: ObjectID,
version: u64,
}
The article data structure contains three fields, version
is used to record the version number of the article, title
is used to record the title of the article, and body
is used to record the content of the article.
Define a function to create an article:
/// Create article
public fun create_article(
ctx: &mut Context,
owner: &signer,
title: String,
body: String,
): ObjectID {
assert!(std::string::length(&title) <= 200, error::invalid_argument(EDATA_TOO_LONG));
assert!(std::string::length(&body) <= 2000, error::invalid_argument(EDATA_TOO_LONG));
let article = Article {
version: 0,
title,
body,
};
let owner_addr = signer::address_of(owner);
let article_obj = context::new_object_with_owner(
ctx,
owner_addr,
article,
);
let id = object::id(&article_obj);
context::add_object(ctx, article_obj);
let article_created_event = ArticleCreatedEvent {
id,
};
event::emit_event(ctx, article_created_event);
id
}
In this function, first check whether the length of the article title and content exceeds the limit. Then create the article object, add the article object to the object store, and finally send the article creation event and return the ID of the article.
Then define the modification function:
/// Update article
public fun update_article(
ctx: &mut Context,
owner: &signer,
id: ObjectID,
new_title: String,
new_body: String,
) {
assert!(std::string::length(&new_title) <= 200, error::invalid_argument(ErrorDataTooLong));
assert!(std::string::length(&new_body) <= 2000, error::invalid_argument(ErrorDataTooLong));
let article_obj = context::borrow_object_mut<Article>(ctx, id);
let owner_address = signer::address_of(owner);
// only article owner can update the article
assert!(object::owner(article_obj) == owner_address, error::permission_denied(ErrorNotOwnerAccount));
let article = object::borrow_mut(article_obj);
article.version = article.version + 1;
article.title = new_title;
article.body = new_body;
let article_update_event = ArticleUpdatedEvent {
id,
version: article.version,
};
event::emit(ctx, article_update_event);
}
In this function, first check whether the length of the new article title and content exceeds the limit. Then get the article object from the object store, check if the caller is the owner of the article, and throw an exception if not. Finally, update the version number, title and content of the article object, and send an article update event.
Then define the delete function:
/// Delete article
public fun delete_article(
ctx: &mut Context,
owner: &signer,
id: ObjectID,
) {
let article_obj = context::remove_object<Article>(ctx, id);
let owner_address = signer::address_of(owner);
// only article owner can delete the article
assert!(object::owner(&article_obj) == owner_address, error::permission_denied(ErrorNotOwnerAccount));
let article_deleted_event = ArticleDeletedEvent {
id,
version: object::borrow(&article_obj).version,
};
event::emit(ctx, article_deleted_event);
drop_article(article_obj);
}
In this function, first delete the article object from the object store, check whether the caller is the owner of the article, and throw an exception if not. Finally send the article delete event and destroy the article object.
Finally, we also need to provide a function to query articles by ID for use by other contracts:
/// get article object by id
public fun get_article(ctx: &Context, article_id: ObjectID): &Object<Article> {
context::borrow_object<Article>(ctx, article_id)
}
The complete contract code is as follows:
module simple_blog::article {
use std::error;
use std::signer;
use std::string::String;
use moveos_std::event;
use moveos_std::object::{Self, Object};
use moveos_std::object_id::ObjectID;
use moveos_std::context::{Self, Context};
const ErrorDataTooLong: u64 = 1;
const ErrorNotOwnerAccount: u64 = 2;
struct Article has key {
version: u64,
title: String,
body: String,
}
struct ArticleCreatedEvent has copy,store {
id: ObjectID,
}
struct ArticleUpdatedEvent has copy,store {
id: ObjectID,
version: u64,
}
struct ArticleDeletedEvent has copy,store {
id: ObjectID,
version: u64,
}
/// Create article
public fun create_article(
ctx: &mut Context,
owner: &signer,
title: String,
body: String,
): ObjectID {
assert!(std::string::length(&title) <= 200, error::invalid_argument(ErrorDataTooLong));
assert!(std::string::length(&body) <= 2000, error::invalid_argument(ErrorDataTooLong));
let article = Article {
version: 0,
title,
body,
};
let owner_addr = signer::address_of(owner);
let article_obj = context::new_object_with_owner(
ctx,
owner_addr,
article,
);
let id = object::id(&article_obj);
context::add_object(ctx, article_obj);
let article_created_event = ArticleCreatedEvent {
id,
};
event::emit(ctx, article_created_event);
id
}
/// Update article
public fun update_article(
ctx: &mut Context,
owner: &signer,
id: ObjectID,
new_title: String,
new_body: String,
) {
assert!(std::string::length(&new_title) <= 200, error::invalid_argument(ErrorDataTooLong));
assert!(std::string::length(&new_body) <= 2000, error::invalid_argument(ErrorDataTooLong));
let article_obj = context::borrow_object_mut<Article>(ctx, id);
let owner_address = signer::address_of(owner);
// only article owner can update the article
assert!(object::owner(article_obj) == owner_address, error::permission_denied(ErrorNotOwnerAccount));
let article = object::borrow_mut(article_obj);
article.version = article.version + 1;
article.title = new_title;
article.body = new_body;
let article_update_event = ArticleUpdatedEvent {
id,
version: article.version,
};
event::emit(ctx, article_update_event);
}
/// Delete article
public fun delete_article(
ctx: &mut Context,
owner: &signer,
id: ObjectID,
) {
let article_obj = context::remove_object<Article>(ctx, id);
let owner_address = signer::address_of(owner);
// only article owner can delete the article
assert!(object::owner(&article_obj) == owner_address, error::permission_denied(ErrorNotOwnerAccount));
let article_deleted_event = ArticleDeletedEvent {
id,
version: object::borrow(&article_obj).version,
};
event::emit(ctx, article_deleted_event);
drop_article(article_obj);
}
fun drop_article(article_obj: Object<Article>) {
let (_id, _owner, article) = object::unpack(article_obj);
let Article {
version: _version,
title: _title,
body: _body,
} = article;
}
/// Read function of article
/// get article object by id
public fun get_article(ctx: &Context, article_id: ObjectID): &Object<Article> {
context::borrow_object<Article>(ctx, article_id)
}
/// get article id
public fun id(article_obj: &Object<Article>): ObjectID {
object::id(article_obj)
}
/// get article version
public fun version(article_obj: &Object<Article>): u64 {
object::borrow(article_obj).version
}
/// get article title
public fun title(article_obj: &Object<Article>): String {
object::borrow(article_obj).title
}
/// get article body
public fun body(article_obj: &Object<Article>): String {
object::borrow(article_obj).body
}
}
4.3.2 Blog Contract Integration Article Contract
Next, we integrate the article contract in blog.move
and provide the entry function:
public entry fun create_article(
ctx: &mut Context,
owner: signer,
title: String,
body: String,
) {
let article_id = article::create_article(ctx, &owner, title, body);
add_article_to_myblog(ctx, &owner, article_id);
}
public entry fun update_article(
ctx: &mut Context,
owner: signer,
id: ObjectID,
new_title: String,
new_body: String,
) {
article::update_article(ctx, &owner, id, new_title, new_body);
}
public entry fun delete_article(
ctx: &mut Context,
owner: signer,
id: ObjectID,
) {
article::delete_article(ctx, &owner, id);
delete_article_from_myblog(ctx, &owner, id);
}
When creating and deleting articles, update the list of articles in the blog at the same time.
4.3.3 Creating blog posts
A test article can be created by submitting a transaction using the Rooch CLI like this:
rooch move run --function 0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::blog::create_article --sender-account default --args 'string:Hello Rooch' "string:Accelerating World's Transition to Decentralization"
--function
specifies to execute the create_article
function in the blog
module published at address 0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01
, that is, create a new blog post. --sender-account
specifies who should submit this transaction. This function requires us to pass two parameters to it, specified by --args
, the first is the title of the article, I named it Hello Rooch
; the second is the content of the article, I wrote the slogan of Rooch: Accelerating World's Transition to Decentralization
.
The parameter passed is a string, which needs to be wrapped in quotation marks and specified through string:
. There are single quotation marks in the content of the second parameter, so use double quotation marks to wrap it, otherwise you must use an escape character (\
).
You can freely change the content of the first parameter (title
) and the second parameter (body
) after --args
to create more articles.
4.3.4 Query Articles
Now, you can get the ObjectID
of the created article by querying the event:
curl --location --request POST 'http://localhost:50051' \
--header 'Content-Type: application/json' \
--data-raw '{
"id":101,
"jsonrpc":"2.0",
"method":"rooch_getEventsByEventHandle",
"params":["0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::article::ArticleCreatedEvent", null, 1000]
}'
The returned response content:
{"jsonrpc":"2.0","result":{"data":[{"event":{"event_id":{"event_handle_id":"0xc48dc675718370db4273a419875967e7c32615f907262d475730d8faf0afca44","event_seq":0},"type_tag":"0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::article::ArticleCreatedEvent","event_data":"0x1f27bd310f51b09915648d53319e65509dcc7ca42ffc1cf989bfa24073d78a41","event_index":0},"sender":"0x0000000000000000000000000000000000000000000000000000000000000000","tx_hash":null,"timestamp_ms":null,"parsed_event_data":{"abilities":13,"type":"0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::article::ArticleCreatedEvent","value":{"id":"0x1f27bd310f51b09915648d53319e65509dcc7ca42ffc1cf989bfa24073d78a41"}}}],"next_cursor":0,"has_next_page":false},"id":101}
Since there are many output contents, you can add a pipeline operation ( | jq '.result.data[0].parsed_event_data.value.id'
) at the end of the above command to quickly filter out the ObjectID
of the first article.
Tip: Before using the
jp
command (jq - commandline JSON processor), you may need to install it first.
The command after adding jp
processing looks like this:
curl --location --request POST 'http://localhost:50051' \
--header 'Content-Type: application/json' \
--data-raw '{
"id":101,
"jsonrpc":"2.0",
"method":"rooch_getEventsByEventHandle",
"params":["0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::article::ArticleCreatedEvent", null, 1000]
}' | jq '.result.data[0].parsed_event_data.value.id'
The object ID of the blog that is screened through jp
is:
"0x1f27bd310f51b09915648d53319e65509dcc7ca42ffc1cf989bfa24073d78a41"
Then, you can use the Rooch CLI to query the status of the object, passing --id
to specify the ID of the article object (replace it with the ObjectID of your article):
rooch state --access-path /object/0x1f27bd310f51b09915648d53319e65509dcc7ca42ffc1cf989bfa24073d78a41
[
{
"state": {
"value": "0x1f27bd310f51b09915648d53319e65509dcc7ca42ffc1cf989bfa24073d78a41bbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d0100000000000000000b48656c6c6f20526f6f636833416363656c65726174696e6720576f726c642773205472616e736974696f6e20746f20446563656e7472616c697a6174696f6e",
"value_type": "0x2::object::Object<0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::article::Article>"
},
"move_value": {
"abilities": 0,
"type": "0x2::object::Object<0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::article::Article>",
"value": {
"id": "0x1f27bd310f51b09915648d53319e65509dcc7ca42ffc1cf989bfa24073d78a41",
"owner": "0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01",
"value": {
"abilities": 8,
"type": "0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::article::Article",
"value": {
"body": "Accelerating World's Transition to Decentralization",
"title": "Hello Rooch",
"version": "0"
}
}
}
}
}
]
Pay attention to the two key-value pairs title
and body
in the output, and you can see that this object does 'store' the blog post you just wrote.
We can also use the previous command to query MyBlog
Resource under the account:
rooch state --access-path /resource/0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01/0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::blog::MyBlog
[
{
"state": {
"value": "0x0a526f6f636820626c6f67011f27bd310f51b09915648d53319e65509dcc7ca42ffc1cf989bfa24073d78a41",
"value_type": "0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::blog::MyBlog"
},
"move_value": {
"abilities": 8,
"type": "0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::blog::MyBlog",
"value": {
"articles": [
"0x1f27bd310f51b09915648d53319e65509dcc7ca42ffc1cf989bfa24073d78a41"
],
"name": "Rooch blog"
}
}
}
]
As you can see, the articles
field in MyBlog
stores the object ID of the article we just created.
4.3.5 Updating Articles
We try to update the content of an article using the update_article
function.
--function
specifies to execute the update
function in the blog
module published at address 0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01
, that is, to update a blog post. Also need to use --sender-account
to specify the account that will send this update article transaction. This function requires us to pass three parameters to it, specified by --args
, the first parameter is the object ID of the article to be modified, and the latter two parameters correspond to the title and body of the article respectively.
Change the title of the article to be Foo
and the body of the article to be Bar
:
rooch move run --function 0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::blog::update_article --sender-account default --args 'object_id:0x1f27bd310f51b09915648d53319e65509dcc7ca42ffc1cf989bfa24073d78a41' 'string:Foo' 'string:Bar'
In addition to using the Rooch CLI, you can also query the state of objects by calling JSON RPC:
curl --location --request POST 'http://127.0.0.1:50051/' \
--header 'Content-Type: application/json' \
--data-raw '{
"id":101,
"jsonrpc":"2.0",
"method":"rooch_getAnnotatedStates",
"params":["/object/0x1f27bd310f51b09915648d53319e65509dcc7ca42ffc1cf989bfa24073d78a41"]
}'
In the output, it can be observed that the title and body of the article are successfully modified:
{"jsonrpc":"2.0","result":[{"state":{"value":"0x1f27bd310f51b09915648d53319e65509dcc7ca42ffc1cf989bfa24073d78a41bbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01010000000000000003466f6f03426172","value_type":"0x2::object::Object<0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::article::Article>"},"move_value":{"abilities":0,"type":"0x2::object::Object<0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::article::Article>","value":{"id":"0x1f27bd310f51b09915648d53319e65509dcc7ca42ffc1cf989bfa24073d78a41","owner":"0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01","value":{"abilities":8,"type":"0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::article::Article","value":{"body":"Bar","title":"Foo","version":"1"}}}}}],"id":101}
4.3.6 Delete article
A transaction can be submitted like this, calling blog::delete_article
to delete an article:
rooch move run --function 0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::blog::delete_article --sender-account default --args 'object_id:0x1f27bd310f51b09915648d53319e65509dcc7ca42ffc1cf989bfa24073d78a41'
--function
specifies to execute the delete_article
function in the blog
module published at 0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01
, that is, to delete a blog post. Also need to use --sender-account
to specify the account to send this delete article transaction. This function only needs to pass one parameter to it, which is the object ID corresponding to the article, specified by --args
.
4.3.7 Check whether the article is deleted normally
rooch state --access-path /object/0x1f27bd310f51b09915648d53319e65509dcc7ca42ffc1cf989bfa24073d78a41
[
null
]
As you can see, when querying the object ID of the article, the result returns null
. The description article has been deleted. Then query MyBlog
Resource:
rooch state --access-path /resource/0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01/0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::blog::MyBlog
[
{
"state": {
"value": "0x0a526f6f636820626c6f6700",
"value_type": "0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::blog::MyBlog"
},
"move_value": {
"abilities": 8,
"type": "0xbbfc33692c7d57839fde9643681fb64c83b377e4c70b1e4b76aa35ff1e410d01::blog::MyBlog",
"value": {
"articles": [],
"name": "Rooch blog"
}
}
}
]
As you can see, the articles
array is empty, indicating that the article list has also been updated.
5. Summary
At this point, I believe you have a basic understanding of how Rooch v0.1 works, how to publish contracts, and how to interact with contracts. To experience more contract examples on Rooch, see rooch/examples
(opens in a new tab).
If you want to directly experience the functions of this blog contract, you can directly download the blog source code (opens in a new tab), decompress it, and switch to the root directory of the blog contract project. For the interactive method, please refer to the above.
wget https://github.com/rooch-network/rooch/archive/refs/heads/main.zip
unzip main.zip
cd rooch-main/examples/simple_blog