Sui - Custom Transfer

Sui - Custom Transfer

·

3 min read

Hello everyone...

Welcome to another day in exploring Web3 Engineering. In today's blog, let us check out how to write custom transfer method for a Sui object.

A Sui object is usually transferred using the public_transfer from the transfer module in the sui package. This function can directly be called from a smart contract or from a PTB as well.

This function doesn't interact with the module that the object is defined and just transfers the ownership of the object without raising any events as well. And we can't control the execution as well. There might be some cases where the developer wants to impose some certain actions while transferring the objects. For example, allowing transfers to only eligible users by the app, imposing some fee for transfer of the NFT etc. For such situations, we can write our own transfer function for the Objects and implement our own logic according to the app.

For a object to be eligible to use the public_transfer function, it must contain the store ability. So, if we create an object which doesn't have store, it can only be transferred using transfer method from the transfer module. The transfer function can only transfer objects that are defined in the same module.

Now, let us write a move smart contract that tracks the number of times that the object is transferred.

/// Module: custom_transfer
module custom_transfer::custom_transfer {
    use sui::vec_map::{Self, VecMap};

    public struct CustomTransfer has key, store {
        id: UID,
        transfers_count: VecMap<ID, u64>
    }

    fun init(ctx: &mut TxContext) {
        transfer::transfer(CustomTransfer {
            id: object::new(ctx),
            transfers_count: vec_map::empty<ID, u64>()
        }, ctx.sender())
    }

    public struct Asset has key {
        id: UID,
        val: u64
    }

    public fun new(c: &mut CustomTransfer, val: u64, ctx: &mut TxContext) {
        let id = object::new(ctx);
        c.transfers_count.insert(id.to_inner(), 0);
        transfer::transfer(Asset {
            id,
            val
        }, ctx.sender())
    }

    // notice: Custom transfer function
    public fun transfer(c: &mut CustomTransfer, asset: Asset, receiver: address) {
       let counter = c.transfers_count.get_mut(&asset.id.to_inner());
        *counter = *counter + 1;
        transfer::transfer(asset, receiver)
    }

    public fun count(c: &CustomTransfer, asset: &Asset): u64 {
        *c.transfers_count.get(&asset.id.to_inner())
    }

    #[test_only]
    public fun init_for_test(ctx: &mut TxContext) {
        init(ctx)
    }
}

In the above contract, we have the

  • CustomTransfer object which will save the transfer counts.
  • Asset object on which we are implementing the custom transfer method.

  • new method which will generate a new Asset

  • transfer our custom transfer method, which will increase the transfers count in the CustomTransfer object

Now, let us write the test cases


#[test_only]
module custom_transfer::custom_transfer_tests {
    use sui::test_scenario;
    use custom_transfer::custom_transfer;

    const USER: address = @0xCAFE;
    const ANOTHER_USER: address = @0xFACE;

    #[test]
    fun test_custom_transfer() {
        let mut scenario = test_scenario::begin(USER);
        {
            custom_transfer::init_for_test(scenario.ctx());
        };

        scenario.next_tx(USER);
        {
            let mut ct = scenario.take_from_sender<custom_transfer::CustomTransfer>();
            custom_transfer::new(&mut ct, 10, scenario.ctx());
            scenario.return_to_sender(ct);
        };

        scenario.next_tx(USER);
        {
            let mut ct = scenario.take_from_sender<custom_transfer::CustomTransfer>();
            let asset = scenario.take_from_sender<custom_transfer::Asset>();

            let counter = custom_transfer::count(&ct, &asset);
            assert!(counter == 0, 0);

            custom_transfer::transfer(&mut ct, asset, ANOTHER_USER);
            scenario.return_to_sender(ct);
        };

        scenario.next_tx(USER);
        {
            let ct = scenario.take_from_sender<custom_transfer::CustomTransfer>();
            let asset = scenario.take_from_address<custom_transfer::Asset>(ANOTHER_USER);

            let counter = custom_transfer::count(&ct, &asset);
            assert!(counter == 1, 0);

            scenario.return_to_sender(ct);
            test_scenario::return_to_address(ANOTHER_USER, asset);
        };
        scenario.end();
    }
}

Here we have the test case which will test the increase the count. And also, if we try to use the public_transfer method on the Asset, the move analyzer directly gives us an error message or if it is not installed, we will get an error while compiling the code itself

Full code can be found here: https://github.com/jveer634/sui_contracts/

Thank you.