見出し画像

【Sui直コンバトル】1st 直コン手順解説

この記事では、Sui直コンバトル 1stにおいて、どのような手順で直コンを呼べば報酬を獲得できたのか?という解説をしたいと思います📝

今回の1stバトルでは、チャレンジャーと直コンマスターという2つのタスクを用意しました

🔹報酬について
チャレンジャーは成功すれば確実に5 SUIをゲットでき、直コンマスターは最大5名がSUIやKOTOをゲットできるように設計しました

🔹難易度について
チャレンジャーは他の参加者が成功したトランザクションを完コピすれば達成できるような容易さにし、直コンマスターは他者のトランザクションを完コピしただけでは達成できないような難しさにしました

もちろんどちらとも、他者のトランザクションを参考にせずとも、コントラクトコードおよびコントラクト運営者のトランザクションを読み解けさえすれば達成できるタスクではありますが、参加者のトランザクションを参考にするのはとても良い一手です💡ただし、完コピを成功できた経験をもとに直コンマスターに挑もうとすると永遠に成功しない、という罠に陥る危険性もありました

この記事では、コントラクトコードと運営者のトランザクションを読み解いて2つのタスクを達成する方法を解説していきます。他参加者のトランザクションは発生していない前提です。また、コードの詳細までは踏み込みません

コントラクトとコードについては以下の記事を参照ください

チャレンジャータスクの解説

チャレンジャータスクは、mintという関数を適切に実行すれば、成功します

   entry public fun mint(director: &mut Director, ctx: &mut TxContext) {
        assert!(director.paused == false, EDirectorIsPaused);
        assert!(table::contains(&director.participants, ctx.sender()) == true, ENotWhitelisted);
        assert!(table::contains(&director.minted, ctx.sender()) == false, EAlreadyMinted);

        let reward_balance = director.mint_success_reward.split(MINT_SUCCESS_REWARD_SUI);
        transfer::public_transfer(coin::from_balance(reward_balance, ctx), ctx.sender());

        table::add(&mut director.minted, ctx.sender(), true);

        transfer::transfer(ChallengerNFT { id: object::new(ctx) }, ctx.sender());
    }

mint関数には、2つのオブジェクトを引数として渡す必要があります

・Director: 共有オブジェクト
・TxContext: 特殊なオブジェクト

このうち、TxContextというオブジェクトは、Moveの実行環境側で自動で渡してくれるため、直コンを呼ぶユーザーが指定が渡す必要はありません。

💡 TxContextオブジェクトを渡さなくて良いのはmint関数だけではなく、全ての関数に共通して言えることです。また、TxContextは、現在実行中のトランザクションに関する情報(トランザクション実行者のアドレスなど)が含まれています

SuiVisionからmint関数を実行するフォームを見ると、渡すべき引数の入力欄が1つしかないのはこのためです

つまり、mint関数に渡すべきはDirectorオブジェクト1つのみとなります

💡 Suiのオブジェクトについては、Yohさんとくりぷとくりぷとさんのnote記事がわかりやすいです!

では、Directorオブジェクトとは一体どんなオブジェクトなんでしょうか?

// Directorオブジェクトの構造    
public struct Director has key, store {
    id: UID,
    paused: bool,
    participants: Table<address, bool>,
    minted: Table<address, bool>,
    rewarded: Table<address, bool>,
    mint_success_reward: Balance<SUI>,
}

// init関数
fun init(otw: CHOKUCON_BATTLE_FIRST,  ctx: &mut TxContext) {
    // 中略

    // Directorオブジェクトを作成し、共有オブジェクトとして公開
    transfer::share_object(Director {
        id: object::new(ctx),
        paused: true,
        participants: table::new<address, bool>(ctx),
        minted: table::new<address, bool>(ctx),
        rewarded: table::new<address, bool>(ctx),
        mint_success_reward: balance::zero(),
    });
}

上記がDirectorオブジェクトに関連するコードを抜粋したものです

structはオブジェクトがどんなデータを保持するかを定義するためのものです
Directorでは、
・バトルの停止再開をコントロールするpausedフィールド
・登録参加者を管理するparticipantsフィールド
・mint成功時の報酬を格納しておくmint_success_rewardフィールド
などが定義されています

このDirectorオブジェクトがinit関数という、コントラクト公開時に1度だけ呼ばれる特殊な関数の中で"1つだけ"作成され、共有オブジェクトとして公開されています

つまり、Directorオブジェクトは、バトル全体の情報が格納されているオブジェクトであり、誰もがアクセスできる共有オブジェクト、ということがわかります💡

もう一度mint関数を見てみましょう

entry public fun mint(director: &mut Director, ctx: &mut TxContext)

mint関数はDirectorオブジェクトを渡せばいいのでした

そして、Directorオブジェクトは今回、init関数で1つだけ作成されているのでした。もしDirectorオブジェクトが複数作成されていたら、どのDirectorを渡すか迷ってしまいますが、今回は1つだけです💡

