0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

How to develop a blockchain game. Day7 ゲーム内通貨購入

Last updated at Posted at 2025-01-02

Previous << Day6 - プレイヤー登録
Next >> Day8 - デッキ編集

Why Flow

FLOW(または$FLOW)トークンは、Flowネットワークのネイティブ通貨です。開発者およびユーザーは、FLOWを使用してネットワーク上で取引(transact)を行うことができます。開発者は、ピアツーピア決済(他人同士の決済)、サービス料金徴収、または消費者向け特典(rewards)のために、FLOWを直接アプリに統合することができます。

ということでやっていきます、猿でも分かるP2P(ピアツーピア)決済アプリ開発!

💡もし、エミュレータの起動方法やスマートコントラクトのデプロイについて操作に自信がない場合はこちらを参照してください。

現在のゲーム内通貨残高を表示する

AwesomeCardGame.cdcPlayerリソースの定義に以下のようにget_player_scoreメソッドを追加します。

AwesomeCardGame.cdc
           :

  access(all) resource Player {
    access(all) let player_id: UInt
    access(all) let nickname: String

    init(nickname: String) {
      AwesomeCardGame.totalPlayers = AwesomeCardGame.totalPlayers + 1
      self.player_id = AwesomeCardGame.totalPlayers
      self.nickname = nickname

      AwesomeCardGame.playerList[self.player_id] = CyberScoreStruct(player_name: nickname)
    }

    access(all) fun get_player_score(): CyberScoreStruct {
      return AwesomeCardGame.playerList[self.player_id]!
    }
  }

           :

デプロイ済みのAwesomeCardGameスマートコントラクトの更新自体は以下のコマンドでできるのですが

flow accounts update-contract ./AwesomeCardGame.cdc

実はリソースをストレージに保存済みである場合、(Day6でPlayerリソースをアカウントストレージに保存していましたので)リソースが古い状態でありget_player_scoreを呼び出そうとしてもunknown memberとなり呼び出しに失敗します。
そのためエミュレータアカウントがリソースを持たない状態まで戻す必要があり、一度エミュレータを止めて、エミュレータを再起動する必要があります。そうすると、アカウントのリソースも消えてなくなります。
よって、エミュレータアカウントにリソースを保存した状態で、スマートコントラクトのリソース定義を変更した場合の確認手順は

  1. エミュレータを止める
  2. flow emulator -vコマンドでエミュレータを起動する
  3. flow dev-walletコマンドでDev Walletを起動する
  4. flow project deployコマンドでAwesomeCardGameスマートコントラクトをデプロイする
  5. 新規登録ボタンを押してPlayerリソースを保存する
  6. これでもうまくいかない時はブラウザ上でDevWalletのログインをし直す(ログイン情報が古い為)

となります。

Day5のFlowToken所持金取得スクリプトを以下のように加筆します。

get_balance.cdc
    import "AwesomeCardGame"
    import "FlowToken"
    import "FungibleToken"

    access(all) fun main(address: Address): [AnyStruct] {
      let data: [AnyStruct] = []
      let vaultRef = getAccount(address).capabilities
          .borrow<&FlowToken.Vault>(/public/flowTokenBalance)
        ?? panic("Something wrong happened.")
      let cap = getAccount(address).capabilities
          .borrow<&AwesomeCardGame.Player>(/public/AwesomeCardGamePlayer)
        ?? panic("Doesn't have capability!")

      data.append(vaultRef.balance)
      data.append(cap.get_player_score().cyber_energy)
      return data
    }

flowTokenBalanceはUFix64、ゲーム内通貨(cyber_energy)はUInt8であるため、一度にこれらの値を返却できるように、返却値の型を[AnyStruct]としました。AnyStructは全ての一般型の上位互換型です。

これをfcl.queryで取得できるようにして、次に+page.svelteを以下のように修正します。

+page.svelte
           :
