Rooch Object
In Rooch, an Object is akin to a box model. Creating an Object is equivalent to creating a box in the state space, encapsulating an instance of type T
within it, with ObjectID
serving as the address of this box. Moreover, this box supports the dynamic addition of states, which is the dynamic field feature of an Object.
If we consider the state space of smart contracts as analogous to the heap memory of a program, an Object is similar to a smart pointer, providing references and operations on the state, as well as lifecycle management.
Ownership of Object
The owner
field of ObjectEntity
signifies which account address owns the Object. Through owner
, Objects can be categorized into:
SystemOwnedObject
: Objects withowner
as0x0
. After the creation of the object, it is defaulted toSystemOwnedObject
.UserOwnedObject
: Objects withowner
non-equal to0x0
. Once the Object is transferred to a user,owner
will be set as the address of that user.
Object Life Cycle
Creating an Object
An Object of type T
can be created by invoking object::new
method.
module moveos_std::object {
#[private_generics(T)]
public fun new<T: key>(value: T): Object<T>;
}
- This method is protected by
private_generics(T)
, hence, it can only be invoked by the module whereT
is located. The developer ofT
module gets to decide whether to provide the methods to encapsulateT
into theObject
. T
must has thekey
ability.- The ObjectID of this Object is a globally unique ID automatically assigned by the system.
In addition to the above normal Objects, two special Object creation methods are provided that do not automatically assign IDs; instead, they generate IDs through a predetermined algorithm, called Named Object.
module moveos_std::object {
#[private_generics(T)]
public fun new_named_object<T: key>(value: T): Object<T>;
#[private_generics(T)]
public fun new_account_named_object<T: key>(account: address, value: T): Object<T>;
}
- NamedObject: ObjectID is generated using type name of
T
. The generation formula issha3(type_name<T>())
. This is generally used for globally unique Objects, like0x2::object::Timestamp
. - Account NamedObject: ObjectID is generated using both the account address and type name of
T
. The generating formula issha3(account + type_name<T>())
. It's generally used for Objects of which each user owns only one, like0x3::coin_store::CoinStore<CoinType>
.
Operating an Object
Transferring Ownership
Transfer the Object to new_owner
:
module moveos_std::object {
public fun transfer<T: key + store>(self: Object<T>, new_owner: address);
}
The owner
retrieves their Object through object_id
:
module moveos_std::object {
public fun take_object<T: key + store>(owner: &signer, object_id: ObjectID): Object<T>;
}
Note: Once the Object is retrieved, the
owner
is set to0x0
, at which point the Object becomes aSystemOwnedObject
.
For the above methods, T
must has key + store
ability. Such types of Object are called PublicObject
, and the user can transfer the ownership of PublicObject
on their own.
If it is the type of Object that only has key
ability, we can call it PrivateObject
. Users cannot directly transfer the ownership of PrivateObject
, and the ownership transfer of PrivateObject
must be assisted by the API provided by the module where T
is located.
module moveos_std::object {
#[private_generics(T)]
public fun take_object_extend<T: key>(object_id: ObjectID): (address, Object<T>);
}
The take_object_extend
method is protected by private_generics(T)
, and can only be invoked by modules where T
is located. The developer gets to decide whether to provide the method to transfer PrivateObject
to other users.
Object Reference
Object<T>
can be referenced in two ways, one is read-only reference &Object<T>
, the other one is mutable reference &mut Object<T>
.
We can get &T
through object::borrow(&Object<T>)
method, and &mut T
through object::borrow_mut(&mut Object<T>)
. As for what operations can be performed after obtaining &T
and &mut<T>
, this is defined by T
's module.
There are two ways to get the Object reference:
- Passed in through the
entry
method.
entry fun my_entry(obj: &Object<MyStruct>, obj_mut: &mut Object<MyStruct>) {
// do something
}
- Obtained through
borrow_object
andborrow_mut_object
methods.
module moveos_std::object {
public fun borrow_object<T: key>(object_id: ObjectID): &Object<T>;
public fun borrow_mut_object<T: key>(owner: &signer, object_id: ObjectID): &mut Object<T>;
}
- Note, all Objects in Rooch are open to read, everyone can get any
&Object<T>
throughObjectID
. - The owner of the Object can get
&mut Object<T>
reference throughobject::borrow_mut_object
.
Method extension for developers:
module moveos_std::object {
#[private_generics(T)]
public fun borrow_mut_object_extend<T: key>(object_id: ObjectID): &mut Object<T>;
}
- The module where
T
is located can get any&mut Object<T>
reference throughObjectID
, except for cases when that Object is frozen.
Shared and Frozen Object
SystemOwnedObject Object<T>
has two states, shared
and frozen
.
SharedObject
: Everyone can directly get the&mut Object<T>
reference.FrozenObject
: No one can get the&mut Object<T>
reference, even the module whereT
is located.
Following method can shift Object<T>
to SharedObject
.
module moveos_std::object {
public fun to_shared<T: key>(self: Object<T>);
}
To get the mutable reference of SharedObject, it must be passed through entry
parameters or obtained via the method provided by object
:
module moveos_std::object {
public fun borrow_mut_object_shared<T: key>(object_id: ObjectID): &mut Object<T>;
}
Following method can shift Object<T>
to FrozenObject
.
module moveos_std::object {
public fun to_frozen<T: key>(self: Object<T>);
}
Note: Once Object becomes
frozen
orshared
, it automatically becomeSystemOwnedObject
; no one can directly get the instance ofObject<T>
, only the operations on the Object are feasible through the reference.
Nested Object
Given Object<T>
itself has store
ability, it can be nested in other structures as fields or be saved in containers like vector
, Table
, etc.
struct Avatar has key {
head: Object<Head>,
body: Object<Body>,
}
In the above example, Object<Head>
and Object<Body>
are fields of the Avatar
structure. These two objects belong to the Avatar
structure. If the Object<Avatar>
is transferred to other user, then the Object<Head>
and Object<Body>
will be transferred to the other user with the Object<Avatar>
.
- Even in a nested state, Objects will still exist in Object Storage and can be accessed through the reference retrieval method described earlier.
- Nested Objects will always be
SystemOwnedObject
.
Deleting an Object
The following method can be used to delete Object:
module moveos_std::object {
#[private_generics(T)]
public fun remove<T: key>(self: Object<T>): T;
}
Deleting an Object will return the encapsulated data in the Object, this method can only be called by the module where T
is located.
In summary, different users have different permissions for operations on Objects in different states. The following are the operations that contract developers and normal users can perform on different types of Objects using the methods provided by moveos_std::object
:
- Contract developers
object | owner | value abilities | transfer | borrow mut | take value | remove |
---|---|---|---|---|---|---|
shared | SystemOwnedObject | not required | × | √ | × | × |
frozen | SystemOwnedObject | not required | × | × | × | × |
public | UserOwnedObject | key, store | √ | √ | √ | √ |
private | UserOwnedObject | key | √ | √ | √ | √ |
- Normal users
object | owner | value abilities | transfer | borrow mut | take value | remove |
---|---|---|---|---|---|---|
shared | SystemOwnedObject | not required | × | √ | × | × |
frozen | SystemOwnedObject | not required | × | × | × | × |
public | UserOwnedObject | key, store | √ | √ | √ | × |
private | UserOwnedObject | key | × | √ | × | × |
Object RPC
ObjectEntity
data can be retrieved through rooch_getState
RPC interface.
curl -H "Content-Type: application/json" -X POST \
--data '{"jsonrpc":"2.0","method":"rooch_getStates","params":["/object/0x2::object::Timestamp", {"decode":true}],"id":1}' \
https://dev-seed.rooch.network
{
"jsonrpc": "2.0",
"result": [
{
"value": "0x711ab0301fd517b135b88f57e84f254c94758998a602596be8ae7ba56a0d14b3000000000000000000000000000000000000000000000000000000000000000004002db02e34050600",
"value_type": "0x2::object::ObjectEntity<0x2::object::Timestamp>",
"decoded_value": {
"abilities": 0,
"type": "0x2::object::ObjectEntity<0x2::object::Timestamp>",
"value": {
"flag": 4,
"id": "0x05921974509dbe44ab84328a625f4a6580a5f89dff3e4e2dec448cb2b1c7f5b9",
"owner": "0x0000000000000000000000000000000000000000000000000000000000000000",
"value": {
"abilities": 8,
"type": "0x2::object::Timestamp",
"value": {
"milliseconds": "1694571540000000"
}
}
}
}
}
],
"id": 1
}
Object-related Method List
The context
and object
modules provide the following functions that can operate on Object
:
Object Function | #[private_generics<T>] | Details |
---|---|---|
object::new<T: key>(T): Object<T> | true | Create Object that encapsulates T within, return Object<T> |
object::new_named_object<T: key>(T): Object<T> | true | The ObjectID of this Object is generated using T type |
object::new_account_named_object<T: key>(address, T): Object<T> | true | The ObjectID of this Object is generated using the address and T type |
object::borrow_object<T: key>(ObjectID): &Object<T> | false | Borrow read-only reference of Object<T> through ID |
object::borrow_mut_object<T: key>(&signer, ObjectID): &mut Object<T> | false | owner(&signer) borrows mutable reference of Object<T> through ID |
object::borrow_mut_object_shared<T: key>(ObjectID): &mut Object<T> | false | Borrow mutable reference of a shared Object<T> through ID |
object::borrow_mut_object_extend<T: key>(ObjectID): &mut Object<T> | true | Extension method for developers, the module where T located can get any &mut Object<T> through ObjectID |
object::exists_object(ObjectID): bool | false | Check if the Object exists through its ObjectID |
object::id<T>(&Object<T>): ObjectID | false | Get ObjectID |
object::owner<T: key>(&Object<T>): address | false | Get owner address |
object::borrow<T: key>(&Object<T>): &T | false | Borrow read-only reference of T through &Object |
object::borrow_mut<T: key>(&mut Object<T>): &mut T | false | Borrow mutable reference of T through &mut Object |
object::transfer<T: key + store>(Object<T>, address) | false | Transfer ownership of Object<T> to address |
object::transfer_extend<T: key>(Object<T>, address) | true | Extension method for developers, transfer ownership of Object<T> to address |
object::to_shared<T: key>(Object<T>) | false | Turn Object<T> to a SharedObject , where anyone can get its &mut Object<T> |
object::is_shared<T: key>(&Object<T>): bool | false | Check if Object<T> is SharedObject |
object::to_frozen<T: key>(Object<T>) | false | Turn Object<T> to a FrozenObject , where no one can get its &mut Object<T> |
object::is_frozen<T: key>(&Object<T>): bool | false | Check if Object<T> is FrozenObject |
object::remove<T: key>(Object<T>): T | true | Remove Object<T> , and return the T within. Only the module where T is located can deleteObject<T> |
In the above functions, if the #[private_generics<T>]
column is true
, it indicates that only the module where T
is located can call the function.
Dynamic Fields of Object in Rooch
Rooch provides the capability to manage dynamic fields for objects. Dynamic fields are Resources or Objects stored within an Object in the form of key-value pairs. Notably, the key can be heterogeneous, meaning it is not restricted by the type of the key. More specifically, an Object can be used as a Table (opens in a new tab) or Bag (opens in a new tab).
Rooch objects offer two types of dynamic fields: normal types and Object types.
Normal dynamic fields are resources with store
ability stored under an object; Object type dynamic fields store child Object instances under an object.
Note: Since the Object type itself also has the store
ability, what is the difference between storing the entire Object<T>
as a normal field under an object and using an Object type field?
- If a child object is created via
add_object_field
, it is a child of the parent object and is under the same SMT subtree. This facilitates management of the entire parent object's state transition, queries and so on. - If an object is created globally, even if it is added to the dynamic fields of an object via
add_field
, it actually a global object, and its state tree is under the global Root.
List of Methods for Regular Dynamic Fields
Method | Description |
---|---|
add_field<T: key, K: copy + drop, V: store>(obj: &mut Object<T>, key: K, val: V) | Adds a dynamic field to the object. If the same key already exists, it aborts. The field itself is not stored in the object and cannot be discovered from the object. |
borrow_field<T: key, K: copy + drop, V: store>(obj: &Object<T>, key: K): &V | Gets an immutable reference to the value corresponding to the key in the object. If there is no corresponding key, it aborts. |
borrow_field_with_default<T: key, K: copy + drop, V: store>(obj: &Object<T>, key: K, default: &V): &V | Gets an immutable reference to the value corresponding to the key in the object. If there is no corresponding key, it returns the default value. |
borrow_mut_field<T: key, K: copy + drop, V: store>(obj: &mut Object<T>, key: K): &mut V | Gets a mutable reference to the value corresponding to the key in the object. If there is no corresponding key, it aborts. |
borrow_mut_field_with_default<T: key, K: copy + drop, V: store + drop>(obj: &mut Object<T>, key: K, default: V): &mut V | Gets a mutable reference to the value corresponding to the key in the object. If there is no corresponding key, it inserts the key-value pair (key , default ) and then returns a mutable reference to the corresponding value. |
remove_field<T: key, K: copy + drop, V: store>(obj: &mut Object<T>, key: K): V | Removes the field corresponding to the key from the object and returns the value of the field. If there is no corresponding key, it aborts. |
contains_field<T: key, K: copy + drop>(obj: &Object<T>, key: K): bool | Returns true if the object contains the field corresponding to the key, otherwise false . |
contains_field_with_type<T: key, K: copy + drop, V: store>(obj: &Object<T>, key: K): bool | Returns true if the object contains the field corresponding to the key and the value type is V , otherwise false . |
upsert_field<T: key, K: copy + drop, V: store + drop>(obj: &mut Object<T>, key: K, value: V) | If the object contains the field corresponding to the key, it updates the value of the field. If there is no corresponding key, it inserts the key-value pair (key , value ). |
field_size<T: key>(obj: &Object<T>): u64 | Returns the number of fields in the object, i.e., the number of key-value pairs. |
List of Methods for Object Type Dynamic Fields
Method | Description |
---|---|
add_object_field<T: key, V: key>(obj: &mut Object<T>, v: V): Object<V> | Adds a new child object field to the object and returns the newly added child object. Only shared objects can add child object fields. |
add_object_field_with_id<T: key, ID:drop, V: key>(obj: &mut Object<T>, id: ID, v: V): Object<V> | Adds a new child object field to the object with a custom ID and returns the newly added child object. Only shared objects can add child object fields. |
remove_object_field<T: key, V: key>(obj: &mut Object<T>, child: Object<V>): V | Removes the specified child object field from the object and returns its value. If the child object field does not exist, the operation fails. |
Comparison between Rooch Object, Sui Object, and Aptos Object
Sui Object
- Sui Object is a special kind of
struct
that requires thestruct
to has akey
ability, and UID must be its first field. An Object is provided by the VM and storage, and there's no Object type in Move. In Rooch, Object is a type defined in Move itself. - Sui Object is indexed by the external system, and there's no method provided in the contract to retrieve the Object using ID; it can only be passed through parameters. Rooch provides both methods.
- If a Sui Object gets nested or saved into other containers, it will become invisible in the global Object Storage. However, even when nested or saved into other containers, the Rooch Object can still be accessed in the global Object Storage.
Aptos Object
- At the base level, an Aptos Object is a special account, where the
address
is theObjectID
. Object<T>
represents the reference to an Object that can becopy
,drop
, whereas in Rooch,Object<T>
is a single instance and cannot becopy
,drop
.- Aptos Object uses
DeleteRef
,ExtendRef
,TransferRef
to indicate different operation permissions on Object. But Rooch Object differentiates different permissions using read-only reference, mutable reference, and instance.
TODO: This part of this document needs to be improved