Previous << Day5 - 所持金表示
Next >> Day7 - ゲーム内通貨購入
Why Flow
FLOW(または$FLOW)トークンは、Flowネットワークのネイティブ通貨です。開発者およびユーザーは、FLOWを使用してネットワーク上で取引(transact)を行うことができます。開発者は、ピアツーピア決済(他人同士の決済)、サービス料金徴収、または消費者向け特典(rewards)のために、FLOWを直接アプリに統合することができます。
ということでやっていきます、猿でも分かるP2P(ピアツーピア)決済アプリ開発!
💡もし、エミュレータの起動方法やスマートコントラクトのデプロイについて操作に自信がない場合はこちらを参照してください。
プレイヤーResourceを作成する
リソースはタイトル保有数や戦績、実績の保有者であることを保証するものです。ゲームにはタイトルや戦績などさまざまな情報がありますので、それを保証してくれるものがリソースの存在です。また、リソースを通じて関数を呼び出すと自動的にリソースのIDが取得できるのも利点です。
Day3からHelloWorld.cdc
をsrc
内にコピーしてきてファイル名をAwesomeCardGame.cdc
に変更して、flow.json
の以下の部分を新しいコントラクト名に変更します。
"contracts": {
"AwesomeCardGame": "./AwesomeCardGame.cdc"
},
:
"deployments": {
"emulator": {
"emulator-account": ["AwesomeCardGame"]
}
}
AwesomeCardGame.cdc
を以下の内容に変更します。
import "FlowToken"
import "FungibleToken"
access(all) contract AwesomeCardGame {
access(self) let playerList: {UInt: CyberScoreStruct}
access(self) var totalPlayers: UInt
access(all) let starterDeck: [UInt16]
access(self) let FlowTokenVault: Capability<&{FungibleToken.Receiver}>
access(self) let PlayerFlowTokenVault: {UInt: Capability<&{FungibleToken.Receiver}>}
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) 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
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
}
}
access(all) fun createPlayer(nickname: String, flow_vault_receiver: Capability<&{FungibleToken.Receiver}>): @AwesomeCardGame.Player {
let player <- create Player(nickname: nickname)
if (AwesomeCardGame.PlayerFlowTokenVault[player.player_id] == nil) {
AwesomeCardGame.PlayerFlowTokenVault[player.player_id] = flow_vault_receiver
}
return <- player
}
init() {
self.FlowTokenVault = self.account.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
self.PlayerFlowTokenVault = {}
self.starterDeck = [1, 1, 2, 2, 3, 3, 4, 4, 5, 6, 7, 8, 9, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]
self.totalPlayers = 0
self.playerList = {}
}
}
<&FlowToken.Receiver>
ではなく、<&{FungibleToken.Receiver}>
となっているのはReceiver
がFungibleTokenインタフェース内で定義されているからです。インタフェースの型は、リソース内の複数のフィールド/関数に対してアクセス権を細かく設定することが出来ます。インタフェースの型はインターセクション{}
で囲って作成します(これらはJavaなどで一般的です)。FlowTokenにはVaultの実装があり、そのVaultのインタフェースがFungibleTokenに存在するため、FlowToken.Vault内でアクセス出来る機能を絞る必要がある場合は<&{FungibleToken.Receiver}>
のようになります。しかし、FlowToken.Vaultリソースである事には変わりはありません。
init内
のFlowTokenVault
はこのスマートコントラクトをデプロイしているAdminアカウントのFlow入金先で、createPlayer内
のPlayerFlowTokenVault
はゲームプレイヤーに対する賞金の入金先です。(あくまで入金先であり、FLOWの引き出しはインターセクション型により制限されて出来ません。)
entitlementによるアクセス制限の実装を忘れてました。entitlementについてはDay3を参照して下さい。
Playerリソースをアカウントのストレージに保存する
Player
リソースは以下のコードでアカウントのストレージに保存します:
import "AwesomeCardGame"
transaction(nickname: String) {
prepare(signer: auth(Storage, Capabilities) &Account) {
let FlowTokenReceiver = signer.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
signer.storage.save(<- AwesomeCardGame.createPlayer(nickname: nickname, flow_vault_receiver: FlowTokenReceiver), to: /storage/AwesomeCardGamePlayer)
let cap = signer.capabilities.storage.issue<&AwesomeCardGame.Player>(/storage/AwesomeCardGamePlayer)
signer.capabilities.publish(cap, at: /public/AwesomeCardGamePlayer)
}
execute {
log("success")
}
}
storage.save
でリソースをアカウントストレージに保存し、その後Capability
を作成します。Capability
はリソース情報の取得に便利な機能です。リソース内の情報はリソース自体かCapability
を通じてしか取得できませんが、リソース自体はアカウントのトランザクションでしか取得できないため、Capability
として公開することでjavascriptでどこからでも取得することが出来ます。
HTMLとロジックを書く
メイン画面のロジックを以下のように加筆します。
<script>
import { config, authenticate, unauthenticate, currentUser } from '@onflow/fcl';
import { getBalance, isRegistered } from '../scripts';
import { createPlayer } from '../transactions'
import flowJSON from '../flow.json';
const network = 'emulator';
let walletUser;
let flowBalance;
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) {
flowBalance = await getBalance(user.addr);
hasResource = await isRegistered(user.addr);
}
});
</script>
{#if !walletUser?.addr}
<button onclick={authenticate}>ログイン</button>
{/if}
{#if walletUser?.addr}
FLOW残高: {flowBalance}
<button onclick={unauthenticate}>ログアウト</button>
{#if !hasResource}
Playerリソースが作成されていません。
<button onclick={() => createPlayer('ニックネームAAA')}>新規登録</button>
{/if}
{#if hasResource}
Playerリソース作成済みです。
{/if}
{/if}
isRegistered
はリソースを保持済みか確認するメソッドです。リソースを保持済みなのに同じパスにリソースを保存しようとするとエラーになるため、このメソッドでそのエラーを防ぎます。
src/scripts.js
に以下を追記します。
import { query } from "@onflow/fcl";
export const isRegistered = async function (address) {
const result = await query({
cadence: `
import "AwesomeCardGame"
access(all) fun main(address: Address): &AwesomeCardGame.Player? {
return getAccount(address).capabilities.get<&AwesomeCardGame.Player>(/public/AwesomeCardGamePlayer).borrow()
}
`,
args: (arg, t) => [arg(address, t.Address)],
});
return result;
};
createPlayer
はjsからリソース作成トランザクションを実行するためのメソッドです。
src/transactions.js
を新規作成し以下を入力します。
import { mutate, authz } from "@onflow/fcl";
export const createPlayer = async function (nickname) {
const txId = await mutate({
cadence: `
import "AwesomeCardGame"
import "FlowToken"
import "FungibleToken"
transaction(nickname: String) {
prepare(signer: auth(Storage, Capabilities) &Account) {
let FlowTokenReceiver = signer.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
signer.storage.save(<- AwesomeCardGame.createPlayer(nickname: nickname, flow_vault_receiver: FlowTokenReceiver), to: /storage/AwesomeCardGamePlayer)
let cap = signer.capabilities.storage.issue<&AwesomeCardGame.Player>(/storage/AwesomeCardGamePlayer)
signer.capabilities.publish(cap, at: /public/AwesomeCardGamePlayer)
}
execute {
log("success")
}
}
`,
args: (arg, t) => [arg(nickname, t.String)],
proposer: authz,
payer: authz,
authorizations: [authz],
limit: 999,
});
console.log(txId);
return txId;
};
トランザクションの最後でtxIdをコンソールログに出すようにしていますが、エミュレータを起動していると、そこにトランザクションのエラー情報も出力されます。
スマートコントラクトをデプロイする
エミュレータが起動している状態で、以下を実行します。
flow project deploy
もしデプロイした後に再デプロイしたい場合は、以下のコマンドを実行して一度コントラクトを削除します。
flow accounts remove-contract AwesomeCardGame
リソースを作成してアカウントのストレージに保存する
これでリソース作成/保存のロジックはすべて準備できましたので、http://localhost:5173/ にアクセスします。
新規登録ボタンをクリックします。
トランザクションが完了してisRegistered
メソッドを再び実行すると以下のように表示されます。
リソースを保有しているか確認するメソッドは登録後に数秒単位で実行する必要があります。
これでゲーム賞金の入金先のスマートコントラクトへの保存とPlayer
リソースの作成ができました。
この記事のソースコードはこちらにあります。