画面上部にゲームのユーザ情報を出します。
ゲーム画面上部に以下のように記述することで
{#if havingResource}
<p class="paragraph sticky">
<span class="allura">Hello,</span>{havingResource?.nickname}
<span class="total-prize">
( Total prize won: ₣
<span class="cinzel balance">
{currentSituation?.prizeWinners[havingResource?.gamerId] ?? 0}
</span>
)</span>
</p>
{/if}
以下のようにユーザ情報を出しました。
ゲームにコインを投入するロジック
ゲームをスタートするメソッドをスマートコントラクトに追加します。
resource Gamerの中に以下を追記します。
access(all) fun insert_coin(payment: @FlowToken.Vault) {
pre {
payment.balance == 1.1: "payment is not 1.1FLOW coin."
}
if let challenged = TestnetTest2.gamersInfo.tryingPrize[self.gamerId] {
if (challenged > 0) {
panic("You are now on playing the game. Payment is not accepted.")
}
}
TestnetTest2.gamersInfo.setTryingPrize(gamerId: self.gamerId)
TestnetTest2.FlowTokenVault.borrow()!.deposit(from: <- payment)
}
一番下のコードでは前回の記事で追加した金庫(vault)に、ユーザに支払ってもらったゲーム料金を格納しています。スマートコントラクトの中ではこの1行だけでお金を集金できるんですね。
これを flow project deploy --update --network testnet
コマンドでデプロイします。
(テストネットであればhttps://flow-view-source.comで自分がデプロイしたコードを見ることができます)
/flow_blockchain/transactions.js
に以下を追記します。
export const insertCoin = async function () {
const txId = await mutate({
cadence: `
import "TestnetTest2"
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: 1.1) as! @FlowToken.Vault
let gamer = signer.storage.borrow<&TestnetTest2.Gamer>(from: /storage/TestnetTest2Gamer)
?? panic("Could not borrow reference to the Owner's Gamer Resource.")
gamer.insert_coin(payment: <- payment)
}
execute {
log("success")
}
}
`,
args: (arg, t) => [],
proposer: authz,
payer: authz,
authorizations: [authz],
limit: 999,
});
console.log(txId);
return txId;
};
これを「Insert ₣1.1 coin」ボタンを押した時に呼び出します。
その前に、ゲームをプレイしているユーザのTestnet用$FLOW(Flow blockchainのネイティブ暗号通貨です)を用意します。https://faucet.flow.com/fund-accountにアクセスし、アドレスを入力することで、Testnet用の100,000FLOWコインをそのアドレスに付与することができます。
アドレスは {gameUser?.addr}
とどっかに貼り付けて画面からコピーしましょう(あまり画面にアドレスを出してもプレイヤーは喜ぶと思わないので私は画面にアドレスを出さない予定です)。
ゲームスタートボタンを押した時のロジックを用意します。
import { insertCoin } from '../../../flow_blockchain/transactions'
import Dialog from './../Dialog.svelte';
async function startBtnClicked() {
if (!btnClicked) {
btnClicked = true
modal.showModal()
let txId = await insertCoin()
tx(txId).subscribe((res) => {
console.log('tx status:', res);
});
}
}
<Dialog bind:dialog={modal}>
<div>You need to accept the transaction on the wallet. Game fee is only ₣1.1!</div>
</Dialog>
ボタンを押します。
ここはScript ℹ️
の右矢印を押すと本当に1.1FLOW引かれるのかどうかコードで分かります。ですが、エンジニアではない普通の人には到底分かりません。有象無象のスマートコントラクトが世の中に溢れているので、ウォレットに大金を持っていたらそれを引き出すコードかもしれないのです。対策としてはInteraction Templatesにより機関側で見破ることで、ユーザが安心してゲームをできるようになることは間違い無いでしょう。
Approveを押します。(この時エラーになったらtxId
をFlowScan等に貼り付けてエラー内容を確認することをお勧めします)
所持金をリアルタイムに取得する
ゲーム代金を払ったので、残高の推移をリアルタイムに見せる必要があります。
1.5秒おきに実行するsetInterval関数にgetBalance
メソッドを追加します。
setInterval(async () => {
if (gameUser?.addr) {
flowBalance = await getBalance(gameUser.addr);
havingResource = await isRegistered(gameUser.addr);
}
currentSituation = await getGamersInfo();
}, 1500);
先ほどのApproveを押した時点でtx(txId).subscribe((res) => {
のres
にerrorMessage
もなく、statusString
がSEALED
になると支払いが完了したことになります。その時点でゲームをスタートさせます。画面にはSEALED
になったと同時に以下のように表示しました。
ゲームが勝った時の処理をGraphQLサーバーで処理する
次はゲームが勝った時と負けた時の処理をGraphQLサーバーで実行します。
前々回の記事で以下のように勝った時と負けた時にGraphQL呼び出しを以下のように実装していました。フロントエンドはここを少し修正するだけです。
query
のtype
を’shooting_game_outcome’
にします。message
にはonGameLose
の時に’false’
を、onGameWon
の時には’true’
をセットします。playerId
にはgamerId
をセットします。(messageの型がStringなので。)
次にGraphQLサーバー側です。
GraphQLサーバーからトランザクションを行うということは秘密鍵を用意してトランザクションを行うことになります。ということは、スマートコントラクトをデプロイしたアカウントによるトランザクションになりますので、Admin Resource
が必要です。(init
内で宣言することでinit
以外では生成することができないように実装されているリソースのことを俗にAdmin Resource
と言います)
スマートコントラクトにAdminResourceを書いていきます。
1.init
内に以下を追記します
self.account.storage.save( <- create Admin(), to: /storage/TestnetTest2Admin)
2.init
の枠の上に以下のコードを書きます
/*
** [Resource] Admin (Game Server Processing)
*/
access(all) resource Admin {
/*
** Save the Gamer's shooting game outcome
*/
access(all) fun shootingGameOutcome(gamerId: UInt, outcome: Bool) {
let prizePaid = TestnetTest2.gamersInfo.setCurrentPrize(added: 1, gamerId: gamerId, paid: outcome)
if (prizePaid > 0) {
// Pay the prize.
let reward <- TestnetTest2.account.storage.borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(from: /storage/flowTokenVault)!.withdraw(amount: UFix64.fromString(prizePaid.toString().concat(".0"))!) as! @FlowToken.Vault
TestnetTest2.GamerFlowTokenVault[gamerId]!.borrow()!.deposit(from: <- reward)
}
}
}
型が厳格に判定処理される(UInt
からUFix
に直接変換できない)ので、UInt
整数から文字列に変換して”.0"
をconcat
してUFix
に変換しています。(フィールドをUFix
で修正するとまた大変なので)
これをデプロイします。
flow project deploy --update --network testnet
を実行します。
デプロイできました。このようにコードの修正であれば、スマートコントラクトはコードの上書きを受け付けてくれます。(フィールドを変更すると駄目です)
GraphQLサーバのリゾルバを作る
まずは、AWS Lambda Functionのコンソールを開きます。
前々回の記事で作成したoragaESports関数を選択し、Topの画面からDownloadを押してzip形式でダウンロードします。
プロジェクトルートに/aws_lambda/src
というフォルダを新規作成し、そこにダウンロードして解凍したindex.mjs
ファイルを置きます。(aws_lambda
フォルダの名称は何でも構いません)
こんな感じです。(flow_blockchain
フォルダ赤い(エラーがある)ですが、スマートコントラクトのデプロイはできているのでCadence VSCode Extentionの私の使い方がちょっと間違っているのかもしれません)
このindex.mjs
に、トランザクションを実行するコードを書くのですが、@onflow/fcl
などのパッケージをインストールする必要があります。
ですので、/aws_lambda/src
内にターミナルで移動し
npm init
コマンドを実行してpackage.json
ファイルを作成します。コマンド実行時に聞かれる選択肢は全てデフォルトでいいですが、entry pointだけはindex.jsではなくindex.mjs
にしました。
そしてターミナルで次のコマンドを実行します。
npm install @onflow/fcl elliptic sha3
ちょっとこれでいいか怖いので一旦srcフォルダをzip化して、Lambda Functionにアップロードしてリゾルバとして正常に機能するか見てみます。(一応zipしたファイルと分かるようにindex.mjs
から返す値を少し変えてます)
src
フォルダをzip化するコマンド(src
内から実行):
zip -r ../src.zip .
これによって作成されたsrc.zipをAWS Lambda Functionにアップロードします。
ゲームを実行し、勝つか負けるとGraphQLクエリとそれに対するSubscriptionが呼ばれるのでそれを待ちます。
おっ、ちゃんと動いてそうですね。AWS Lambdaにアップロードしたzipファイルが正常にGraphQLリゾルバとして認識され、Subscriptionが今修正したreturn値で返されています。
これが便利で、いいんですよ。バックエンドはLambda Functionにアップロードするだけ、フロントエンドはS3にアップロードするだけ、それだけでプロジェクト全体の更新ができるのですから開発速度は爆上がりです。(AWS Lambda Functionコンソールからアップロードする以外の形式は、AWSのリソース(=Cloud Formation)を通じてGraphQLサーバの実装を更新する必要があるため、すごく時間がかかりストレスが溜まります。AWSが仕様を変更しないことを望みます..)
では/aws_lambda/src/index.mjs
にGraphQLサーバの処理としてのブロックチェーンに対するトランザクション処理を書いていきます。
バックエンドからトランザクションを実行する処理
index.mjs
の書き出しは以下のような感じです。
const fs = require("node:fs");
const { config, sansPrefix, withPrefix, mutate , tx } = require("@onflow/fcl");
const { SHA3 } = require("sha3");
export const handler = async (event) => {
console.log('Event', JSON.stringify(event, 3));
const input = event.variables?.input || {};
let gamerId = input.playerId ? parseInt(input.playerId) : 0;
let message = input.message ? JSON.parse(input.message) : {};
let transaction = '';
if (input.type === 'shooting_game_outcome') {
transaction = `
import TestnetTest2 from 0x975b04756864e9ea
transaction(gamerId: UInt, outcome: Bool) {
prepare(signer: auth(BorrowValue) &Account) {
let admin = signer.storage.borrow<&TestnetTest2.Admin>(from: /storage/TestnetTest2Admin)
?? panic("Could not borrow reference to the Administrator Resource.")
admin.shootingGameOutcome(gamerId: gamerId, outcome: outcome)
}
execute {
log("success")
}
}
`;
}
フロントエンドから来た情報であるevent.variables
からplayerIdとmessageとtypeを取り出し、トランザクションコードをtypeによって決定します。
import
するスマートコントラクトのアドレスはflow.json
に書いてあるものと同じです。
次にfclの設定を行います。
config({
"flow.network": "testnet",
"accessNode.api": "https://rest-testnet.onflow.org",
});
バックエンドから承認処理も自分たちで行うので設定する内容はこれぐらいになります。
次の関数内で秘密鍵を参照するので、testnet-account.pkey
もsrc
フォルダにコピーしておきます。(AWSのサービスを使って秘密鍵を保存しなくていいのでコストが浮きますな。)
次に承認を行う関数を作ります。
let txId;
try {
var IT_KEY_ID = 0;
if (fs.existsSync('/tmp/sequence.txt')) {
IT_KEY_ID = parseInt(fs.readFileSync('/tmp/sequence.txt', { encoding: 'utf8' }));
} else {
IT_KEY_ID = 10;
}
IT_KEY_ID = !IT_KEY_ID || IT_KEY_ID >= 10 ? 0 : IT_KEY_ID + 1;
fs.writeFileSync('/tmp/sequence.txt', IT_KEY_ID.toString());
console.log('IT_KEY_ID', IT_KEY_ID);
const EC = require('elliptic').ec;
const ec = new EC('p256');
// CHANGE THESE THINGS FOR YOU
const PRIVATE_KEY = fs.readFileSync('testnet-account.pkey', { encoding: 'utf8' });
const ADDRESS = '0x975b04756864e9ea';
const KEY_ID = 0;
const hash = (message) => {
const sha = new SHA3(256);
sha.update(Buffer.from(message, 'hex'));
return sha.digest();
};
const sign = (message) => {
const key = ec.keyFromPrivate(Buffer.from(PRIVATE_KEY, 'hex'));
const sig = key.sign(hash(message)); // hashMsgHex -> hash
const n = 32;
const r = sig.r.toArrayLike(Buffer, 'be', n);
const s = sig.s.toArrayLike(Buffer, 'be', n);
return Buffer.concat([r, s]).toString('hex');
};
async function authFunction(account) {
return {
...account,
tempId: `${ADDRESS}-${KEY_ID}`,
addr: sansPrefix(ADDRESS),
keyId: Number(KEY_ID),
signingFunction: async (signable) => {
return {
addr: withPrefix(ADDRESS),
keyId: Number(KEY_ID),
signature: sign(signable.message)
};
}
};
}
async function authFunctionForProposer(account) {
return {
...account,
tempId: `${ADDRESS}-${IT_KEY_ID}`,
addr: sansPrefix(ADDRESS),
keyId: Number(IT_KEY_ID),
signingFunction: async (signable) => {
return {
addr: withPrefix(ADDRESS),
keyId: Number(IT_KEY_ID),
signature: sign(signable.message)
};
}
};
}
秘密鍵はSecretsManagerなどで管理した方がいいと思います。Lambda Function内でSecretsManagerなどAWSサービスを使用するには準備が必要なので、今回はtestnet-account.pkey
を使用する形にしました。testnet-account.pkey
ファイルもsrc
内にコピーしておきます。
承認関数3つのうちProposer
は鍵をサイクルさせることが仕様になっています(後述のJacob君の動画で詳述されます)。Proposer
(提案者)承認関数の中で同じキーIDの鍵を連続で使うとエラーになります。そのため我流ですがkeyId
をIT_KEY_ID
でサイクルさせるコードを書いてます。テストネットなので、とりあえず10個のキーIDでサイクルさせます。
ここで鍵が10個必要ですので、作ります(キーIDが10あれば良いので中身は全く同じ鍵でも大丈夫です)。
FlowScanでテストネットで作成したアカウントのkeys
を見てみます。
https://testnet.flowscan.io/account/975b04756864e9ea?tab=keys
最初はkeyId
が0のものだけ存在します。
ここにkeyId
が1~9のものを追加します。keyの作り方はJacob君の動画を参考にしつつ、こちらのCadenceV1.0対応された方法とを見比べつつ実装します。(ややこしい..) コードは以下になります。
transaction(publicKey: String, numOfKeyToAdd: Int) {
prepare(signer: auth(AddKey) &Account) {
let key = PublicKey(
publicKey: publicKey.decodeHex(),
signatureAlgorithm: SignatureAlgorithm.ECDSA_secp256k1
)
var counter = 0
while counter < numOfKeyToAdd {
counter = counter + 1
signer.keys.add(
publicKey: key,
hashAlgorithm: HashAlgorithm.SHA3_256,
weight: 0.0
)
}
}
}
Proposer
なのでweight
は0
でも問題ありません(Jacob君の動画より)。
/flow_blockchain/cadence/transactions/
の下にaddKey.cdc
というファイルを作成、上記内容をペーストします。
そしてそれをFlow CLIで実行します。(Jacob君の動画の9分目あたりからとSend a Transactionのページを参考に)
flow transactions send ./cadence/transactions/addKey.cdc b37154d511d5ade734a36c29d85f013be7afc3a935c91bab84c9c69bb9ad7f9e984e8ed1f3e39d5c670d6199d242d439e12f8062f368483e9b0b3057fda94a51 9 --network=testnet --signer=testnet-account
ここではkeyId=0の公開鍵から1~9を作っていますが、本当は別々の公開鍵秘密鍵のペアから作らないとあまり意味がありません。ですが、サイクル処理に対応しないと連続でトランザクションをエラーなく実行できないので、とりあえず同一の公開鍵秘密鍵ペアで作成しています。(この方法は鍵サイクルという点では意味がほぼ無しです)
/flow_blockchain
ディレクトリ下で実行します。
なんかこんなアウトプットがされたら成功しています。
FlowScanで確認します。
https://testnet.flowscan.io/account/975b04756864e9ea?tab=keys
ちゃんと鍵が10個できてますね。(keyId=0の公開鍵と同じ公開鍵をkeyId=1~9に設定しました。鍵サイクルの観点からすると全く意味がありませんね。)
やっとトランザクション実行部分です!
ブロックチェーンエンジニアって知識の蓄積が大事なんだな、と思いました。
if (input.type === "shooting_game_outcome") {
const outcome = message == "true" || message == true;
txId = await mutate({
cadence: transaction,
args: (arg, t) => [arg(gamerId, t.UInt), arg(outcome, t.Bool)],
proposer: authFunctionForProposer,
payer: authFunction,
authorizations: [authFunction],
limit: 999,
});
console.log(`txId: ${txId}`);
message = `Tx[shooting_game_outcome] is On Going.`;
tx(txId).subscribe((res) => {
console.log(res);
});
}
return {
id: new Date().getTime(),
type: input.type || "",
message: `${input.message} , txId: ${txId}`,
playerId: gamerId,
createdAt: new Date(),
updatedAt: new Date(),
};
} catch (error) {
return {
id: new Date().getTime(),
type: "E:" + input.type,
message: error.toString(),
playerId: gamerId,
createdAt: new Date(),
updatedAt: new Date(),
};
}
};
最後をこのように締めくくってsrc
フォルダをzipします。(src
内から実行):
zip -r ../src.zip .
AWS Lambdaコンソールからアップロードしてゲームをします。結果は…
const EC = require("elliptic").ec;
などの部分が駄目だ、と言っていますので、requireをimportに直して再トライ
"SyntaxError: Named export 'ec' not found. The requested module 'elliptic' is
a CommonJS module, which may not support all module.exports as named exports.\n
CommonJS modules can always be imported via the default export, for example
using:\n\n
import pkg from 'elliptic';\n
const { ec: EC } = pkg;\n"
やっぱりダメか。ellipticのEC contextがimport出来なかったのでrequireにしていました。
このelliptic 開発者、Fedor Indutnyって調べるとすごい人だった。Node.jsのコア開発者で突然Nodeのバージョンが勢いよく上がるきっかけになった人。(NIST) P256とsecp256k1カーブについて端的によくまとまっているサイトはこちら 。ビットコインなどで採用されているsecp256k1はKoblits Curveでありその名前を持つNeal Koblitzはelliptic curveの共同発明者である。P256(secp256r1)は業界標準
対応策は.. CloudWatchログ見るとちゃんと紹介してました。(ブラウザのconsole.logは見切れていたのですが..)
import pkg from "elliptic";
const { ec: EC } = pkg;
に直して再トライ。
"RequestId: 3af050bd-5bea-4e58-b31d-d0c36c0d50b8 Error: Task timed out after 3.00 seconds"
requireは解決したようですね。次は3秒を超えた、というエラーでした。
AWS Lambda FunctionのConfigurationからTimeoutを10秒にしておきます。
再トライ
message: "undefined, txId: undefined"
AWSがGraphQL Direct Lambda Resolverの仕様を変えていたようです。
event.variables?.input
ではなくevent.input
から取得するように直して再トライ。(最近CloudWatchLogの反応が早くなりました?GraphQLサーバーの動きをすぐに確認できるのですごく助かります!)
再トライ(Trouble Shooting)
[Error Code: 1008] error caused by: 1 error occurred:
* transaction verification failed: [Error Code: 1006] invalid proposal key:
public key 9 on account 975b04756864e9ea does not have a valid signature:
[Error Code: 1008] invalid payload signature: public key 9 on account
975b04756864e9ea does not have a valid signature: signature is not valid
今度はトランザクションエラーでした。keyが有効でない、というエラーですね。これで一番考えられるのは秘密鍵を正しく取得できていないことが考えられます。key周りのエラーは一般論的なエラーになるので秘密鍵を渡せてなかった時もこんなエラーになったと思います。
(追記: 秘密鍵の先頭に0xがついていてもこのエラーになります。flowのコマンドでアカウントを作成すると先頭に0xがついた状態でpkeyファイルに保存されますので注意が必要です。)
念の為、私が書いたauthorization関数とsign関数が合っているのか調べました(長いので折り畳んでいます)
sign
関数はkeyFromPrivate
でQiitaを検索するとここにヒットしました。
import * as fcl from '@onflow/fcl';
import { ec as EC } from 'elliptic';
import { SHA3 } from 'sha3';
const ec: EC = new EC('p256');
const produceSignature = (privateKey, msg) => {
const key = ec.keyFromPrivate(Buffer.from(privateKey, 'hex'));
const sig = key.sign(this.hashMsg(msg));
const n = 32;
const r = sig.r.toArrayLike(Buffer, 'be', n);
const s = sig.s.toArrayLike(Buffer, 'be', n);
return Buffer.concat([r, s]).toString('hex');
};
sign
関数は合ってそうですね。同じ書き方です。
authorization
関数は先ほどの記事の下のDetailed Explanationのリンクの先に、公式が紹介している関数がありました。
const authorizationFunction = async (account) => {
// authorization function need to return an account
return {
...account, // bunch of defaults in here, we want to overload some of them though
tempId: `${ADDRESS}-${KEY_ID}`, // tempIds are more of an advanced topic, for 99% of the times where you know the address and keyId you will want it to be a unique string per that address and keyId
addr: ADDRESS, // the address of the signatory
keyId: Number(KEY_ID), // this is the keyId for the accounts registered key that will be used to sign, make extra sure this is a number and not a string
signingFunction: async signable => {
// Singing functions are passed a signable and need to return a composite signature
// signable.message is a hex string of what needs to be signed.
return {
addr: ADDRESS, // needs to be the same as the account.addr
keyId: Number(KEY_ID), // needs to be the same as account.keyId, once again make sure its a number and not a string
signature: sign(signable.message), // this needs to be a hex string of the signature, where signable.message is the hex value that needs to be signed
}
}
}
}
こちらも合ってそうですね。英語が、Flowって最先端のブロックチェーンだから学術論文のような英語で書かれてあるんですよね。DeepLで翻訳してみます。
認証関数の作成方法
幸いなことに、認証関数の作成は比較的簡単です。
認証関数は、最低限以下の2つの機能を備える必要があります。
誰が署名するか -- 署名するアカウントと、そのアカウントが使用する鍵のkeyIdを特定する
どのように署名するか -- 最初のステップで指定されたアカウントと鍵から署名を取得する方法を知る
認証関数には「アカウント」という概念があります。アカウントは取引の署名者候補を表し、署名する主体と署名方法の両方を包含します。認証機能には空のアカウントが渡され、アカウントを返す必要があります。認証機能を作成する際の主な作業は、このアカウントに情報を入力し、署名したいアカウントが署名できるようにすることです。
例えば、事前にアカウントとキーIDが分かっている場合、署名可能な関数があると仮定しましょう。
const ADDRESS = "0xba1132bc08f82fe2"
const KEY_ID = 1 // このテストネット上のアカウントには3つのキーがあります。そのうち、インデックスが1でウェイトが1000のキーを取得したいと考えています。
const sign = msg => { /* ... 指定されたメッセージに対して、上記の鍵の署名を生成します。 ... */ }
私達の認証関数は、主に値を埋める作業になります:
例2:
const authorizationFunction = async (account) => {
// 認証関数はアカウントを返す必要があります
return {
...account, // ここにデフォルト値がいくつかありますが、一部を上書きしたい場合があります
tempId: `${ADDRESS}-${KEY_ID}`, // tempIdはより高度なトピックです。アドレスとKEY_IDが既知の場合、そのアドレスとKEY_IDごとに一意の文字列にしたい場合がほとんどです
addr: ADDRESS, // 署名者のアドレス
keyId: Number(KEY_ID), // これはアカウントに登録されたキーのキーIDで、署名に使用されます。これが数値であり文字列ではないことを確認してください
signingFunction: async signable => {
// 署名関数はsignableを受け取り、複合署名返す必要があります
// signable.messageは署名対象のヘックス文字列です。
return {
addr: ADDRESS, // account.addrと一致する必要があります
keyId: Number(KEY_ID), // アカウントのkeyIdと一致する必要があります。再度、数値であり文字列ではないことを確認してください
signature: sign(signable.message), // これは署名の16進数文字列で、signable.messageは署名対象の16進数値です
}
}
}
}
このように認証関数は合ってそうだったので、一番理由として考えられる秘密鍵だと思うのでconsole.logで変数の中身を出力してあとでCloudWatchLogで見てみます。
const PRIVATE_KEY = fs.readFileSync("testnet-account.pkey", "utf8");
console.log("PRIVATE_KEY:", PRIVATE_KEY);
ちゃんと取得できていました。結論としては、
const ec = new EC(“secp256k1”)
の部分がconst ec = new EC(“P256”)
になっていることが原因でした。(本当にkeyのcurveが原因でしたね..)
最後のTrouble Shootingはこちらです。
panic("Could not borrow reference to the Administrator Resource.")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
これはですね.. スマートコントラクトをupdate
したことが原因です。init
関数はデプロイした時にしか呼ばれません。その為init
関数でAdmin
関数をsave
したのが無かったことになっています。
以上、これらが大体GraphQL Direct Lambda Resolverの実装をした時に対応が必要な箇所達です。最後は以下のようにコントラクト名の一括変換を行なってデプロイし直せば、このトランザクションは成功します。
flow project deploy --network testnet
最終的に完成したGraphQL Direct Lambda Resolverのコードはこちらです
とりあえず、先ほど私が実装したスマートコントラクトの実装は合ってそうです。
(ゲームに負けたことで) Prizeが1から2に増えてますね。
ここまでお読みいただいたら分かると思いますが、Cadenceの実装やBuildは非常に開発者フレンドリーでほとんど一発で成功します。今まででつまづいたのはインフラ処理ばかりだったと思います。
開発が佳境に入ったら
ここまで来ると、開発が佳境に入ってきます。
スマートコントラクトの修正も結構入ってきます。フィールドを追加すると名前をカウントアップ(TestNetTest3 -> TestNetTest4)させて以下の処理を継続的に行うことになります。
# /flow_blockchain の下で
flow project deploy --network testnet
# /aws_lambda/src の下で
zip -r ../src.zip .
# これをAWS LambdaにUpload
# ローカル開発であれば以上で十分ですが、CloudFront経由でテストしたい場合はプロジェクトルートで
npm run build
# 生成される /dist 以下のファイルを全てS3にUpload
GraphQLのReturn値を使用したフロントエンドの実装
まず、一番簡単な負けた時の実装からします。
リアルタイム同時接続のGraphQLを利用して、画面を見ている全員に賞金が上がったよ、とモーダルで通知します。
前々回の記事で実装したGraphQLのSubscription処理:
(赤いのはtypescriptじゃないかららしい。個人開発なので細かいことは気にせず..)
ここをtxでSEALEDになるまでtransactionをsubscribeします。
let notificationModal
let notificationMessage
const createSub = client
.graphql({ query: subscriptions.onCreateGameServerProcess })
.subscribe({
next: ({ data }) => {
console.log(data.onCreateGameServerProcess)
if (data.onCreateGameServerProcess?.type == 'shooting_game_outcome') {
const res = data.onCreateGameServerProcess?.message.split(' , txId: ')
const outcome = res[0]
const txId = res[1]
if (outcome == 'false') {
notificationModal.showModal()
notificationMessage = `Prize money rises! ₣ ${currentPrize} ==> ${currentPrize + 1}`
tx(txId).subscribe((res) => {
if (!res.errorMessage && res.statusString == 'SEALED') {
notificationModal.close()
}
});
}
}
},
error: (error) => console.warn(error)
});
というようにすることで、トランザクションが行われた後にダイアログで画面を見ている人全員に賞金額が上がったことを伝えます。そしてSEALEDになったらダイアログを閉じます。
tx.subscribe
のstatus
が返るのは少し遅いのでSEALED
になる頃にはcurrentSituationで取得したprizeはすでに6に更新されていました。
ここでGraphQLの大人数リアルタイム同時接続を確認したいのでCloudFront(S3)にアップロードします。
おぉ、すごいですね。こんなことができるGraphQLを個人で簡単にAWS Lambdaにアップロードするだけでできるなんて。Twitterみたいな動きをみんなに自慢できますね。
(一番右の機種はOSバージョンの問題で下にダイアログが出ています)
ここで前回の記事で上げたこのゲームの機能要件を見てみます。
- お金を入金できる。ゲームを開始できる。
- ゲームに勝利した時に賞金を受け取れる
- ゲームに負けた場合は賞金額に1FLOWを上乗せする
- FLOWを持っていない人のために1回だけ無料プレイできるようにTip jar機能を用意する(Tip jarから1FLOWを取り出してそれでゲームする)
- Tip jarに寄付(1FLOW)できる
- 勝敗の記録をブロックチェーンに残せる
1, 3, 4, 5, 6はできました。(4と5は記載していませんが2日ほどで作成しました)残りは2だけです。
4と5の開発まで来るとスマートコントラクトは200行を超えます。この辺りで大体自分が書いたスマートコントラクトのバグと戦う時期になります。
よくある、と思うのがelse文を書き忘れること。
access(all) fun useTipJarForFreePlay(gamerId: UInt) {
if let freePlayed = TestnetTest5.gamersInfo.freePlayCount[gamerId] {
:
}
}
こういうふうにfreePlayCount
というオブジェクトに何回無料プレイしたか保存し、その中の値から判断して、無料プレイを実行する処理を書くとします。
このif let
文の中の処理は、チェック処理など含めると結構ボリュームがあるので、これで出来たと思いがちなんですね。でも実際は一度も無料プレイしていない人はこのif let
文には入りません。else
文を書かないと1行も処理されずにこのメソッド処理がSEALEDされてしまうのです。
何で何の処理もしてないのにSEALED
なんだよ、と憤慨してCadenceのバグを疑う前に自分のコードをちゃんと確認する作業、というのがブロックチェーンエンジニアには発生します。
この書き方は面倒くさい(コード数も増える)ので
let freePlayed = TestnetTest5.gamersInfo.freePlayCount[gamerId]!
みたいな書き方はできないものか、とDiscordで聞いたことがあります。
そしたらコア開発者でリードエンジニアのJoshua Hannanに「Yeah, 毎回if let
文を書いてね。」と言われちゃいました。スマートコントラクトで値があるかないかは、リソース(資産)があるかないかなので、クリティカル(資産紛失)になり得るので、面倒くさくてもif let
で、ある時とない時の両方のロジックを、コード数が倍になっても書く必要があるのです。
最後のロジック
では最後のゲームに勝利した時に賞金を受け取れるロジックを仕上げます。
ゲームに負けた時と勝った時のスマートコントラクトのメソッドは、以下のようなSetter関数を通っていました(前回の記事の最後の方で書きました)。
access(contract) fun setCurrentPrize(added: UInt, gamerId: UInt, paid: Bool): UInt {
if (paid == false) {
self.currentPrize = self.currentPrize + added
self.unsetTryingPrize(gamerId: gamerId)
return 0
} else {
if let paidPrize = self.tryingPrize[gamerId] {
if (self.currentPrize - paidPrize > 0) {
self.currentPrize = self.currentPrize - paidPrize
self.unsetTryingPrize(gamerId: gamerId)
if let prizeHistory = self.prizeWinners[gamerId] {
self.setPrizeWinners(gamerId: gamerId, prize: paidPrize + prizeHistory.prize, nickname: nil)
} else {
self.setPrizeWinners(gamerId: gamerId, prize: paidPrize, nickname: nil)
}
return paidPrize + added
}
}
panic("Error.")
}
}
負けた時は上部で、勝った時は下を通るのですが、勝った時にpanic(“Error”)
が呼ばれていることがFlowScanでは確認できました(トランザクションが失敗していた)。
ではなぜpanicの行にロジックが到達したのかを調べないといけないのですが、エミュレータと違ってテストネットだとFlowScanで見れるエラー内容だけが頼りなので、私は以下のようにして調査しました。
access(contract) fun setCurrentPrize(added: UInt, gamerId: UInt, paid: Bool): UInt {
if (paid == false) {
self.currentPrize = self.currentPrize + added
self.unsetTryingPrize(gamerId: gamerId)
return 0
} else {
if let paidPrize = self.tryingPrize[gamerId] {
if (self.currentPrize - paidPrize > 0) {
self.currentPrize = self.currentPrize - paidPrize
self.unsetTryingPrize(gamerId: gamerId)
if let prizeHistory = self.prizeWinners[gamerId] {
self.setPrizeWinners(gamerId: gamerId, prize: paidPrize + prizeHistory.prize, nickname: nil)
} else {
self.setPrizeWinners(gamerId: gamerId, prize: paidPrize, nickname: nil)
}
return paidPrize + added
}
panic("Error2. Oops, something is not good.".concat(paidPrize.toString()).concat(self.currentPrize.toString()))
}
panic("Error. Oops, something is not good.".concat(self.currentPrize.toString()))
}
}
panicのエラー内容に変数の値を吐き出させました。
FlowScanで確認した結果はこちら:
FlowScanはちゃんとpanicの発生行数も教えてくれるのですが、変数の値を確認しないと原因はなかなか分からないですよね。
で原因はわかりました。if (self.currentPrize — paidPrize > 0) {
の部分を
if (self.currentPrize — paidPrize >= 0) {
にしていなかったからpanic
の行まで行ってしまったんですね。
というデバッグ方法のご紹介でした。
勝った時のロジックは先ほどの修正でちゃんと動くようになりました(お金が実際に動く(ウォレットに振り込まれる)処理がこんなにすんなり実装できるのか、とあっけなく動作したらすごく無性に嬉しくなります。ぜひ皆さんも実装して体験してみてください)。
ゲームをしている間に他のプレイヤーが賞金を取った時と他のプレイヤーが賞金を増やした時の対応
他のプレイヤーが賞金を増やした時は、その後でプレイする人がその賞金を獲得するので、考える必要はなかったです。
問題はゲームをしている間に他のプレイヤーが賞金を取った時ですね。
これは前々回の記事で書いたように「おっと誰かが賞金をかっさらったようだ。あなたの賞金は₣1にリセットされてしまったよ。」と伝えてあげなければいけません。
ゲームをプレイ中に他のプレイヤーが賞金をかっさらったら、以下のように表示することにします。
これはゲームに負けた時と同じようにGraphQLのSubscription内で実装します(他の人が勝った時もこのQueryのSubscriptionが呼ばれるからです)。
Subscriptionで受け取る値にはPlayerIDがあるので、それを比較して他人と自分の両方のパターンに分け、ゲーム中ならば邪魔にならないように上記画像のような画面で、ゲーム中ではないならダイアログで出して数秒後に閉じる実装にします。
こんな感じにしました。
これで実装は以上です!開発期間は(フルタイムで仕事しながらで)3週間でした。こんな短期間で開発できて、世界中からゲームをしてもらってお金を稼げるなんてすごい副業じゃないですか?Cadenceなら実装がこんなに簡単なんですよ!
ぜひ皆さんもゲームを作ってFlow blockchain eSportsを盛り上げましょう。盛り上がれば盛り上がるほどあなたの作ったゲームが世界中でバズってお金を稼げるようになるはずです。
Mainnetにリリース
リリースしちゃいましょう。どうせ、告知しなければ誰も怪しんでゲームしませんからさっさとスマートコントラクトをMainnet
にあげてしまいます。
私がMainnet
にリリースするのはCadenceのVersionが1.0に変わる前以来ですから、私もうまくいくかドキドキです。
- スマートコントラクトをコピーし、メインネット用とテストネット用(
OragaESportsForTestnet.cdc
)に分ける -
flow.json
ファイルの上部のcontracts
とdeployments
を以下のようにする
"contracts": {
"TestnetTest5": {
"source": "cadence/contracts/OragaESportsForTestnet.cdc",
"aliases": {
"testing": "0000000000000007"
}
},
"OragaESports": {
"source": "cadence/contracts/OragaESports.cdc",
"aliases": {
"testing": "0000000000000007"
}
}
},
"deployments": {
"emulator": {
"emulator-account": ["TestnetTest5"]
},
"testnet": {
"testnet-account": ["TestnetTest5"]
},
"mainnet": {
"mainnet-account": ["OragaESports"]
}
},
3. /aws_lambda
フォルダの下にtestnet
フォルダとmainnet
フォルダを作成し、その下に/aws_lambda/src
フォルダをそれぞれ配置する
4. /flow_blockchain
フォルダの下にtestnet
フォルダとmainnet
フォルダを作成し、その下にscripts.js
とtransactions.js
ファイルをそれぞれ配置する。
5. /aws_lambda/mainnet
ディレクトリと/flow_blockchain/mainnet
ディレクトリ配下のファイル内とOragaESports.cdc
ファイル内のコントラクト名をOragaESports
に一括変換、testnet-account.pkey
はmainnet-account.pkey
にリネームする。
6. mainnet
用の公開鍵秘密鍵を生成する。
flow keys generate --sig-algo "ECDSA_secp256k1"
mainnet-account.pkey
に上記コマンドで生成されたPrivate Key
をペーストする。mainnet-account.pkey
を.gitignore
に追加する。git status
を実行してmainnet-account.pkey
が出てこないことを確認する。(秘密鍵のファイル管理は本当に危ないので非推奨です。AWSかGCPのサービスを使って下さい)。
7. 次はメインネットのアカウントを作成します。Jacob君のビデオでは約2年前にflow-cli
にmainnet
用にアカウント作成コマンドが作成されているようなのでそれを実行します。(私は公式ドキュメントよりJacob君のビデオの方を信頼しています。公式も無闇に変えられないのです。)
flow accounts create --network=mainnet
account nameを入れろ? って何だそれは。2年前はなかったぞ?
ここに以下のような表記がありました。
現在はaccount nameを入力しないとアカウントを作れないようですね。
何て入れようかな。oragaでいいか。
うん? public key? ということはflow.jsonは..
あっ、oraga.pkey
というファイルに秘密鍵が生成されている。
鍵の種類もECDSA_secp256k1
というビットコインやイーサリアムで使われているカーブではなくなっているようなので、再トライします。(その前にmainnet-account.pkey
を削除しておきます)
flow accounts create --network=mainnet --sig-algo=ECDSA_secp256k1
Enter an account name: mainnet-account█
なるほど、確かに便利です。Flowのメインネットのアドレスとそれに紐づく公開鍵、秘密鍵を生成してくれて、flow.jsonにも自動でセットしてくれました。
(追記:注意! --sig-algo
は --key
フラグがないと無視されます。そしてECDSA_P256
カーブでkeyが作成されてしまいます。ですのでECDSA_secp256k1
カーブでkeyを作成する場合はやっぱり6の処理で公開鍵/秘密鍵を作って --key
フラグで公開鍵を渡す必要があります。)
8. 残りはコントラクトのデプロイです。
flow project deploy --network mainnet
これでflow.jsonの設定値を参考にデプロイしてくれるはず。どきどき。。
ストレージリミットを超えたからTokenを持たせろ、と言っている...
トークンを送付しますか…
b576a3926d239682 に対してFlowトークンを送付します。
どれぐらい必要なのかわからないのでとりあえず1FLOW
のトークンをBloctoを使って送付します。
再度実行。
flow project deploy --network mainnet
めっちゃ時間かかる。。
あっできた。この時点でFlowScanのContracts
タブを見てみるとOragaESports
コントラクトが見えます。
https://www.flowscan.io/contract/A.b576a3926d239682.OragaESports?tab=deploymentsの画面でデプロイしたスマートコントラクトのコードも見ることができました。(FlowScanの仕様変更でリンク切れになるかもしれませんが)
9. では次は今デプロイしたこのコントラクトにアクセスします。
fcl.config({
'flow.network': 'testnet',
'accessNode.api': 'https://rest-testnet.onflow.org',
'discovery.wallet': 'https://wallet-v2-dev.blocto.app/-/flow/authn',
'app.detail.title': 'Oraga eSports',
'app.detail.icon': 'https://oraga-esports.com/assets/MMO%20RPG.png',
}).load({ flowJSON });
この部分をBloctoのサイトを参考に以下のようにします。
fcl.config({
'flow.network': 'mainnet',
'accessNode.api': 'https://rest-mainnet.onflow.org',
'discovery.wallet': 'https://wallet-v2.blocto.app/-/flow/authn',
'app.detail.title': 'Oraga eSports',
'app.detail.icon': 'https://oraga-esports.com/assets/MMO%20RPG.png',
}).load({ flowJSON });
とします。以上かな?
10. ゲーム画面にアクセス。(10の内容は私の凡ミスによるエラーに対するトラブルシューティングが書かれているだけなので折り畳んでいます)
エラーになりますね。。トラブルシューティングに入ります。
これはコントラクトが参照できてない内容のエラーですね。どこか他にtestnet
に対してconfig
処理しているところがないか探します。
あっ。。。ブログの方だけ変えてコードの方はtestnet
のままだった。。
今度は違うエラー… OragaESports
コントラクトを参照できるようにはなったけど、違うエラーが出ています。
このエラーが発生しているのは以下のコードの場所なのでここを調べます。
export const getGamersInfo = async function () {
const result = await query({
cadence: `
import "OragaESports"
access(all) fun main(): OragaESports.GamersInfo {
return OragaESports.getGamersInfo()
}
`,
});
return result;
};
import “OragaESports”
の部分は }).load({ flowJSON });
の部分の行によって
import OragaESports from 0xb576a3926d239682
に変換されています。
hostname=https://rest-testnet.onflow.org
path=/v1/scripts?block_height=final
method=POST
requestBody={"script":"CiAgICBpbXBvcnQgT3JhZ2FFU3BvcnRzIGZyb20gMHhiNTc2YTM5MjZkMjM5NjgyCgogICAgYWNjZXNzKGFsbCkgZnVuIG1haW4oKTogT3JhZ2FFU3BvcnRzLkdhbWVyc0luZm8gewogICAgICByZXR1cm4gT3JhZ2FFU3BvcnRzLmdldEdhbWVyc0luZm8oKQogICAgfQogICAg","arguments":[]}
responseBody={
"code": 400,
"message": "Invalid Flow argument: failed to execute the script on the execution node execution-001.devnet52.nodes.onflow.org:3569: rpc error: code = InvalidArgument desc = failed to execute script: [Error Code: 1101] failed to execute script at block (790b1d90d17156ac582acb57081531195d53b911552cac7ca7794eda602cf8d7): [Error Code: 1101] error caused by: 1 error occurred:\n\t* [Error Code: 1101] cadence runtime error: Execution failed:\nerror: cannot find declaration `OragaESports` in `b576a3926d239682.OragaESports`\n --\u003e ec03acd882738e97425e1ced2090a99fd1cd75476a8d4373eb32963a33b34104:2:11\n |\n2 | import OragaESports from 0xb576a3926d239682\n | ^^^^^^^^^^^^ available exported declarations are:\n\n\nWas this error unhelpful?\nConsider suggesting an improvement here: https://github.com/onflow/cadence/issues.\n\n\n"
ブラウザのconsoleログに出ていたエラーをペーストしてきました。
hostname
がhttps://rest-testnet.onflow.orgになっていますね。。なんでtestnet
?
あっ。。違うファイルでもconfig
処理していた。(すみません、fcl.config
とconfig
と違う書き方をしていたので気づきませんでした)
というわけで10は全て私が悪いです。ちゃんと画面には現在の賞金額とTip Jarの残高がデプロイしたMainnet
の情報で表示されていました。
11. SignInボタンでBloctoにサインインします。(ここも私の凡ミスですが)
すると画面がうっすら暗くなっただけ。左上に文字が小さく見えるのでF12ツールを使ってコピーしてきました。
{
"error_code":"authentication_id_required",
"message":"Missing 'authenticationId' query param"
}
何だろ、Bloctoと関係あるのかな。authenticationId…
何かBloctoに登録しないといけなかったっけ。あっ、またやってしまった..
https://wallet-v2.blocto.app/-/flow/authn
でした、discovery.wallet
に設定しないといけないのは。/-/
って何だ、と思ってBloctoのサイトのback channelの方をセットしてしまっていました(よく確認しましょう)。9.のところは直しておきます。
ログインできました。残高も表示されました。
12. 最後はS3に対するアップロードとLambdaに対するアップロードのみです。
npm run build
# /build以下をS3にアップロード
cd /aws_lambda/mainnet/src
zip -r ../src.zip .
# /aws_lambda/mainnet/src.zip をLambdaにアップロード
Lambdaにアップロードした後はcodeタブでコードを確認できるので軽く更新されているか確認してみます。
13. https://oraga-esports.com/のURLで確認します
何だ何だ、またエラーか。。
このpanic
を呼び出しているところを探します。
export const getBalance = async function (address) {
const result = await query({
cadence: `
import "FlowToken"
import "FungibleToken"
access(all) fun main(address: Address): UFix64 {
let vaultRef = getAccount(address).capabilities
.borrow<&FlowToken.Vault>(/public/flowTokenBalance)
?? panic("Something wrong happened.")
return vaultRef.balance
}
`,
args: (arg, t) => [arg(address, t.Address)],
});
return result;
};
ここですね。ウォレット残高取得部分か.. ローカルでは出来てたから..
結果はキャッシュでした。CloudFrontでInvalidation
するだけではChromeはキャッシュを表示し続けるのですね。(Chromeのキャッシュクリアで解決します)
次はスマートフォンです。なぜかSigninするポップアップが出ない。
signoutボタンが出ているので以前のログイン情報が何故か残っていたようです。signoutボタンを押すとポップアップが出ました。
Let’s play my eSports!
名前を登録してFLOW Tokenを別のアドレスから送金しておきプレイ開始します。
負けた。。こうやって1FLOWでもコインを失うのは悲しいですね。取り返したくなる。。(Prizeが増えてないのでLambdaファイルにトランザクションが実行できないバグがあるようです。後で直しておきます)
先ほど作成したMainnet
のスマートコントラクトをデプロイしたアドレスには、プレイ料金が0.1FLOWずつ蓄積されます。それを他の自分のウォレットアカウントに送金する方法はこちらで紹介されているコードとJacob君のビデオをご覧ください。
以上です〜。