Previous << Day6 - プレイヤー登録
Next >> Day8 - デッキ編集
Why Flow
FLOW(または$FLOW)トークンは、Flowネットワークのネイティブ通貨です。開発者およびユーザーは、FLOWを使用してネットワーク上で取引(transact)を行うことができます。開発者は、ピアツーピア決済(他人同士の決済)、サービス料金徴収、または消費者向け特典(rewards)のために、FLOWを直接アプリに統合することができます。
ということでやっていきます、猿でも分かるP2P(ピアツーピア)決済アプリ開発!
💡もし、エミュレータの起動方法やスマートコントラクトのデプロイについて操作に自信がない場合はこちらを参照してください。
現在のゲーム内通貨残高を表示する
AwesomeCardGame.cdc
のPlayer
リソースの定義に以下のようにget_player_score
メソッドを追加します。
:
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
となり呼び出しに失敗します。
そのためエミュレータアカウントがリソースを持たない状態まで戻す必要があり、一度エミュレータを止めて、エミュレータを再起動する必要があります。そうすると、アカウントのリソースも消えてなくなります。
よって、エミュレータアカウントにリソースを保存した状態で、スマートコントラクトのリソース定義を変更した場合の確認手順は
- エミュレータを止める
-
flow emulator -v
コマンドでエミュレータを起動する -
flow dev-wallet
コマンドでDev Walletを起動する -
flow project deploy
コマンドでAwesomeCardGame
スマートコントラクトをデプロイする - 新規登録ボタンを押して
Player
リソースを保存する - これでもうまくいかない時はブラウザ上でDevWalletのログインをし直す(ログイン情報が古い為)
となります。
Day5のFlowToken所持金取得スクリプトを以下のように加筆します。
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
を以下のように修正します。
:
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} <- 修正
:
この状態で画面を表示すると以下のようにゲーム内通貨の残高を表示できます。
リソースのニックネームも一緒に表示する
Day6でPlayer
リソースを新規登録した時にnickname
も登録していたのでそれも一緒に画面に表示します。
:
data.append(vaultRef.balance)
data.append(cap.get_player_score().cyber_energy)
data.append(cap.get_player_score().player_name) <- 追加
return data
}
:
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
も表示することができます。
ゲーム内通貨を購入する
では画面上に表示されているFLOW残高を使ってゲーム内通貨を購入していきます。
なぜFLOWがあるのにゲーム内通貨を購入するかというと、それはFLOWはアカウントのストレージにあるのでユーザーの許可なしに動かせないのに対して、ゲーム内通貨はスマートコントラクト内にあるのでAdmin
リソースが自由に変更できるからです。
なぜ、これが利点かというと、アカウントのストレージにFLOWがあるということは、これを引き出すときには必ずウォレットのポップアップでApproveボタンを押す必要があります。
これが割と面倒くさいのです。ポップアップでApproveするとFLOWを自分のウォレットから引き出すことができるのですが、ゲームは短時間に何回も入金することがあります。その度にポップアップでApproveするのは面倒くさいので、あらかじめゲーム3~4回分ぐらいあらかじめ購入できるように、ゲーム内通貨が登場します。
AwesomeCardGame.cdc
のPlayer
リソースの定義に以下のようにbuy_en
メソッドを追加します。
:
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.cdc
のCyberScoreStruct
に以下のようにset_cyber_energy
Setter関数を追加します。
(access(all)
のStruct(構造体)のパラメータは、Setter関数が無いと、更新しようとする時にエラーになるためです)
:
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で呼ぶ必要がありますので、そのメソッドは以下のようになります。
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
に以下のコードを追加します
import { createPlayer, buyCyberEn } from '../transactions'
:
{#if hasResource}
Playerリソース作成済みです。
<br>
<button onclick={buyCyberEn}>CyberEnergy購入</button>
{/if}
これで準備ができました。画面上でCyberEnergy購入ボタンを押して、Approveを押します。
Flow残高が減ってないけど、ゲーム内通貨は増えてます。これはAwesomeCardGame
スマートコントラクトをデプロイしているのがemulator-account
でありPlayer
リソースをアカウントに保存したのもemulator-account
なので、CyberEnergy購入処理で一度2FlowToken
を引き出したものの、スマートコントラクトの中でFlowTokenVaultに2FlowToken
を預け入れたので、結局同じ額に戻ってしまいました。
そこで一度ログアウトして新しいアカウントを作成してそのアカウントでゲーム内通貨購入を試してみます。
アカウントが作られました。(FlowTokenもたくさん持っています。)
(何故舌を出す...)
Account Aでログインして、新規登録します。(nickname
はコード直打ちでしたが少し変更しました)
これで準備ができました。画面上でCyberEnergy購入ボタンを押して、Approveを押します。
2FLOWを支払ってゲーム内通貨100(CyberEnergy)を購入しました。
ここで、元のemulator-account
のアカウントに戻るとどうなるでしょう?一度ログアウトしてemulator-account
のアカウントでログインし直してみましょう。
前は999999999.995
だったのが1000000001.994
に増えてます。つまり、1.999FLOW増えたことになっています。(0.001は先ほどアカウントを作成した時のトランザクション費用です)
エミュレータが計算する0.001FLOWなどのトランザクション費用は考えられる限り最も高い費用が設定されています。実際は0.000003FLOW未満になると考えてよく、非常に少額になります。(Flowの転送だけなら$0.00000185FLOWだけです。)
これでピアツーピア決済アプリの中枢部分が実装できたことになります。
Mainnetにデプロイしたらもうあなたは世界レベルの事業者です。MainnetデプロイはCLIで鍵作成して、CLIでデプロイします。
この記事のソースコードはこちらにあります。