Open16

move and sui

shunshun
  • モジュールの新規作成
move new Sample0713
cd Sample0713
touch sources/SmapleCoin.move
  • ビルドとデプロイ(Sandbox環境へ)
move build
move sandbox publish

build/storage/ のディレクトリが生成される

shunshun

SuiとMoveの違い

項目 Move Sui
リソースの基本 アカウント オブジェクト
ストレージ Map<(address, ResourceType)> Map<ObjectID, Object>
shunshun
  • acquires
    関数定義の返り値の部分に、acquires(リソース)とすることで、関数でリソースを操作できるようになる
public fun mint(account: &signer, amount: u64) acquires SampleCoin {
    let account_address = signer::address_of(account);
    let coin_ref = borrow_global_mut<SampleCoin>(account_address);
    coin_ref.value = coin_ref.value + amount;
  }
shunshun
  • publish, mint, transferの基本的な実装とテストコード
module Sample::SampleCoin {
  use std::signer;

  const EALREADY_HAS_COIN: u64 = 1;
  const ESHOULD_HAS_COIN: u64 = 2;
  const EINVALID_VALUE: u64 = 3;

  // Coinという構造体を定義
  // keyというAbilityを持つことでcopy, drop, storeが不可能になっている
  // https://move-language.github.io/move/abilities.html
  struct SampleCoin has key {
    value: u64
  }

  // functionはデフォルトでprivate
  // public, public(friend), public(script)が存在する
  // 
  // move_toでaccountにSampleCoinを紐付けるイメージ
  // move_to以外にGlobal Storage Operatorsとして4つのOperationが存在する
  // https://move-language.github.io/move/global-storage-operators.html
  // move_from, borrow_global, borrow_global_mut, existsの4つ
  public fun public_coin(account: &signer){
    let account_address = signer::address_of(account);
    assert!(!exists<SampleCoin>(account_address), EALREADY_HAS_COIN);

    let coin = SampleCoin{value: 0};
    move_to(account, coin);
  }

  public fun mint(account: &signer, amount: u64) acquires SampleCoin {
    assert!(amount > 0, EINVALID_VALUE);
    let account_address = signer::address_of(account);
    assert!(exists<SampleCoin>(account_address), ESHOULD_HAS_COIN);
    let coin_ref = borrow_global_mut<SampleCoin>(account_address);
    coin_ref.value = coin_ref.value + amount;
  }

  public fun transfer(from: &signer, to: &signer, amount: u64) acquires SampleCoin {
    assert!(amount > 0, EINVALID_VALUE);

    let from_address = signer::address_of(from);
    let to_address = signer::address_of(to);
    assert!(exists<SampleCoin>(from_address), ESHOULD_HAS_COIN);
    assert!(exists<SampleCoin>(to_address), ESHOULD_HAS_COIN);
    
    let from_coin_ref = borrow_global_mut<SampleCoin>(from_address);
    assert!(from_coin_ref.value >= amount, EINVALID_VALUE);
    from_coin_ref.value = from_coin_ref.value - amount;

    let to_coin_ref = borrow_global_mut<SampleCoin>(to_address);    
    to_coin_ref.value = to_coin_ref.value + amount;
  }


  /// テストコード

  #[test(user = @0x2)]
  fun test_publish_coin(user: &signer) acquires SampleCoin{
    public_coin(user);
    let user_address = signer::address_of(user);
    assert!(exists<SampleCoin>(user_address), ESHOULD_HAS_COIN);
    let coin_ref = borrow_global<SampleCoin>(user_address);
    assert!(coin_ref.value==0, 0);
  }

  #[test(user = @0x2)]
  #[expected_failure(abort_code = EALREADY_HAS_COIN)]
  fun test_not_double_publish_coin(user: &signer){
    public_coin(user);
    public_coin(user);
  }

  #[test(user = @0x2)]
  fun test_mint(user: &signer) acquires SampleCoin{
    public_coin(user);
    mint(user, 100);
    let user_address = signer::address_of(user);
    let coin_ref = borrow_global<SampleCoin>(user_address);
    assert!(coin_ref.value==100, 0);
  }

