🔺

Sui Devnetで簡単なスマートコントラクトを実装する

2023/01/24に公開

ブロックチェーン上に存在する値を増やしたり減らしたりするだけの簡単なオンチェーンプログラムです。練習がてらに作りました。

1. Sui binary をインストールする

この記事に関しては必須になるためインストールします。

https://docs.sui.io/build/install#binaries

2. Move Package を作成する

sui move new my_first_package

として、sui用のmoveプロジェクトを作成します。

touch my_first_package/sources/my_counter.move

として、ソースファイルも作成します。

3. moveの実装を行う

サクッとコード書くとこんな感じです。

my_counter.move
module my_first_package::my_counter {
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};

    struct CountHistory has store {
        up: u64,
        down: u64
    }

    struct Counter has key {
        id: UID,
        count: u64,
        count_history: CountHistory
    }

    fun init(ctx: &mut TxContext) {
        let counter = Counter {
            id: object::new(ctx),
            count: 0,
            count_history: CountHistory {
                up: 0,
                down: 0,
            }
        };
        transfer::transfer(counter, tx_context::sender(ctx));
    }


    public entry fun count_up(self: &mut Counter) {
        self.count = self.count + 1;
        self.count_history.up = self.count_history.up + 1;
    }

    public entry fun count_down(self: &mut Counter) {
        if(self.count > 0){
            self.count = self.count - 1;
            self.count_history.down = self.count_history.down + 1;
        } else {
            abort 0
        }

    }

}

※ move対応してないため記事作成時点では真っ白っぽいです。。

ざっくりですがコードの解説書いていきます。

use sui::object::{Self, UID};
use sui::transfer;
use sui::tx_context::{Self, TxContext};

最初のpackage作成時に一緒にできているMove.toml見るとわかるんですが、sui-frameworkというcrateからもってきてるやつです。

詳細把握してないので大まかにしか言えないですが、suiでスマートコントラクトを実装する上で基盤になる要素だと理解しておけばいいような気がします。

struct CountHistory has store {
    up: u64,
    down: u64
}

struct Counter has key {
    id: UID,
    count: u64,
    count_history: CountHistory
}

ここで定義した構造体をもとにオブジェクトを作成することが出来ます。
オブジェクトとして作成したい構造体は、最初のフィールドとしてid: UIDを持つことが必須です。

has key と has storeについてですが、ざっくりとした認識ではhas keyをすることでオブジェクトとして生成することのできる構造体とすることができ、has storeとすることで別構造体のフィールドで呼び出すことができるようになるという感じです。

fun init(ctx: &mut TxContext) {
    let counter = Counter {
        id: object::new(ctx),
        count: 0,
        count_history: CountHistory {
            up: 0,
            down: 0,
        }
    };
    transfer::transfer(counter, tx_context::sender(ctx));
}

fun init()は特別な関数で、モジュールが公開されたときに一度だけ呼び出される関数となります。

transfer::transfer(counter, tx_context::sender(ctx))
は、実際にオブジェクトを作成する部分で、第一引数に作成するオブジェクト。第二引数にそのownerとなるアドレスを入れます。

この場合はmoduleを公開するtxを送信したaddressをownerとしたCounterオブジェクトが生成されるという感じです。

public entry fun count_up(self: &mut Counter) {
    self.count = self.count + 1;
    self.count_history.up = self.count_history.up + 1;
}

public entry fun count_down(self: &mut Counter) {
    if(self.count > 0){
        self.count = self.count - 1;
        self.count_history.down = self.count_history.down + 1;
    } else {
        abort 0
    }
}

これらは作成されたオブジェクトを引数に取ることで、その値を増減させる関数です。

詳細不明ですが、おそらく数値はマイナスを取ることが出来ないと思われるので(i64などの型がなさそう)、もしcountが0の場合はabortするようにcount_downのほうの関数は定義しています。

abortでtxの実行を中止することが出来、後に続く値をエラーコードとして返せます。

https://move-book.com/syntax-basics/control-flow.html#conditional-abort

それから関数をentryとすることでtransactionから実行することができるようになります。逆に言えばentryとしなければtransactionで実行することは出来ません。

https://docs.sui.io/devnet/explore/move-examples/basics#entry-functions

4. ビルドしてデプロイする

sui client active-address

として、有効なアドレスが返ってこない場合、まずsui client new-address ed25519などとしてアカウントを作成します。

またトランザクションを発行するにはガス代としてsuiが必要になるため、faucetします。

入手方法は色々ありますが、手間的に一番楽なのは下記サイトにアクセスして、現在アクティブになっているアドレスを入力してsuiを受け取る方法です。

https://faucet.wavewallet.app/

suiを取得したら先ほど作成したmoveをビルドします。

sui move build

上記コマンドはpackage内にいる状態で入力する必要があります。

問題なくビルドできた場合は、続けて

sui client publish --gas-budget 1000

としてデプロイします。

