Using Objects

Sui authentication mechanisms ensure only you can use objects owned by you in Sui Move calls. To use an object in Sui Move calls, pass them as parameters to an entry function. Similar to Rust, there are a few ways to pass parameters, as described in the following sections.

Passing objects by reference

There are two ways to pass objects by reference: read-only references (&T) and mutable references (&mut T). Read-only references allow you to read data from the object, while mutable references allow you to mutate the data in the object.

public entry fun copy_into(from_object: &ColorObject, into_object: &mut ColorObject) {
    into_object.red = from_object.red;
    into_object.green = from_object.green;
    into_object.blue = from_object.blue;
}

In the preceding function signature, from_object can be a read-only reference because you only need to read its fields. Conversely, into_object must be a mutable reference since you need to mutate it. For a transaction to make a call to the copy_into function, the sender of the transaction must be the owner of both from_object and into_object.

Although from_object is a read-only reference in this transaction, it is still a mutable object in Sui storage--another transaction could be sent to mutate the object at the same time. To prevent this, Sui must lock any mutable object used as a transaction input, even when it's passed as a read-only reference. In addition, only an object's owner can send a transaction that locks the object.

The take_from_sender<T> function takes an object of type T from the global storage created by previous transactions. However, if there are multiple objects of the same type, take_from_sender<T> is no longer able to determine which one to return. To solve this problem, use two new, test-only functions. The first is tx_context::last_created_object_id(ctx), which returns the ID of the most recently created object. The second is test_scenario::take_from_sender_by_id<T>, which returns an object of type T with a specific object ID.

Unit test for interacting with multiple objects of the same type in tests:

let owner = @0x1;
let scenario_val = test_scenario::begin(owner);
let scenario = &mut scenario_val;
// Create two ColorObjects owned by `owner`, and obtain their IDs.
let (id1, id2) = {
    let ctx = test_scenario::ctx(scenario);
    color_object::create(255, 255, 255, ctx);
    let id1 = object::id_from_address(tx_context::last_created_object_id(ctx));
    color_object::create(0, 0, 0, ctx);
    let id2 = object::id_from_address(tx_context::last_created_object_id(ctx));
    (id1, id2)
};

The preceding code creates two objects. Note that right after each call, it makes a call to tx_context::last_created_object_id to get the ID of the object the call created. At the end, id1 and id2 capture the IDs of the two objects. Next, retrieve both of them and test the copy_into function:

test_scenario::next_tx(scenario, owner);
{
    let obj1 = test_scenario::take_from_sender_by_id<ColorObject>(scenario, id1);
    let obj2 = test_scenario::take_from_sender_by_id<ColorObject>(scenario, id2);
    let (red, green, blue) = color_object::get_color(&obj1);
    assert!(red == 255 && green == 255 && blue == 255, 0);

    let ctx = test_scenario::ctx(scenario);
    color_object::copy_into(&obj2, &mut obj1);
    test_scenario::return_to_sender(scenario, obj1);
    test_scenario::return_to_sender(scenario, obj2);
};

This uses take_from_sender_by_id to take both objects using different IDs. Use copy_into to update the value for obj1 using the value for obj2. You can verify that the mutation works:

test_scenario::next_tx(scenario, owner);
{
    let obj1 = test_scenario::take_from_sender_by_id<ColorObject>(scenario, id1);
    let (red, green, blue) = color_object::get_color(&obj1);
    assert!(red == 0 && green == 0 && blue == 0, 0);
    test_scenario::return_to_sender(scenario, obj1);
};
test_scenario::end(scenario_val);

Pass objects by value

You can also pass objects by value into an entry function. By doing so, the object is moved out of Sui storage. It is then up to the Sui Move code to decide where this object should go.

Since every Sui object struct type must include UID as its first field, and the UID struct does not have the drop ability, the Sui object struct type cannot have the drop ability either. Hence, any Sui object cannot be arbitrarily dropped and must be either consumed (for example, transferred to another owner) or deleted by unpacking, as described in the following sections.

There are two ways to handle a pass-by-value Sui object in Move:

  • delete the object

  • transfer the object

Delete the object

If the intention is to actually delete the object, unpack it. You can do this only in the module that defined the struct type, due to Move's privileged struct operations rules. If any field is also of struct type, you must use recursive unpacking and deletion when you unpack the object.

However, the id field of a Sui object requires special handling. You must call the following API in the object module to signal Sui that we intend to delete this object:

public fun delete(id: UID) { ... }

Define a function in the ColorObject module that allows us to delete the object:

    public entry fun delete(object: ColorObject) {
        let ColorObject { id, red: _, green: _, blue: _ } = object;
        object::delete(id);
    }

The object unpacks and generates individual fields. You can drop all of the u8 values, which are primitive types. However, you can't drop the id, which has type UID, and must explicitly delete it using the object::delete API. At the end of this call, the object is no longer stored on-chain.

To add a unit test for it:

let owner = @0x1;
// Create a ColorObject and transfer it to @owner.
let scenario_val = test_scenario::begin(owner);
let scenario = &mut scenario_val;
{
    let ctx = test_scenario::ctx(scenario);
    color_object::create(255, 0, 255, ctx);
};
// Delete the ColorObject we just created.
test_scenario::next_tx(scenario, owner);
{
    let object = test_scenario::take_from_sender<ColorObject>(scenario);
    color_object::delete(object);
};
// Verify that the object was indeed deleted.
test_scenario::next_tx(scenario, &owner);
{
    assert!(!test_scenario::has_most_recent_for_sender<ColorObject>(scenario), 0);
};
test_scenario::end(scenario_val);

The first part of the example repeats the example used in Object Basics, and creates a new ColorObject and puts it in the owner's account. The second transaction gets tested. It retrieves the object from the storage and then delete it. Since the object is deleted, there is no need (in fact, it is impossible) to return it to the storage. The last part of the test checks that the object is indeed no longer in the global storage and hence cannot be retrieved from there.

Option 2. Transfer the object

The owner of the object might want to transfer it to another address. To support this, the ColorObject module needs to define a transfer function:

public entry fun transfer(object: ColorObject, recipient: address) {
    transfer::transfer(object, recipient)
}

You cannot call transfer::transfer directly as it is not an entry function.

Add a test for transferring too. First, create an object in the account of the owner, and then transfer it to a different account recipient:

let owner = @0x1;
// Create a ColorObject and transfer it to @owner.
let scenario_val = test_scenario::begin(owner);
let scenario = &mut scenario_val;
{
    let ctx = test_scenario::ctx(scenario);
    color_object::create(255, 0, 255, ctx);
};
// Transfer the object to recipient.
let recipient = @0x2;
test_scenario::next_tx(scenario, owner);
{
    let object = test_scenario::take_from_sender<ColorObject>(scenario);
    let ctx = test_scenario::ctx(scenario);
    transfer::transfer(object, recipient, ctx);
};

Note that in the second transaction, the sender of the transaction should still be owner, because only the owner can transfer the object that it owns. After the transfer, you can verify that owner no longer owns the object, and recipient now owns it:

// Check that owner no longer owns the object.
test_scenario::next_tx(scenario, owner);
{
    assert!(!test_scenario::has_most_recent_for_sender<ColorObject>(scenario), 0);
};
// Check that recipient now owns the object.
test_scenario::next_tx(scenario, recipient);
{
    assert!(test_scenario::has_most_recent_for_sender<ColorObject>(scenario), 0);
};
test_scenario::end(scenario_val);

Last updated