  #[test(user = @0x2)]
  #[expected_failure(abort_code = ESHOULD_HAS_COIN)]
  fun test_mint_when_no_resource(user: &signer) acquires SampleCoin {
    mint(user, 100);
  }

  #[test(user = @0x2)]
  #[expected_failure(abort_code = EINVALID_VALUE)]
  fun test_mint_when_use_insufficient_arg(user: &signer) acquires SampleCoin {
    public_coin(user);
    mint(user, 0);
  }

  #[test(from = @0x2, to = @0x3)]
  fun test_transfer(from: &signer, to: &signer) acquires SampleCoin {
    public_coin(from);
    public_coin(to);
    mint(from, 100);
    transfer(from, to, 20);
    assert!(borrow_global<SampleCoin>(signer::address_of(from)).value==80, 0);
    assert!(borrow_global<SampleCoin>(signer::address_of(to)).value==20, 0);
  }

  #[test(from = @0x2, to = @0x3)]
  #[expected_failure(abort_code = EINVALID_VALUE)]
  fun test_transfer_when_use_insufficient_amount(from: &signer, to: &signer) acquires SampleCoin {
    transfer(from, to, 0);
  }

  #[test(from = @0x2, to = @0x3)]
  #[expected_failure(abort_code = ESHOULD_HAS_COIN)]
  fun test_transfer_when_no_coin_in_from(from: &signer, to: &signer) acquires SampleCoin {
    public_coin(to);
    transfer(from, to, 10);
  }

  #[test(from = @0x2, to = @0x3)]
  #[expected_failure(abort_code = ESHOULD_HAS_COIN)]
  fun test_transfer_when_no_coin_in_to(from: &signer, to: &signer) acquires SampleCoin {
    public_coin(from);
    transfer(from, to, 10);
  }

  #[test(from = @0x2, to = @0x3)]
  #[expected_failure(abort_code = EINVALID_VALUE)]
  fun test_transfer_when_amount_over_balance(from: &signer, to: &signer) acquires SampleCoin {
    public_coin(from);
    public_coin(to);
    mint(from, 10);
    transfer(from, to, 100);
  }
}
shunshun
  • init処理
    moduleをpublishした時に一度だけ実行される
module examples::one_timer {
    use sui::transfer;
    use sui::object::{Self, UID};
    use sui::tx_context::{Self, TxContext};

    struct CreatorCapability has key {
        id: UID
    }

    /// Use it to make sure something has happened only once, like
    /// here - only module author will own a version of a
    /// `CreatorCapability` struct.
    fun init(ctx: &mut TxContext) {
        transfer::transfer(CreatorCapability {
            id: object::new(ctx),
        }, tx_context::sender(ctx))
    }
}
shunshun
  • entry関数
    entryをつけて関数を定義することで、関数をトランザクションなどから直接呼び出せるようになる
    トランザクションで直接呼び出すことしかできないため、return valueを持てない
shunshun
  • import
    use sui::object::{Self, UID}; /// UID shows object id. Struct member is "id"
    use sui::transfer; /// transfer have functions related transfer
    use sui::tx_context::{Self, TxContext}; /// TxContext shows info related transaction. Struct members are sender, tx_hash, epoch, epoch_timestamp_ms, ids_created

例えば最後のuse定義は、「suiというパッケージのtx_contextというモジュールのTxContextという構造体をimportしている」という意味。

shunshun
  • keyオブジェクトは必ずsuiのUID(Sui独自のオブジェクトID)を持つ必要がある。
struct Object has key {
        id: UID,
        /// Custom objects can have fields of arbitrary type...
        custom_field: u64,
        /// ... including other objects
        child_obj: ChildObject,
        /// ... and other global objects
        nested_obj: AnotherObject,
    }
shunshun

基本的なオブジェクト定義のサンプル