つまり、このDirectorオブジェクト(正確にはオブジェクトのID)を見つけ出し、そのIDをArg0に入力してExecuteを実行すれば成功します!

さて、Directorオブジェクトはどうやって見つければよいのでしょうか?

init関数によってDirectorオブジェクトが作成されるので、コントラクト公開時のトランザクションを見てみましょう

コントラクト公開時のトランザクションは「Packageページ -> Transaction Blocksタブ -> Updated Objectsタブ の一番古いトランザクション」です

上記が今回のコントラクト公開時のトランザクションです。リンクを開いてください

Transaction Blockというのがトランザクションのページです。ここの「Changesタブ -> Object Changes」の部分に、コントラクト公開時にCreated(作成) / Mutated(変更)オブジェクトや、Published(公開)されたオブジェクト(これがコントラクトそのもの)の情報が記載されています

Directorオブジェクトは作成されるものなので、ACTIONがCreatedのオブジェクトに注目しながら、TYPEの名前でDirectorという文字があるものを探します。ありましたね!OWNERがSharedになっていることもわかります

OBJECTのリンクをクリックしてみましょう

これがDirectorオブジェクトの実体です。右のFields欄に、Directorオブジェクトに格納されているデータが表示されているのがわかります

このDirectorオブジェクトのIDをmintのArg0に入力して、EXECUTEを実行すると、mintが成功し、チャレンジャー報酬と、達成の証であるチャレンジャーNFTがゲットできます🚀

画像はtestnetで行っているためIDが異なっています

Approveする前の画面はきちんと確認しておきましょう。特にBalance Changesは常にチェックしないといけません

Balance Changesで4.99 SUI(5 SUI - ガス代)がYou(あなたのアドレス)に送金されることがわかりますね!

これでチャンレンジャータスクは達成です💡

直コンマスタータスクの解説

直コンマスタータスクは、get_rewardという関数を適切に実行すれば成功します。mintと同様に関数のコードを見てみましょう

entry public fun get_reward<T>(_: &ChallengerNFT, director: &mut Director, reward: &mut Reward<T>, ctx: &mut TxContext) {
    assert!(director.paused == false, EDirectorIsPaused);
    assert!(table::contains(&director.participants, ctx.sender()) == true, ENotWhitelisted);
    assert!(table::contains(&director.rewarded, ctx.sender()) == false, EAlreadyRewarded);


    let balance = reward.balance.withdraw_all();
    transfer::public_transfer(coin::from_balance(balance, ctx), ctx.sender());

    table::add(&mut director.rewarded, ctx.sender(), true);

    transfer::transfer(MasterNFT { id: object::new(ctx) }, ctx.sender());
}

get_reward関数には、4つの引数を渡す必要があります。1つはTxContextのため、ユーザーが渡すべき引数は3つになります

・ChallengerNFT: 所有オブジェクト
・Director: 共有オブジェクト
・Reward: 共有オブジェクト

このうちDirectorオブジェクトは1つしか存在しないため、mintに渡した同じIDを渡すことになります

get_rewardが難しいのは、残りの2つ、ChallengerNFTとRewardの値の探し方になります

ChallengerNFTオブジェクトを探す

ChallengerNFTとはなんでしょうか?今までの説明からなんとなくわかるかもしれませんが、これはチャンレンジャータスクを達成した証としてユーザーに送信されるNFTになります

public struct ChallengerNFT has key, store {
    id: UID,
}

structはとてもシンプルです。ユニークなIDを持つフィールドだけが定義されています。このNFTはいつどのタイミングで作成されるのでしょうか?

もう一度mint関数を見てみましょう

entry public fun mint(director: &mut Director, ctx: &mut TxContext) {
    // 中略

    transfer::transfer(ChallengerNFT { id: object::new(ctx) }, ctx.sender());
}

mint関数の最後で、ChallengerNFTを作成し、 ctx.sender() つまり、トランザクション実行者にtransferしていることがわかります。プログラミング言語がわからなくても、英単語をざっと追えば雰囲気は掴めると思います💡

💡 コードを左から読むと、
・transferで何かを送ってるんだな〜
・何を?ChallengerNFTだね!
・誰に?ctx.sender()

ctxはトランザクション情報が入っているオブジェクトで、senderはトランザクション実行者だから、トランザクション実行者にNFTを送ってるんだ〜

といった具合

チャレンジャータスクの達成者が複数人いれば、ChallengerNFTは複数存在することになります。そのため、他人が持っているChallengerNFTのオブジェクトIDを指定しても意味はありません。自分が受け取ったNFTを見つける必要があります

💡 つまり、get_rewardは、チャレンジャータスクを達成した人のみが実行できる関数になっており、ChallengerNFTがその鍵となっているのです。このNFTを他のアドレスに送ってしまったりすると、get_rewardは呼べなくなってしまいます

