Open16
move and sui
- モジュールの新規作成
move new Sample0713
cd Sample0713
touch sources/SmapleCoin.move
- ビルドとデプロイ(Sandbox環境へ)
move build
move sandbox publish
build/
と storage/
のディレクトリが生成される
SuiとMoveの違い
項目 | Move | Sui |
---|---|---|
リソースの基本 | アカウント | オブジェクト |
ストレージ | Map<(address, ResourceType)> | Map<ObjectID, Object> |
- 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;
}
- 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);
}
}
- 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))
}
}
- entry関数
entryをつけて関数を定義することで、関数をトランザクションなどから直接呼び出せるようになる
トランザクションで直接呼び出すことしかできないため、return valueを持てない
- 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している」という意味。
- 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,
}
上記suiのリポジトリにあるexamplesが参考になる
基本的なオブジェクト定義のサンプル
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) });
}
}
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);
}
}
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);
}
}
サンプルファイルのデプロイ方法
参照: 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つ落とすとエラーが出る。
- アカウントの残りのガスをチェック
上記のデプロイ実行後
sui client gas `sui client active-address`
結果
Object ID | Gas Value
----------------------------------------------------------------------------------
0xXXXXXXXXXXXXXXXXXXXXXXXXXXXX | 9985652760
あとは
に大量にドキュメントがあるので、読み込めば開発できるようになりそう開発の流れ
- 初期化
sui move new ディレクトリ名
- move書く
- test書く
- ビルドチェック
sui move build
- テスト実行
sui move test