module basics::object {
    use sui::object::{Self, UID}; // UIDはオブジェクトのIDを意味する。Structのfieldとしてidを持つ。
    use sui::transfer; // transferに関する関数
    use sui::tx_context::{Self, TxContext}; // トランザクションに関する情報をもつ構造体。sender, tx_hash, epoch, epoch_timestamp_ms, ids_createdをfieldとして持つ。
    use sui::clock::{Self, Clock};
    use sui::event;

    /// オブジェクトの定義
    /// keyをつけることでグローバルオブジェクトプーつに保存される
    /// その際には、suiのオブジェクトID管理のためにid: UIDを持つ必要がある
    struct Object has key {
        id: UID,
        /// Custom objects can have fields of arbitrary type...
        custom_field: u64,
        /// ... including other objects
        child_obj: ChildObject,
        /// ... and other global objects
        nested_obj: AnotherObject,
    }

    /// オブジェクトはグローバルオブジェクトや他の子オブジェクトに格納することもできる
    /// その際は、IDは必要ない
    struct ChildObject has store {
        a_field: bool,
    }

    /// グローバルオブジェクトをnestすることもできる
    struct AnotherObject has key, store {
        id: UID,
    }

    /// getterサンプル
    /// オブジェクトのfieldを読み取る
    public fun read(o: &Object): u64 {
        o.custom_field
    }

    /// オブジェクトをインスタンス化する関数
    public fun create(tx: &mut TxContext): Object {
        Object {
            id: object::new(tx),
            custom_field: 0,
            child_obj: ChildObject { a_field: false },
            nested_obj: AnotherObject { id: object::new(tx) }
        }
    }

    /// オブジェクトをtransferするサンプル
    public entry fun transfer(o: Object, recipient: address) {
        assert!(some_conditional_logic(), 0);
        transfer::transfer(o, recipient)
    }

    /// オブジェクトを更新するサンプル
    /// すべてのオブジェクトのfieldはプライベートであり、モジュール内でのみ更新ができる
    public entry fun update(o: &mut Object, v: u64) {
        o.custom_field = v
        // emit an event so the world can see the new value
        event::emit(NewValueEvent { new_value: o.custom_field })
    }

    /// オブジェクトを削除するサンプル
    public entry fun delete(o: Object) {
        let Object { id, value: _ } = o;
        object::delete(id);
    }

    /// 時間をevent emitする関数
    /// 組み合わせで利用できる
    struct TimeEvent has copy, drop {
        timestamp_ms: u64,
    }
    public entry fun get_time(clock: &Clock, _ctx: &mut TxContext) {
        event::emit(TimeEvent { timestamp_ms: clock::timestamp_ms(clock) });
    }
}

shunshun

Shared Objectのサンプル(テスト付き)

/// shared objectのサンプル
/// Rules:
/// - 誰でもカウンターを作って共有できる
/// - 誰でもカウンターを1つだけ増やせる
/// - カウンターの所有者だけリセットできる
module basics::counter {
    use sui::transfer;
    use sui::object::{Self, UID};
    use sui::tx_context::{Self, TxContext};

    /// カウンターオブジェクトの定義
    struct Counter has key {
        id: UID,
        owner: address,
        value: u64
    }

    /// Read系の関数
    public fun owner(counter: &Counter): address {
        counter.owner
    }
    public fun value(counter: &Counter): u64 {
        counter.value
    }

    /// カウンターオブジェクトを生成し、ctxに含まれるsenderにtransfer
    public entry fun create(ctx: &mut TxContext) {
        transfer::share_object(Counter {
            id: object::new(ctx),
            owner: tx_context::sender(ctx),
            value: 0
        })
    }

    /// Increment a counter
    public entry fun increment(counter: &mut Counter) {
        counter.value = counter.value + 1;
    }

    /// 任意の値を設定する関数
    /// assertによってownerのみが実行できるよう制限している
    public entry fun set_value(counter: &mut Counter, value: u64, ctx: &TxContext) {
        assert!(counter.owner == tx_context::sender(ctx), 0);
        counter.value = value;
    }

    /// カウンターの値をassertする関数
    public entry fun assert_value(counter: &Counter, value: u64) {
        assert!(counter.value == value, 0)
    }
}