mint関数のトランザクションを見てみましょう

自分のアカウントページを開き、Activityタブを開くとトランザクション履歴を見ることができます。ここのTYPE/TIMEがmintになっているものが該当のトランザクションです。ページを開きましょう

5 SUIが増え、ChallengerNFTを受け取っていることがわかります。画像でいうと、 0xfca6…75c6 が渡すべきIDになります!これはユーザーごとに異なります

Rewardオブジェクトを探す

Rewardオブジェクトとはなんでしょうか?これはDirectorオブジェクトと同じ共有オブジェクトです。ただしDirectorオブジェクトは1つだけしか存在しないのに対し、Rewardオブジェクトは複数存在します(今回のバトルの場合、6つ存在していました)

public struct Reward<phantom T> has key {
     id: UID,
     balance: Balance<T>,
}

structはシンプルで、idとbalanceというフィールドが定義されています。例えば1つのRewardオブジェクトには50 SUIというbalanceがあり、他の1つのRewardオブジェクトには30M KOTOというbalanceがある、という具合です

つまり、自分がほしい報酬が格納されているRewardオブジェクトを見つけ出し、そのIDを引数として渡す、というのがミッションです

では、Rewardオブジェクトはどこで作成されているのでしょうか?該当のコードを見てみましょう

entry public fun add_reward<T>(c: Coin<T>, ctx: &mut TxContext) {
    transfer::share_object(Reward {
        id: object::new(ctx),
        balance: c.into_balance(),
    });
}

add_reward関数が、Rewardオブジェクトを作成している関数です。引数としてCoin(SUIやKOTO)のオブジェクトIDを渡すだけの、とてもシンプルな関数です

運営がこのadd_reward関数を呼び、報酬を追加したのです。つまりそのトランザクションを見つけることで、Rewardオブジェクトの所在がわかります

緑は成功したトランザクション、赤は失敗したものです

Directorオブジェクトを見つけるために、コントラクト公開時のトランザクションを追いました。そのときは Updated Objects タブを開きました

一方、コントラクト公開後に呼ばれたトランザクションは Input Objects タブを開くことで見つけることができます。上記画像を見ると、ずらーっとトランザクションがあるのがわかりますね

トランザクションは、最新のものが上に表示されます。報酬の追加(add_reward)は、運営がコントラクト公開後に実行したと推測ができるので、そのトランザクションは下の方にあることが推測できます

ありました!このトランザクションページのChangesタブを開きます

Balance Changesを見ると、今回の報酬は合計 100 SUIと40M KOTOだったので、その分運営のアドレスからマイナスになっていることがわかります

Object Changesを見ると、Rewardオブジェクトが複数作成されているのがわかります。50 SUI、30 SUI、20 SUI、30M KOTO、10M KOTOのRewardオブジェクトがそれぞれ作成されている、ということですね

💡運営アドレスから出て行ったSUIとKOTOは、5つのオブジェクトにそれぞれ格納されているということですね

例として1つRewardオブジェクトを見てみましょう

これがRewardオブジェクトの実体です。balanceフィールドが0になっているのは、すでに誰かがこのオブジェクトを利用して報酬を獲得したということになります。獲得前はこの値が 50,000,000,000 (50 SUIのこと) などになっていた、ということです

このRewardオブジェクトのIDを3つ目の入力欄に渡すことで直コンマスターの報酬を獲得することができます!(balanceが0になっているRewardを指定しまうと、報酬はなしでNFTだけゲットできます😇)

実はまだ入力できていない欄がありますね。Type0 というところです。ここは少々専門的な話になってしまうのですが、結論からいうと、

・SUIのRewardオブジェクトを指定する場合は、Type(型)として 0x2::sui::SUI
・KOTOの場合は、Typeとして 0xa99166e802527eeb5439cbda12b0a02851bf2305d3c96a592b1440014fcb8975::koto::KOTO

を入力すればOKです。この文字列は、
コインID::モジュール名::コインシンボル名
という構成になっており、コインIDはDEXとかでよく見る長い文字列ですね

💡 SUIの場合は、0x2のように、本来は長い文字列(0x0000000000000000000000000000000000000000000000000000000000000002)を省略することができます

引数としては3つ入力するのですが、コインの型として1つ入力しなければならないのは、少し専門的でトリッキーな部分でもありました

entry public fun add_reward<T>(c: Coin<T>, ctx: &mut TxContext)

add_rewardをよく見ると <T> という表記がありますが、これがあると型を指定しないといけない、ということになります

💡 型はMove言語における専門的な話になるので、なんとなくの理解で結構です。型の指定が必要ない関数(例: mint関数)もあります

get_reward関数を適切に実行すれば、報酬をゲットし、達成の証として直コンマスターNFTが送付される仕組みです!


この記事が気に入ったらサポートをしてみませんか?