let flowBalance;
let cyberEnergyBalance; <- 追加
let hasResource;
config({
  'flow.network': network,
  'accessNode.api': 'http://localhost:8888',
  'discovery.wallet': 'http://localhost:8701/fcl/authn',
}).load({ flowJSON });
currentUser.subscribe(async (user) => {
  walletUser = user;
  if (user.addr) {
    hasResource = await isRegistered(user.addr);
    if (hasResource) { <- 修正
      const [flowTokenBalance, cyberEnergy] = await getBalance(user.addr); <- 修正
      flowBalance = flowTokenBalance; <- 修正
      cyberEnergyBalance = cyberEnergy; <- 修正
    }
  }
});
</script>
{#if !walletUser?.addr}
  <button onclick={authenticate}>ログイン</button>
{/if}
{#if walletUser?.addr}
  FLOW残高: {flowBalance} / ゲーム内通貨: {cyberEnergyBalance} <- 修正
           :

この状態で画面を表示すると以下のようにゲーム内通貨の残高を表示できます。
スクリーンショット 2025-01-02 10.08.24.png

リソースのニックネームも一緒に表示する

Day6でPlayerリソースを新規登録した時にnicknameも登録していたのでそれも一緒に画面に表示します。

get_balance.cdc
           :
      data.append(vaultRef.balance)
      data.append(cap.get_player_score().cyber_energy)
      data.append(cap.get_player_score().player_name) <- 追加
      return data
    }
+page.svelte
           :
let playerName; <- 追加
let flowBalance;
let cyberEnergyBalance;
let hasResource;
config({
  'flow.network': network,
  'accessNode.api': 'http://localhost:8888',
  'discovery.wallet': 'http://localhost:8701/fcl/authn',
}).load({ flowJSON });
currentUser.subscribe(async (user) => {
  walletUser = user;
  if (user.addr) {
    hasResource = await isRegistered(user.addr);
    if (hasResource) {
      const [flowTokenBalance, cyberEnergy, pName] = await getBalance(user.addr); <- 修正
      flowBalance = flowTokenBalance;
      cyberEnergyBalance = cyberEnergy;
      playerName = pName; <- 追加
    }
  }
});
</script>
{#if !walletUser?.addr}
  <button onclick={authenticate}>ログイン</button>
{/if}
{#if walletUser?.addr}
  <b>{playerName}さん</b> FLOW残高: {flowBalance} / ゲーム内通貨: {cyberEnergyBalance}<br> <- 修正
           :

この状態で画面を表示すると以下のようにプレイヤーが登録したnicknameも表示することができます。
スクリーンショット 2025-01-02 10.19.19.png

ゲーム内通貨を購入する

では画面上に表示されているFLOW残高を使ってゲーム内通貨を購入していきます。

なぜFLOWがあるのにゲーム内通貨を購入するかというと、それはFLOWはアカウントのストレージにあるのでユーザーの許可なしに動かせないのに対して、ゲーム内通貨はスマートコントラクト内にあるのでAdminリソースが自由に変更できるからです。

なぜ、これが利点かというと、アカウントのストレージにFLOWがあるということは、これを引き出すときには必ずウォレットのポップアップでApproveボタンを押す必要があります。

これが割と面倒くさいのです。ポップアップでApproveするとFLOWを自分のウォレットから引き出すことができるのですが、ゲームは短時間に何回も入金することがあります。その度にポップアップでApproveするのは面倒くさいので、あらかじめゲーム3~4回分ぐらいあらかじめ購入できるように、ゲーム内通貨が登場します。

AwesomeCardGame.cdcPlayerリソースの定義に以下のようにbuy_enメソッドを追加します。

AwesomeCardGame.cdc
           :

  access(all) resource Player {
    access(all) let player_id: UInt
    access(all) let nickname: String

    init(nickname: String) {
      AwesomeCardGame.totalPlayers = AwesomeCardGame.totalPlayers + 1
      self.player_id = AwesomeCardGame.totalPlayers
      self.nickname = nickname

      AwesomeCardGame.playerList[self.player_id] = CyberScoreStruct(player_name: nickname)
    }

    access(all) fun get_player_score(): CyberScoreStruct {
      return AwesomeCardGame.playerList[self.player_id]!
    }

    access(all) fun buy_en(payment: @FlowToken.Vault) {
      pre {
        payment.balance == 2.0: "payment is not 2FLOW coin."
        AwesomeCardGame.playerList[self.player_id] != nil: "CyberScoreStruct not found."
      }
      AwesomeCardGame.FlowTokenVault.borrow()!.deposit(from: <- payment)
      if let cyberScore = AwesomeCardGame.playerList[self.player_id] {
        cyberScore.set_cyber_energy(new_value: cyberScore.cyber_energy + 100)
        AwesomeCardGame.playerList[self.player_id] = cyberScore
      }
    }
  }

           :

すみません、entitlementによるアクセス制限の実装を忘れてました(access(all)はよい実装ではないです)。entitlementについてはDay3を参照して下さい🙇

ユーザーから受け取った2FLOWをAdminのVaultに入れて(deposit)、代わりにユーザーのゲーム内通貨(cyber_energy)をプラス100しています。(このAdminのVaultに入ったFLOWはDay4のやり方で取引所とかにすぐに送金・換金できます!)

AwesomeCardGame.cdcCyberScoreStructに以下のようにset_cyber_energySetter関数を追加します。
(access(all)のStruct(構造体)のパラメータは、Setter関数が無いと、更新しようとする時にエラーになるためです)

AwesomeCardGame.cdc
           :
  access(all) struct CyberScoreStruct {
    access(all) let player_name: String
    access(all) var score: [{UFix64: UInt8}]
    access(all) var win_count: UInt
    access(all) var loss_count: UInt
    access(all) var ranking_win_count: UInt
    access(all) var ranking_2nd_win_count: UInt
    access(all) var period_win_count: UInt
    access(all) var period_loss_count: UInt
    access(all) var cyber_energy: UInt8
    access(all) var balance: UFix64

    access(contract) fun set_cyber_energy(new_value: UInt8) {
      self.cyber_energy = new_value
    }

    init(player_name: String) {
      self.player_name = player_name
      self.score = []
      self.win_count = 0
      self.loss_count = 0
      self.ranking_win_count = 0
      self.ranking_2nd_win_count = 0
      self.period_win_count = 0
      self.period_loss_count = 0
      self.cyber_energy = 0
      self.balance = 0.0
    }
  }

           :

このbuy_en関数を実行するトランザクションは以下のようになります。

      import "AwesomeCardGame"
      import "FlowToken"
      import "FungibleToken"

      transaction() {
        prepare(signer: auth(BorrowValue) &Account) {
          let payment <- signer.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: /storage/flowTokenVault)!.withdraw(amount: 2.0) as! @FlowToken.Vault

          let player = signer.storage.borrow<&AwesomeCardGame.Player>(from: /storage/AwesomeCardGamePlayer)
              ?? panic("Could not borrow reference to the Owner's Player Resource.")
          player.buy_en(payment: <- payment)
        }
        execute {
          log("success")
        }
      }

ユーザーのウォレットから2FLOW引き出し、それを先ほどのメソッド、buy_enメソッドに渡しています。

これをjsで呼ぶ必要がありますので、そのメソッドは以下のようになります。

transactions.js
import { mutate, authz } from "@onflow/fcl";

export const buyCyberEn = async function () {
  const txId = await mutate({
    cadence: `
      import "AwesomeCardGame"
      import "FlowToken"
      import "FungibleToken"

      transaction() {
        prepare(signer: auth(BorrowValue) &Account) {
          let payment <- signer.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(from: /storage/flowTokenVault)!.withdraw(amount: 2.0) as! @FlowToken.Vault

          let player = signer.storage.borrow<&AwesomeCardGame.Player>(from: /storage/AwesomeCardGamePlayer)
              ?? panic("Could not borrow reference to the Owner's Player Resource.")
          player.buy_en(payment: <- payment)
        }
        execute {
          log("success")
        }
      }
    `,
    args: (arg, t) => [],
    proposer: authz,
    payer: authz,
    authorizations: [authz],
    limit: 999,
  });
  console.log(txId);
  return txId;
};

+page.svelteに以下のコードを追加します

+page.svelte
import { createPlayer, buyCyberEn } from '../transactions'

           :
  {#if hasResource}
    Playerリソース作成済みです
    <br>
    <button onclick={buyCyberEn}>CyberEnergy購入</button>
  {/if}

これで準備ができました。画面上でCyberEnergy購入ボタンを押して、Approveを押します。

結果:
スクリーンショット 2025-01-02 12.24.45.png

Flow残高が減ってないけど、ゲーム内通貨は増えてます。これはAwesomeCardGameスマートコントラクトをデプロイしているのがemulator-accountでありPlayerリソースをアカウントに保存したのもemulator-accountなので、CyberEnergy購入処理で一度2FlowTokenを引き出したものの、スマートコントラクトの中でFlowTokenVaultに2FlowTokenを預け入れたので、結局同じ額に戻ってしまいました。

そこで一度ログアウトして新しいアカウントを作成してそのアカウントでゲーム内通貨購入を試してみます。

Create New Accountをクリックします。
スクリーンショット 2025-01-02 12.32.00.png

Createをクリックします:
スクリーンショット 2025-01-02 12.33.03.png

アカウントが作られました。(FlowTokenもたくさん持っています。)
スクリーンショット 2025-01-02 12.33.45.png
(何故舌を出す...)

Account Aでログインして、新規登録します。(nicknameはコード直打ちでしたが少し変更しました)
スクリーンショット 2025-01-02 12.37.10.png

これで準備ができました。画面上でCyberEnergy購入ボタンを押して、Approveを押します。

結果
スクリーンショット 2025-01-02 12.39.47.png

2FLOWを支払ってゲーム内通貨100(CyberEnergy)を購入しました。

ここで、元のemulator-accountのアカウントに戻るとどうなるでしょう?一度ログアウトしてemulator-accountのアカウントでログインし直してみましょう。

結果:
スクリーンショット 2025-01-02 12.42.21.png

前は999999999.995だったのが1000000001.994に増えてます。つまり、1.999FLOW増えたことになっています。(0.001は先ほどアカウントを作成した時のトランザクション費用です)

エミュレータが計算する0.001FLOWなどのトランザクション費用は考えられる限り最も高い費用が設定されています。実際は0.000003FLOW未満になると考えてよく、非常に少額になります。(Flowの転送だけなら$0.00000185FLOWだけです。)

これでピアツーピア決済アプリの中枢部分が実装できたことになります。

Mainnetにデプロイしたらもうあなたは世界レベルの事業者です。MainnetデプロイはCLIで鍵作成して、CLIでデプロイします。


この記事のソースコードはこちらにあります。


Previous << Day6 - プレイヤー登録

Flow BlockchainのCadence version1.0ドキュメント

Next >> Day8 - デッキ編集

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?