Case study — Team & Open source
ぼどぷる
ボードゲームのプレイ履歴を、盤面の写真と電子サインを添えてNFTとして残すモバイルアプリ。英語名はBoard Game Proofです。
2024年度にWeb3概論のチームプロジェクトとして始まったVer.1を引き継ぎ、Ver.2として実際にチェーン上で動くところまで作り込んでいます。同じタイトルを重ねるほどバッジの外枠が変わり、プレイヤー同士はQRで名刺を交換します。
- 5Badge tiers
- 13Game titles
- 2Contracts
- 133Tests
- Status
- Sepolia テストネットで稼働中
- Proof
- 0xA2c3…EDDC
- Card
- 0xa426…938d
- App
- Expo(iOS / Android)
- API
- Cloudflare Workers + D1
- 配布
- Firebase App Distribution(コミュニティ内)
01 How it works
使い方
遊び終わったその場で証明を発行し、名刺を交換するところまでを1つのアプリで完結させています。
| 1. 撮る | 盤面を撮影し、その上に指で電子サインを書きます。サインは透過PNGとして写真に重ねます。 |
|---|---|
| 2. 発行する | ゲームタイトル、プレイ日、参加者、自由記述を添えて発行します。画像はIPFSにピン留めし、失敗したら発行を中断します。 |
| 3. 重ねる | ホームではタイトルごとにまとめてバッジで表示します。同じタイトルを重ねるほど外枠が変わります。 |
| 4. 交換する | 自分の名刺をQRで見せ、相手のQRを読み取ります。譲渡できない名刺(SBT)なので、その人自身を指します。 |
02 Badge
回数で変わるバッジ
発行時点での通算プレイ回数をチェーンに焼き込んでいるので、外枠はチェーンを走査せずに決まります。1回、3回、5回、10回、20回の5段階です。
-
01 1回目 — 木の枠 -
02 3回目 — 銀の枠 -
03 5回目 — 月桂樹と番号 -
04 10回目 — Master -
05 20回目 — Legendary
- いずれも中心が透過した輪で、ゲームアイコンの上に重ねて使います。枠の内側の穴が画像幅に占める割合を素材ごとに計測して持たせているので、アイコンが枠にぴったり収まります。段位を増やすときは表に1行足すだけです。
- 絵の無いタイトルには、他のタイトルの絵で代用せず頭文字の丸を出します。代用すると「ラブレター」のバッジにカタンの絵が付くことになるためです。
- 外枠の素材はA.J.さん(@JanadoNovel)の作です。
03 Scope
担当範囲
チームで始まったVer.1を引き継ぎ、Ver.2としてアプリ・コントラクト・APIと、そこへ配信するまでの経路を担当しています。
| App | Expoアプリの実装。ウォレット接続、証明の発行と一覧、撮影と電子サイン、名刺のQR交換。 |
|---|---|
| Contracts | 証明(ERC-721)と名刺(ERC-5192)の設計・実装・テスト・Sepoliaへのデプロイ。メタデータはオンチェーンで生成しています。 |
| API | Cloudflare Workersの実装。Firebase IDトークンの検証、IPFSへのピン留め代行、レート制限、D1のスキーマ。 |
| Delivery | GitHub Actionsでの型チェック・テスト・ビルドと、Firebase App Distributionへの配信。 |
| Credit | バッジ外枠の素材はA.J.さん(@JanadoNovel)の作です。Ver.1の企画と仕様はチームによるもので、Ver.2はその上に載せています。 |
04 Stack
技術構成
| App | Expo SDK 54(React Native 0.81 / React 19.1)、expo-router |
|---|---|
| Wallet | WalletConnect、ethers v6 |
| Contracts | Solidity、Hardhat(Scaffold-ETH 2)、OpenZeppelin、ERC-721 / ERC-5192 |
| API | Cloudflare Workers、D1、Pinataへのプロキシ |
| Auth | Firebase Authentication(匿名)、IDトークンをJWKSで検証 |
| Storage | IPFS(Pinata、複数ゲートウェイを順に試行) |
| Quality | Hardhatテスト110件、Workerテスト23件、GitHub Actionsで型チェックとバンドル |
| Delivery | eas build --local(セルフホスト)、Firebase App Distribution |
05 Principles
設計の考え方
残すものが記録なので、あとから直せる範囲がふつうのアプリより狭くなります。画面はいつでも作り直せますが、チェーンに書いたものは消せず、コントラクトを入れ替えれば過去の記録から切り離されます。決める順番を、直せないものから先に、という形にしています。
- 不可逆なものから決める。参加者は表示名ではなくアドレスの配列で、日付はロケールに依存しないunix秒で、通算プレイ回数は発行時点の値をそのまま焼き込んで残します。どれもいまの画面には要りませんが、あとから足すと過去の記録と繋がらなくなるものです。将来プレイヤー同士の相互評価を載せるときも、記録を作り直さずに済みます。
- 信頼境界は、コードが動く場所で引く。モバイルアプリのバンドルは展開できるので、値を環境変数へ移しても秘密にはなりません。外部サービスの鍵は運用側が握るWorkerのsecretにだけ置き、アプリからは代理で叩きます。認証も同じで、IDトークンの検証をGoogleの公開鍵だけで完結させ、サーバーに特権を持たせていません。
- 壊れ方を先に決める。公開IPFSゲートウェイは日常的に落ち、ビューアはbase64を復号したあとの文字コードを選べず(Etherscanはこれをラテン文字として読みます)、交換した相手がまだ名刺を作っていないこともあります。どれも起きる前提で、ゲートウェイは順に試す、メタデータはASCIIだけで書く、壊れた文字は発行を失敗させずに置き換える、名刺の一覧は未登録の相手が混ざっても壊さない、と機能ごとに決めています。逆に、画像のピン留めに失敗したときは中身の無い証明を残さないよう、発行そのものを止めます。
- 摩擦は、永続する場所にだけ置く。証明の発行はチェーンに残すことが目的なので、ガスを払う価値があります。名刺交換はその場でQRを読むだけという手軽さが本体なので、チェーンには書かず端末に控えます。技術的に載せられることと、載せるべきことは分けて考えています。
- 上限は運用ではなく仕組みで持つ。WAF、ユーザーごとのバースト制限、固定窓のカウンタ、トークン検証、本文サイズの上限を重ね、どれか1つに頼らない形にしています。制限の鍵に使うのは、共有されうるIPではなくuidです。いちばん外側の上限は無料枠そのもの(10万リクエスト/日)で、超えたときに起きるのは請求ではなく停止です。
06 On-chain
公開されているもの
アプリはコミュニティ内への限定配布ですが、コントラクトはSepoliaのエクスプローラーからそのまま読めます。
- BoardGameProof ↗ プレイ履歴の証明(ERC-721)。発行された証明とメタデータを確認できます。
- BoardGameCard ↗ プレイヤー名刺(ERC-5192)。1アドレスにつき1枚で、譲渡できません。
- アプリはFirebase App Distributionでコミュニティ内に配布しています。ストアには出していません。
- ソースコードはチーム内の非公開リポジトリで管理しています。