sangraalgoogja.dev

Case study — Team & Open source

ぼどぷる

ボードゲームのプレイ履歴を、盤面の写真と電子サインを添えてNFTとして残すモバイルアプリ。英語名はBoard Game Proofです。

2024年度にWeb3概論のチームプロジェクトとして始まったVer.1を引き継ぎ、Ver.2として実際にチェーン上で動くところまで作り込んでいます。同じタイトルを重ねるほどバッジの外枠が変わり、プレイヤー同士はQRで名刺を交換します。

  • 5Badge tiers
  • 13Game titles
  • 2Contracts
  • 133Tests
  • Team
  • Expo / React Native
  • Solidity
  • Cloudflare Workers
  • Sepolia
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段階です。

  • プレイ1回目のバッジ外枠。中心が透過した木目調の茶色い輪。
    01 1回目 — 木の枠
  • プレイ3回目のバッジ外枠。中心が透過した銀色の輪。
    02 3回目 — 銀の枠
  • プレイ5回目のバッジ外枠。金色の月桂樹の輪の上部に丸囲みの5が入っている。
    03 5回目 — 月桂樹と番号
  • プレイ10回目のバッジ外枠。白銀の月桂樹の輪で、上部に丸囲みの10、下部の帯に Master と書かれている。
    04 10回目 — Master
  • プレイ20回目のバッジ外枠。虹色に光る輪で、上部に 20th、下部に Legendary と書かれている。
    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
WalletWalletConnect、ethers v6
Contracts Solidity、Hardhat(Scaffold-ETH 2)、OpenZeppelin、ERC-721 / ERC-5192
APICloudflare Workers、D1、Pinataへのプロキシ
Auth Firebase Authentication(匿名)、IDトークンをJWKSで検証
StorageIPFS(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のエクスプローラーからそのまま読めます。

  • アプリはFirebase App Distributionでコミュニティ内に配布しています。ストアには出していません。
  • ソースコードはチーム内の非公開リポジトリで管理しています。