/// test_scenarioを使ってテストを複数設定できる
#[test_only]
module basics::counter_test {
    use sui::test_scenario;
    use basics::counter;

    #[test]
    fun test_counter() {
        let owner = @0xC0FFEE;
        let user1 = @0xA1;

        let scenario_val = test_scenario::begin(user1);
        let scenario = &mut scenario_val;

        /// test_scenarioをctxトランザクションとして利用できる
        /// counterをowner内に生成
        test_scenario::next_tx(scenario, owner);
        {
            counter::create(test_scenario::ctx(scenario));
        };

        /// incrementのテスト
        /// ownerのcounterを呼び出しインクリメント
        test_scenario::next_tx(scenario, user1);
        {
            /// shared objectを取り出し、更新可能な状態で設定する
            let counter_val = test_scenario::take_shared<counter::Counter>(scenario);
            let counter = &mut counter_val;

            assert!(counter::owner(counter) == owner, 0);
            assert!(counter::value(counter) == 0, 1);

            counter::increment(counter);
            counter::increment(counter);
            counter::increment(counter);

            /// shared objectを返している
            test_scenario::return_shared(counter_val);
        };

        /// set_valueのテスト
        test_scenario::next_tx(scenario, owner);
        {
            let counter_val = test_scenario::take_shared<counter::Counter>(scenario);
            let counter = &mut counter_val;

            assert!(counter::owner(counter) == owner, 0);
            assert!(counter::value(counter) == 3, 1);

            counter::set_value(counter, 100, test_scenario::ctx(scenario));

            test_scenario::return_shared(counter_val);
        };

        test_scenario::next_tx(scenario, user1);
        {
            let counter_val = test_scenario::take_shared<counter::Counter>(scenario);
            let counter = &mut counter_val;

            assert!(counter::owner(counter) == owner, 0);
            assert!(counter::value(counter) == 100, 1);

            counter::increment(counter);

            assert!(counter::value(counter) == 101, 2);

            test_scenario::return_shared(counter_val);
        };
        test_scenario::end(scenario_val);
    }
}
shunshun

Shared ObjectとユーザーのObjectを跨いだ利用例

/// lockという箱を生成し、そこにTreasureを格納。keyを生成したユーザーが保持する。
/// lockはshared objectなので誰でも確認できるが、keyがないと中身を取り出せない
/// ユーザーがkeyを他の人に渡せば、もらった人はkeyを使ってlockの箱からTreasureを取り出し、
/// 自分のオブジェクトとして取得できる(shared obejectではなくなる)
module basics::lock {
    use sui::object::{Self, ID, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};
    use std::option::{Self, Option};

    const ELockIsEmpty: u64 = 0; /// lockが空の時
    const EKeyMismatch: u64 = 1; /// keyが一致しない時
    const ELockIsFull: u64 = 2; // lockの中にすでに何かが格納されている時

    /// Lockオブジェクト
    /// lockedの中に格納したいオブジェクトを入れる
    /// Optionモジュールを使っている
    struct Lock<T: store + key> has key, store {
        id: UID,
        locked: Option<T>
    }

    /// Keyオブジェクト
    /// forに所有者のアドレスが入る(IDはaddressが格納される型)
    struct Key<phantom T: store + key> has key, store {
        id: UID,
        for: ID,
    }

    /// keyの所有者を返す
    public fun key_for<T: store + key>(key: &Key<T>): ID {
        key.for
    }

    /// LockとKeyを生成し、それぞれを格納
    public entry fun create<T: store + key>(obj: T, ctx: &mut TxContext) {
        let id = object::new(ctx);
        let for = object::uid_to_inner(&id);

        /// lockはshared objectとしてpublish
        transfer::public_share_object(Lock<T> {
            id,
            locked: option::some(obj),
        });

        /// keyはtx_contextのsenderのオブジェクトとしてtransfer
        transfer::public_transfer(Key<T> {
            for,
            id: object::new(ctx)
        }, tx_context::sender(ctx));
    }