----- Certificate ----
Transaction Hash: FSmQtv7YEbrcSbetjzk11Tfr9y89xMza2BuEaiZVaSsS
Transaction Signature: AA==@d/uHJfE0NwREDq+CkecWkEM50oGjYA8pMJ/QQ2+akqlHwxn7VuH5Or1LL6cc0EukzwG5WcjaY362tCDlfQp4Bg==@9W39ppJpVVpiiT8UB0snVjVuv9b6Ew0HPWi6Pg0rxjw=
Signed Authorities Bitmap: RoaringBitmap<[0, 1, 3]>
Transaction Kind : Publish
Sender: 0x70787e97dd3b6cc091a5dd729afe6a80b0f6878e
Gas Payment: Object ID: 0x1fa52f77311868747cb9f9618124711eba31a9a3, version: 0x3b, digest: 0x9e7f812c034bca24ded4280614f2ec2288985b931b633c3c8c8f4f5eca6ee1b7
Gas Price: 1
Gas Budget: 1000
----- Transaction Effects ----
Status : Success
Created Objects:
  - ID: 0x34dae771bc5304be69aaef49f30c20cf8ad272ff , Owner: Account Address ( 0x70787e97dd3b6cc091a5dd729afe6a80b0f6878e )
  - ID: 0xbeaf4fe77260005cc6d24ef4336d3a95ace5ead1 , Owner: Immutable
Mutated Objects:
  - ID: 0x1fa52f77311868747cb9f9618124711eba31a9a3 , Owner: Account Address ( 0x70787e97dd3b6cc091a5dd729afe6a80b0f6878e )

上記のようなログが出てきます。

sui explorerにアクセスして、Transaction Hashの部分を検索してみましょう。

こんな感じの情報が見れます。

Updatedに表示されているアドレスはガス代に使用されたsuiオブジェクト、Createdに表示されているのは作成されたオブジェクト及びパッケージ、Senderに表示されているのはこのtxを送ったアカウントです。

トランザクションを発行してcount_upやcount_downの関数を実行していきたいので、作成されたオブジェクトとパッケージを見ていきます。

5. 作成されたオブジェクトの中身を見る

これはfun init()の部分で作成されたオブジェクトです。

Typeを見ると{package_address}::{設定したモジュール名}::{設定した構造体名}担っていることがわかります。

Propertiesには増減させたいcount。またcount_historyには何度up, downさせたのかをメモするためのフィールドup, downがあることがわかります。

このオブジェクトのカウントを増減させていきたいため、そのために必要なこのオブジェクトのアドレスをコピーしておいてください。

6. 作成されたパッケージの中身を見る

作成されたパッケージの中身はこんな感じになっています。

Executeの部分を見ると、public entryとして定義した関数が表示されていることがわかります。

swaggerみたいな感じで、ここからこれら関数を実行することが可能です(要ウォレット接続)

このパッケージのアドレスもトランザクションを実行するために必要なのでコピーしておきます。

7. 作成した関数をトランザクションを発行して実行していく

sui client call --gas-budget 1000 --package {パッケージアドレス} --module {モジュール名} --function {関数名} --args {引数(更新したいオブジェクトID)}

具体的に書いたほうがわかりやすいと思うので、先程自分の方でデプロイしたパッケージとオブジェクトをベースに書いてみます。

sui client call --gas-budget 1000 --package 0xbeaf4fe77260005cc6d24ef4336d3a95ace5ead1 --module "my_counter" --function "count_up" --args 0x34dae771bc5304be69aaef49f30c20cf8ad272ff

これで、対象オブジェクトのカウントを+1することができます。

実行すると同じようにトランザクションの結果が表示されます。実際オブジェクトのcountがどうなったのか見てみましょう。

count が+1され、count_history内のupも+1されています。想定通りの挙動をしているため成功です。

今度はcount_downを実行してみましょう。

sui client call --gas-budget 1000 --package 0xbeaf4fe77260005cc6d24ef4336d3a95ace5ead1 --module "my_counter" --function "count_down" --args 0x34dae771bc5304be69aaef49f30c20cf8ad272ff

ちゃんと、count_historyのdownとupが1になっており、countが0です。では更にココからもう一度count_downを実行してみます。

Error calling module: Failure {
    error: "MoveAbort(MoveLocation { module: ModuleId { address: beaf4fe77260005cc6d24ef4336d3a95ace5ead1, name: Identifier(\"my_counter\") }, function: 0, instruction: 10 }, 0)",
}

こんな感じでエラーとなりtxが実行が中止されました。これはcount_downのところで実装したabortがちゃんと働いてくれている証拠です。

8. おまけ

ここまで自分のオブジェクトを変更するコマンドを書いていましたが、それらをあなたの環境で実行しても失敗します。

これはtransfer::transfer()で作成したobjectは、指定したアドレスが所有するオブジェクトとなるため、トランザクションの署名書がその指定したアドレスでなければそのオブジェクトを使用することが出来ない。という仕様があるためです。

https://docs.sui.io/build/move/sui-move-library

もしも誰でも変更可能なオブジェクトを作りたいのだとしたら、先程のコードのfun init()を以下のように変更します。

fun init(ctx: &mut TxContext) {
    let counter = Counter {
        id: object::new(ctx),
        count: 0,
        count_history: CountHistory {
            up: 0,
            down: 0,
        }
    };
    // transfer::transfer(counter, tx_context::sender(ctx));
    transfer::share_object(counter);
}

transfer::share_object()で作成されたオブジェクトはOwnerがSharedとなり、署名者が誰であってもそのオブジェクトを使用するトランザクションを実行することが可能です。


Solanaでしかオンチェーンプログラムを実装したことがなかったのですが、それと比べるとデコードなどの処理がなくかなりとっつきやすい印象を受けました。(まだまだ浅瀬でチャプチャプしている状態ですが…)

できることが増えれば増えるほど楽しい気もするので、もっと色々やってみたいですね。

Discussion