ブロックチェーンゲームの作り方7 ゲーム内通貨購入

[Previous << 6 - プレイヤー登録](https://qiita.com/flowcadence/items/8443d8965a8f98486fa8)
[Next >> 8 - デッキ編集](https://qiita.com/flowcadence/items/a701a42d833c63ef3b9a)

https://qiita.com/flowcadence/items/3073d69c6e9895e68b5a#%E9%96%8B%E7%99%BA%E6%96%B9%E6%B3%95

:::note
**Flowブロックチェーンの強みは実際に自分の強みをお金に変えられること**

他業種からIT業界に転身した人なら、ITの知識を活用すれば自分一人でお金を稼げることをいつか知ることになります。

しかし、競争の激しい分野はそれだけ稼ぐのが難しくなります。

でもやりがいは残ります。経済の隆盛は様々な要因が絡みます。その中で伸びる業界、というのはお金を稼げる、と注目される業界です。（2010年頃はそれはそれはGoogleやFacebookのニュースが沢山Bloomberg記事に載ったものです。ITエンジニアとしてはBloombergが技術情報を提供してくれることに不思議な気持ちでした）

Flowブロックチェーンは世界中の人に対して、自分が作ったサービスでお金を稼ぐことができます。金融決済処理を安全で簡単に24時間、コストをかけずに世界中に提供できるためです。

**安全でコストをかけない金融決済処理を自分1人だけで作れるのがFlowを選ぶメリットです。**

以下のコードを見てください、ストレージからお金を引き出しています。[Linear Types](https://qiita.com/flowcadence/items/891aaf12938b0e697ad3#the-move-operator--)という特殊なコードの実装でそれをスマートコントラクトに渡します。スマートコントラクトにはサービスを展開した人の[入金先](https://github.com/temt-ceo/awesome-cardgame/blob/main/src/AwesomeCardGame.cdc#L813)がありますのでそこに入金します。
このコードはブロックチェーン上で動き、そのコンピュータを用意する必要はありません。世界中で24時間そのコードは動き続け、コンピュータが止まることの心配、および運営費用の心配をする必要はありません。つまり24時間お金を稼ぎ続ける仕組みをあなたは1円もお金を出さずに作れるのです。トランザクション費用はウォレットが代わりに払ってくれます。是非、[サンプルアプリを動かして](https://qiita.com/flowcadence/items/bd9dd37723dab0557064#%E6%97%A2%E5%AD%98%E3%81%AEhelloworld%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E3%82%A4%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%88)その凄さを実感してみてください

```ruby
      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)
        }        
```

*Linear TypesはAptos, Sui, Flowブロックチェーンに共通する資産を実際に移動させるロジックです*

:::

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

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

:::note warn
💡もし、エミュレータの起動方法やスマートコントラクトのデプロイについて操作に自信がない場合は[こちら](https://qiita.com/flowcadence/items/8cc25249cda37364cac1)を参照してください。
:::

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

`AwesomeCardGame.cdc`の`Player`リソースの定義に以下のように`get_player_score`メソッドを追加します。
```ruby: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]!
    }
  }

           :
```
:::note warn
デプロイ済みのAwesomeCardGameスマートコントラクトの更新自体は以下のコマンドでできるのですが
```ruby
flow accounts update-contract ./AwesomeCardGame.cdc
```
実はリソースをストレージに保存済みである場合、(Day6で`Player`リソースをアカウントストレージに保存していましたので)**リソースが古い状態であり**、`get_player_score`を呼び出そうとしても`unknown member`となり呼び出しに失敗します。
そのためエミュレータアカウントがリソースを持たない状態まで戻す必要があり、一度エミュレータを止めて、エミュレータを再起動する必要があります。そうすると、アカウントのリソースも消えてなくなります。
よって、エミュレータアカウントにリソースを保存した状態で、スマートコントラクトのリソース定義を変更した場合の確認手順は
1. エミュレータを止める
1. `flow emulator -v`コマンドでエミュレータを起動する
1. `flow dev-wallet`コマンドでDev Walletを起動する
1. `flow project deploy`コマンドで`AwesomeCardGame`スマートコントラクトをデプロイする
1. 新規登録ボタンを押して`Player`リソースを保存する
1. これでもうまくいかない時はブラウザ上でDevWalletのログインをし直す（ログイン情報が古い為）

となります。
:::
[Day5のFlowToken所持金取得スクリプト](https://qiita.com/flowcadence/items/8ff1ed3535bc368f7c68#%E4%BB%A5%E4%B8%8B%E3%81%8Cflowtoken%E6%89%80%E6%8C%81%E9%87%91%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%81%A7%E3%81%99)を以下のように加筆します。
```ruby: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で取得できるようにして](https://qiita.com/flowcadence/items/8ff1ed3535bc368f7c68#%E6%AE%8B%E9%AB%98%E3%82%92js%E3%81%A7%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B)、次に`+page.svelte`を以下のように修正します。

```ruby:+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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/e8b916a3-c286-673b-64f0-e47e1349c35e.png)


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

Day6で`Player`リソースを新規登録した時に`nickname`も登録していたのでそれも一緒に画面に表示します。
```ruby: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
    }
```
```ruby:+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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/6c3839d2-a4b0-569c-0263-907100323fab.png)

#### ゲーム内通貨を購入する

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

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

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

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

`AwesomeCardGame.cdc`の`Player`リソースの定義に以下のように`buy_en`メソッドを追加します。
```ruby: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を参照して下さい](https://qiita.com/flowcadence/items/2911220eb06a20beea14)🙇*

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

`AwesomeCardGame.cdc`の`CyberScoreStruct`に以下のように`set_cyber_energy`というSetter関数を追加します。
*(`access(all)`のStruct(構造体)のパラメータは、Setter関数が無いと、更新しようとする時にエラーになるためです)*
```ruby: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`関数を実行するトランザクションは以下のようになります。
```ruby
      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`メソッドに渡しています。なぜ`borrow`をしているのか、と不思議に思われるかもしれませんが、リソースをストレージの中から取り出して、リソースの`buy_en`メソッドを実行し、リソースを再びストレージに格納し直してもいいです。ただし、それはコンピューティングコストがかかり、トランザクションフィーが上がります。そこで、トランザクションフィーを安く抑えるために、ストレージ内のリソースを`borrow`によって(C言語のポインタのように)参照だけ取得しています。また、再格納が必要ないのでコード量も減ります。<sup>*buy_enメソッドのアクセス制御が`access(all)`ではなく、entitlementを設定していればbuy_enメソッドはstorage内のリソース参照からしか実行できず、つまりリソースの持ち主からしか実行できなくなります。まぁ代わりにゲーム料金を払ってくれるぐらいで、今回は`access(all)`でも害がないですが。。*</sup>

これをjsで呼ぶ必要がありますので、そのメソッドは以下のようになります。
```ruby: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`に以下のコードを追加します
```ruby:+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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/dbf55d2b-55b5-0802-4c45-a8319dfbb142.png)

結果は、Flow残高が減ってないけど、ゲーム内通貨が100に増えています。

これは、スマートコントラクトをデプロイした人(`emulator-account`)が`Player`リソースを使って購入した人と同一人物なので、購入処理で一度`2FlowToken`を引き出しはしたものの、スマートコントラクトの中でFlowTokenVaultに`2FlowToken`を預け入れたので、結局同じ額に戻ってしまった結果でした。

そこで一度ログアウトして新しいアカウントを作成してみます。新しいアカウントでログインし`Player`リソースを作成・保存し、それを通じてゲーム内通貨購入を試してみます。

Create New Accountをクリックします。
![スクリーンショット 2025-01-02 12.32.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/f5305764-d75e-1473-5860-891b0722b6a8.png)

Createをクリックします:
![スクリーンショット 2025-01-02 12.33.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/31a8908b-63ea-c79b-c04a-eaac09370310.png)

アカウントが作られました。（FlowTokenもたくさん持っています。）
![スクリーンショット 2025-01-02 12.33.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/4724aace-656f-565b-3f06-773e827aacb7.png)
<sup>(何故舌を出す... Lukeの仕業か..)</sup>

Account Aでログインして、新規登録します。(`nickname`はコード直打ちでしたが少し変更しました)
![スクリーンショット 2025-01-02 12.37.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/a5accf70-ada0-6e35-bf36-e764592a5f51.png)

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

結果
![スクリーンショット 2025-01-02 12.39.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/73fe4da4-8110-3c84-37c2-901e7780e09b.png)

2FLOWを支払ってゲーム内通貨100(CyberEnergy)を購入しました。(今度はちゃんとFLOW残高が2減っています)

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

結果:
![スクリーンショット 2025-01-02 12.42.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3912821/02d3511b-cb2d-7ab5-bd2c-8bf3a03884ee.png)

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

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

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

Mainnetにデプロイしたらもうあなたは世界レベルの決済アプリ事業者の一員です。
(MainnetデプロイはFlow CLIで鍵を作成して、Flow CLIでデプロイして行います)
***

この記事のソースコードは[こちら](https://github.com/temt-ceo/awesome-cardgame)にあります。

***
<b>[Previous << 6 - プレイヤー登録](https://qiita.com/flowcadence/items/8443d8965a8f98486fa8)
</b>

[Flow blockchain / Cadence version1.0ドキュメント](https://qiita.com/flowcadence/items/3073d69c6e9895e68b5a#%E9%96%8B%E7%99%BA%E6%96%B9%E6%B3%95)

<b>[Next >> 8 - デッキ編集](https://qiita.com/flowcadence/items/a701a42d833c63ef3b9a)</b>