    /// オブジェクトをlockに格納する
    /// その際にすでに別のobjがないか、keyが正しいかをチェック
    public entry fun lock<T: store + key>(
        obj: T,
        lock: &mut Lock<T>,
        key: &Key<T>,
    ) {
        assert!(option::is_none(&lock.locked), ELockIsFull);
        assert!(&key.for == object::borrow_id(lock), EKeyMismatch);

        /// optionの機能を使ってlock.lockedにobjを格納する
        /// 普通に変数として取り扱えない?最初から型がわかっていないからoptionを使っている?
        option::fill(&mut lock.locked, obj);
    }

    /// Lockをkeyで解錠し、中身を取り出す
    public fun unlock<T: store + key>(
        lock: &mut Lock<T>,
        key: &Key<T>,
    ): T {
        assert!(option::is_some(&lock.locked), ELockIsEmpty); // lockが空ではないことを確認
        assert!(&key.for == object::borrow_id(lock), EKeyMismatch); // lockのIDと所有者が正しいことを確認

        option::extract(&mut lock.locked)
    }

    /// Lockをunlockしtx_contextのsenderに送る
    public fun take<T: store + key>(
        lock: &mut Lock<T>,
        key: &Key<T>,
        ctx: &mut TxContext,
    ) {
        transfer::public_transfer(unlock(lock, key), tx_context::sender(ctx))
    }
}

#[test_only]
module basics::lockTest {
    use sui::object::{Self, UID};
    use sui::test_scenario;
    use sui::transfer;
    use basics::lock::{Self, Lock, Key};

    /// Lockに格納したいオブジェクト
    struct Treasure has store, key {
        id: UID
    }

    #[test]
    fun test_lock() {
        let user1 = @0x1;
        let user2 = @0x2;

        let scenario_val = test_scenario::begin(user1);
        let scenario = &mut scenario_val;

        /// User1がLockを生成し、そこにTreasureを格納
        test_scenario::next_tx(scenario, user1);
        {
            let ctx = test_scenario::ctx(scenario);
            let id = object::new(ctx);

            lock::create(Treasure { id }, ctx);
        };

        /// User1が鍵をUser2にtransfer
        test_scenario::next_tx(scenario, user1);
        {
            let key = test_scenario::take_from_sender<Key<Treasure>>(scenario);

            transfer::public_transfer(key, user2);
        };

        /// User2がLockを解錠し、Treasureを自分にtransfer
        test_scenario::next_tx(scenario, user2);
        {
            let lock_val = test_scenario::take_shared<Lock<Treasure>>(scenario);
            let lock = &mut lock_val;
            let key = test_scenario::take_from_sender<Key<Treasure>>(scenario);
            let ctx = test_scenario::ctx(scenario);

            lock::take<Treasure>(lock, &key, ctx);

            test_scenario::return_shared(lock_val);
            test_scenario::return_to_sender(scenario, key);
        };
        test_scenario::end(scenario_val);
    }
}

shunshun

サンプルファイルのデプロイ方法

参照: https://docs.sui.io/build/connect-sui-network

  • 環境のセットアップ
sui client

アドレスを確認

cat ~/.sui/sui_config/client.yaml 
  • faucetの取得
    discordに参加し、# devnet-faucet チャンネルで !faucet <上記で生成したアカウントのアドレス> と入力するだけ

  • deploy実行

sui client publish sui/sui_programmability/examples/move_tutorial --gas-budget 100000000

※ --gas-budgetが低いと、InsufficientGasというエラーが出てしまうので、かなり大きい値を設定している。桁を1つ落とすとエラーが出る。

shunshun
  • アカウントの残りのガスをチェック
    上記のデプロイ実行後
sui client gas `sui client active-address`

結果

           Object ID            |  Gas Value 
----------------------------------------------------------------------------------
 0xXXXXXXXXXXXXXXXXXXXXXXXXXXXX | 9985652760 
shunshun

開発の流れ

  • 初期化 sui move new ディレクトリ名
  • move書く
  • test書く
  • ビルドチェック sui move build
  • テスト実行 sui move test