diff --git a/docs/Base/Base-Mini-Shooting-Game/index.md b/docs/Base/Base-Mini-Shooting-Game/index.md new file mode 100644 index 00000000..0794ca4f --- /dev/null +++ b/docs/Base/Base-Mini-Shooting-Game/index.md @@ -0,0 +1,3 @@ +# Base Mini Shooting Game + + \ No newline at end of file diff --git a/docs/Base/Base-Mini-Shooting-Game/section-1/lesson-0.md b/docs/Base/Base-Mini-Shooting-Game/section-1/lesson-0.md new file mode 100644 index 00000000..5a7b1afd --- /dev/null +++ b/docs/Base/Base-Mini-Shooting-Game/section-1/lesson-0.md @@ -0,0 +1,67 @@ +--- +title: "はじめに" +--- + +# Section 0: はじめに + +## 🚀 このプロジェクトで学べること + +このプロジェクトでは、Baseブロックチェーン上で動作する **Mini App「Mini Shooting Game」** を開発します。 + +**Farcaster** の投稿内で直接遊べるシューティングゲームを構築しながら、以下の技術を実践的に学びます。 + +- **Base**: + Coinbaseが開発した高速・低コストなEthereumレイヤー2ブロックチェーンの基礎知識 +- **OP Stack**: + Baseの基盤技術であるOP Stackと、それが実現するSuperchain構想の理解 +- **Farcaster**: + 分散型SNSプロトコルと、その上で動作するMini Appの仕組み +- **MiniApp**: + MiniApp Kitを使ってオリジナルのミニアプリを公開する方法 +- **スマートコントラクト**: + Remix IDEを使用したスマートコントラクトのデプロイとフロントエンドへの連携 +- **Next.js**: + 最新のWebフレームワークを使ったフロントエンド開発 +- **Vercel**: + フロントエンドアプリケーションのデプロイとMini Appとしての公開 + +## 🎯 プロジェクトのゴール + +このプロジェクトのゴールは、以下の画像のように、Farcasterのフィード上で直接起動し、プレイできるシューティングゲームを開発・公開することです。ゲームをプレイし、倒した敵の数に応じてNFTをミントする機能を実装します。 + + + + + + + +## 📖 プロジェクトの構成 + +このプロジェクトは、以下のセクションで構成されています。 + +- **Section 1: 基礎知識と環境構築** + - Base、OP Stack、Farcasterなどの関連技術について学び、開発環境をセットアップします。 + +- **Section 2: スマートコントラクトの実装** + - ゲームのスコアを記録し、NFTをミントするためのスマートコントラクトをBase Sepoliaテストネットにデプロイします。 + +- **Section 3: フロントエンドアプリケーションの実装** + - Next.jsとOnchainKitを使い、ゲームのロジックとUIを構築します。 + +- **Section 4: デプロイとMini App化** + - 作成したアプリケーションをVercelにデプロイし、Farcaster上でMini Appとして公開します。 + +それでは、さっそく次のセクションから開発を始めていきましょう! + +--- + +### 🙋♂️ 質問する + +ここまでの作業で何かわからないことがある場合は、Discordの`#base`で質問をしてください。 + +ヘルプをするときのフローが円滑になるので、エラーレポートには下記の3点を記載してください ✨ + +1. 質問が関連しているセクション番号とレッスン番号 +2. 何をしようとしていたか +3. エラー文をコピー&ペースト +4. エラー画面のスクリーンショット diff --git a/docs/Base/Base-Mini-Shooting-Game/section-1/lesson-1.md b/docs/Base/Base-Mini-Shooting-Game/section-1/lesson-1.md new file mode 100644 index 00000000..67268dae --- /dev/null +++ b/docs/Base/Base-Mini-Shooting-Game/section-1/lesson-1.md @@ -0,0 +1,172 @@ +--- +title: "Baseと周辺技術を学ぶ" +--- + +# Lesson 1: Baseと周辺技術を学ぶ + +このレッスンでは、Mini App開発に不可欠なBaseブロックチェーンとその周辺技術について学びます。 + +## 🔵 Baseとは + +[Base](https://www.base.org/)は、世界最大級のCEXであるCoinbaseによって構築された高速かつ低コストなEthereumのレイヤー2(L2)ブロックチェーンです。 + +Ethereumのメインネットワーク(レイヤー1)が混雑し、手数料が高騰する問題を解決するために生まれました。 + +簡単に言うと、Ethereumの「高速道路」のような存在で、メインのネットワークよりも速く、安く取引を処理できます。 + +**主な特徴**: + +- **低コスト**: + Ethereumメインネットに比べて、ガス代(取引手数料)が大幅に安価です。 +- **高速処理**: + トランザクションが迅速に承認されます。 +- **EVM互換**: + Ethereumと同じ開発環境(Solidity, Remix, Hardhatなど)が利用でき、既存のdAppを容易に移行できます。 +- **Coinbaseとの連携**: + Coinbaseが提供する豊富な開発者向けプラットフォーム(Coinbase Developer Platform)の恩恵を受けることができます。 + +より詳しい情報や最新の統計は、公式サイトやブロックエクスプローラーで確認できます。 + +- [Base公式サイト](https://www.base.org/) +- [Base Block Explorer (Basescan)](https://basescan.org/) + +## LAYER-2 📜 レイヤー2とは + +レイヤー2(L2)は、Ethereum本体(レイヤー1)のセキュリティを継承しつつ、スケーラビリティ(処理能力)を向上させるために構築されたセカンダリチェーンです。 + +L1の負荷を軽減するために、トランザクションの大部分をL2で処理し、その結果の要約だけをL1に記録します。これにより、高速かつ安価なトランザクションが実現します。 + +**代表的なL2技術**: + +- **Optimistic Rollups**: + BaseやOptimismが採用するアプローチ。トランザクションは基本的に「正しい」と楽観的に(Optimistic)みなし、不正が疑われる場合にのみ検証(Fraud Proof)を行います。 + +- **ZK-Rollups (ZK-EVMs)**: + ゼロ知識証明(Zero-Knowledge Proof)を用いて、トランザクションの正しさを証明するアプローチ。StarkNetやzkSyncなどが採用しています。 + +各L2プロジェクトの状況は、[L2BEAT](https://l2beat.com/scaling/tvl)で詳しく確認できます。 + +## 🥞 OP Stackとは + +[OP Stack](https://stack.optimism.io/)は、Baseが採用しているL2構築のためのオープンソースソフトウェアです。 + +元々はL2プロジェクトであるOptimismを動かすために開発されました。 + +OP Stackを利用することで、Optimism(OP Mainnet)やBaseのように、標準化され、相互運用性を持つL2チェーンを比較的容易に構築できます。 + +**主な特徴**: + +- **EVM等価性**: + Ethereumとほぼ同じ開発体験を提供し、既存のツールやスマートコントラクトとの高い互換性を持ちます。 + +- **モジュール性**: + データ可用性、実行、シーケンシングといった各機能をモジュールとして組み合わせることで、柔軟なチェーン設計が可能です。 + +- **Superchain構想**: + OP Stackで構築されたチェーン同士が安全に通信しあう「Superchain」という未来のビジョンを支える基盤技術です。 + +## 🔗 Superchain構想とは + +[Superchain](https://optimism.io/superchain)は、OP Stackを用いて構築された多数のL2チェーン(OP Chains)が、1つの巨大なチェーンのようにシームレスに連携するネットワーク構想です。 + +個々のチェーンは独立して動作しつつも、共通のブリッジとシーケンサーを共有することで、チェーン間での資産移動や通信が安全かつ効率的に行えるようになります。これにより、ユーザーは自分がどのチェーン上にいるかを意識することなく、エコシステム全体を利用できる世界の実現を目指しています。 + +## 🛠 Coinbase Developer Platform (CDP) + +[Coinbase Developer Platform (CDP)](https://www.coinbase.com/cloud)は、BaseエコシステムでのdApp開発を加速させるための包括的なツール群です。 + +開発者がブロックチェーンアプリケーションを構築するために必要なツールが詰まった「工具箱」のような存在です。 + +**主な提供機能**: + +- **Node Service (RPC)**: + 高信頼性のRPCエンドポイントを無料で利用できます。 + +- **Smart Wallets**: + ユーザーが秘密鍵を管理することなく、パスキーなどでdAppを利用できるウォレットソリューションです。 + +- **Paymaster**: + dAppがユーザーのガス代を肩代わりする「ガスレス」な体験を実現します。 + +- **OnchainKit**: + ReactベースのUIコンポーネントやフックを提供し、フロントエンド開発を簡素化します。 + +- **Faucet**: + Base Sepoliaテストネットで利用するテスト用ETHを簡単に入手できます。 + +## 🦋 Farcasterとは + +[Farcaster](https://www.farcaster.xyz/)は、分散型のソーシャルネットワークプロトコルです。 + +TwitterやInstagramのように投稿やフォローができますが、最大の違いは**ユーザーが自身のデータを完全に所有できる**点にあります。 + +**主な特徴**: + +- **データの所有権**: + あなたの投稿、フォロー情報、プロフィールはあなた自身のものです。特定の企業に依存しません。 + +- **アプリケーションの自由**: + 同じFarcasterアカウントを使って、様々なクライアントアプリ(例: Warpcast, Supercast)からアクセスできます。 + +- **検閲耐性**: + 中央管理者がいないため、一方的にアカウントが削除されたり、投稿が消されたりするリスクが低いです。 + +## 📱 Farcaster WalletとConnected App + +Farcasterを利用するには、まず**Wallet App**でアカウントを作成します。 + +これはあなたのアカウントを安全に管理する「金庫」のようなものです。 + +一度アカウントを作成すれば、様々な**Connected App**(接続アプリ)を追加して、異なるUIや機能でFarcasterを楽しむことができます。 + +- **Wallet App**: + アカウントの作成、復元、接続アプリの管理など、所有者としての全権限を持ちます。公式クライアントである[Warpcast](https://warpcast.com/)が代表的です。 + +- **Connected App**: + 投稿や閲覧など、Wallet Appから許可された機能のみを実行します。 + +この仕組みにより、万が一悪意のあるConnected Appを許可してしまっても、アカウント自体が乗っ取られるリスクを最小限に抑えることができます。 + +## 🆔 Farcaster Accountの仕組み + +Farcasterのアカウントは、いくつかの要素で構成されています。 + +- **Farcaster ID (fid)**: + 各ユーザーに割り当てられる一意の番号。 + +- **ユーザー名**: + `@username`形式の表示名。後から変更可能です。 + +- **Custody Address**: + アカウントの所有権を持つEthereumアドレス。アカウントの譲渡や復元に使用します。 + +- **App Keys**: + 各Connected Appが投稿などの操作を行うために使用する鍵。 + +- **Recovery Address**: + Custody Addressを紛失した際のバックアップ用アドレス。 + +また、Farcasterでは投稿などのデータを保存するために、年間約7ドルでストレージユニットを借りる必要があります。 + +## ✨ Mini Appとは + +Mini Appは、Farcasterの投稿(Cast)内で直接起動・実行できるアプリケーションです。 + +ユーザーは外部サイトに遷移したり、アプリをダウンロードしたりすることなく、ソーシャルフィード上でシームレスにdAppを体験できます。 + +今回のプロジェクトで開発するシューティングゲームも、このMini Appとして実装します。 + +Next.jsで構築した既存のWebアプリケーションをMini App化するためのSDKとして、Coinbaseが提供する**OnchainKit**の一部である**MiniAppKit**を使用します。 + +--- + +### 🙋♂️ 質問する + +ここまでの作業で何かわからないことがある場合は、Discordの`#base`で質問をしてください。 + +ヘルプをするときのフローが円滑になるので、エラーレポートには下記の3点を記載してください ✨ + +1. 質問が関連しているセクション番号とレッスン番号 +2. 何をしようとしていたか +3. エラー文をコピー&ペースト +4. エラー画面のスクリーンショット diff --git a/docs/Base/Base-Mini-Shooting-Game/section-1/lesson-2.md b/docs/Base/Base-Mini-Shooting-Game/section-1/lesson-2.md new file mode 100644 index 00000000..c20a9c21 --- /dev/null +++ b/docs/Base/Base-Mini-Shooting-Game/section-1/lesson-2.md @@ -0,0 +1,77 @@ +--- +title: "環境構築" +--- + +# Lesson 2: 環境構築 + +このレッスンでは、Mini App開発に必要なツールをセットアップし、プロジェクトの基盤を整えます。 + +## 🛠 必要なツール + +開発を始める前に、以下のツールがインストールされていることを確認してください。 + +- [Node.js](https://nodejs.org/ja/)(バージョン23以上) +- [pnpm](https://pnpm.io/ja/installation) +- [Git](https://git-scm.com/) + +## 📂 プロジェクトのセットアップ + +まず、本プロジェクトのスターターコードをクローンし、依存関係をインストールします。 + +ターミナルを開き、以下のコマンドを実行してください。 + +```bash +git clone https://github.com/unchain-tech/Base-Mini-Shooting-Game.git +cd Base-Mini-Shooting-Game +pnpm install +``` + +これにより、開発に必要なライブラリがすべてインストールされます。 + +## 🦋 Farcasterアカウントの作成 + +Mini Appをテスト・公開するには、Farcasterのアカウントが必要です。 + +まだ持っていない場合は、以下の手順で作成してください。 + +1. スマートフォンに[Warpcast](https://warpcast.com/)アプリをダウンロードします。 + + + +2. アプリの指示に従い、アカウントを作成します。 + - アカウント作成には少額の費用(年間約7ドル)がかかります。 + - アカウントの復元に必要な**リカバリーフレーズ**は、必ず安全な場所に保管してください。 + +## 🔑 Coinbase Developer Platform (CDP) APIキーの作成 + +次に、プロジェクトで利用するCDPのAPIキーを作成します。このキーは、フロントエンドからスマートコントラクトを操作するために必要です。 + +1. [Coinbase Developer Platform](https://www.coinbase.com/cloud)にアクセスし、Coinbaseアカウントでサインインします。 + +2. ダッシュボードの左側メニューから「**Build on Base**」を選択します。 + +3. 「**API Keys**」タブに移動し、「**Create API Key**」ボタンをクリックします。 + +4. キーの名前(例: `Mini-Shooting-Game-Key`)を入力し、「**Create API Key**」をクリックします。 + +5. 作成された**Public Key**が表示されます。このキーは後でプロジェクトの環境変数として設定するため、コピーして安全な場所に保存しておいてください。 + + > ⚠️ **注意** + > このキーはフロントエンドで使用される公開キーであり、秘密キーではありません。第三者に見られても直接的な資金の損失にはつながりませんが、取り扱いには注意してください。 + +これで開発に必要なすべての環境構築が完了しました。 + +次のセクションでは、いよいよスマートコントラクトのデプロイに取り掛かります。 + +--- + +### 🙋♂️ 質問する + +ここまでの作業で何かわからないことがある場合は、Discordの`#base`で質問をしてください。 + +ヘルプをするときのフローが円滑になるので、エラーレポートには下記の3点を記載してください ✨ + +1. 質問が関連しているセクション番号とレッスン番号 +2. 何をしようとしていたか +3. エラー文をコピー&ペースト +4. エラー画面のスクリーンショット diff --git a/docs/Base/Base-Mini-Shooting-Game/section-2/lesson-1.md b/docs/Base/Base-Mini-Shooting-Game/section-2/lesson-1.md new file mode 100644 index 00000000..33a57dc1 --- /dev/null +++ b/docs/Base/Base-Mini-Shooting-Game/section-2/lesson-1.md @@ -0,0 +1,160 @@ +--- +title: "スマートコントラクトをデプロイする" +--- + +# Lesson 1: スマートコントラクトをデプロイする + +このレッスンでは、ゲームの結果を記録し、NFTをミントするためのスマートコントラクトをBase Sepoliaテストネットにデプロイします。 + +## 🎮 ゲームコントラクトの概要 + +今回使用するスマートコントラクト`ShootingGameNFT.sol`は、ERC1155規格に準拠したNFTコントラクトです。 + +主な機能は以下の通りです。 + +- **NFTのミント**: + プレイヤーがゲームで倒した敵の数だけ、対応するIDのNFTをミントします。 + +- **所有権**: + OpenZeppelinの`Ownable`を継承しており、コントラクトのオーナーのみがURIの設定や一括ミントなどの管理者機能を実行できます。 + +- **供給量の追跡**: + `ERC1155Supply`を継承しており、各NFTの総供給量を追跡できます。 + +スタータープロジェクトの`packages/contract/src/ShootingGameNFT.sol`にコードが用意されています。 + +```solidity +// SPDX-License-Identifier: MIT +// Compatible with OpenZeppelin Contracts ^5.4.0 +pragma solidity ^0.8.27; + +import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; +import {ERC1155Burnable} from "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Burnable.sol"; +import {ERC1155Supply} from "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Supply.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +/** + * シューティングゲームNFT + * 倒した敵の数だけERC1155のNFTがミントされる + */ +contract ShootingGameNFT is ERC1155, Ownable, ERC1155Burnable, ERC1155Supply { + + /** + * コンストラクター + */ + constructor(address initialOwner) ERC1155("") Ownable(initialOwner) {} + + /** + * トークンURIを変更するメソッド + */ + function setURI(string memory newuri) public onlyOwner { + _setURI(newuri); + } + + /** + * NFTをミントするメソッド + */ + function mint(address account, uint256 id, uint256 amount, bytes memory data) + public + { + _mint(account, id, amount, data); + } + + /** + * 複数のNFTをまとめて一括でミントするメソッド + */ + function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) + public + onlyOwner + { + _mintBatch(to, ids, amounts, data); + } + + // The following functions are overrides required by Solidity. + + function _update(address from, address to, uint256[] memory ids, uint256[] memory values) + internal + override(ERC1155, ERC1155Supply) + { + super._update(from, to, ids, values); + } +} +``` + +## 🔧 Remix IDEでのデプロイ + +スマートコントラクトのデプロイには、ブラウザで利用できる[Remix IDE](https://remix.ethereum.org/)を使用します。 + + + +### 1. ファイルの準備 + +まず、Remix IDEにコントラクトのコードを貼り付けます。 + +1. [Remix IDE](https://remix.ethereum.org/)を開きます。 +2. `File Explorers`パネルの`contracts`フォルダ内に、`ShootingGameNFT.sol`という名前で新しいファイルを作成します。 +3. 上記のソースコードをすべてコピーし、Remixの`ShootingGameNFT.sol`に貼り付けます。 + +### 2. コントラクトのコンパイル + +次に、SolidityのコードをEVMが実行できる形式にコンパイル(変換)します。 + +1. 左のメニューから「**Solidity Compiler**」タブを選択します。 +2. `COMPILER`のバージョンが`0.8.27`以降になっていることを確認します。 +3. `ShootingGameNFT.sol`が開かれている状態で、「**Compile ShootingGameNFT.sol**」ボタンをクリックします。 +4. コンパイラタブのアイコンに緑色のチェックマークが表示されれば、コンパイル成功です。 + + + +### 3. Base Sepoliaへの接続 + +デプロイ先をBase Sepoliaテストネットに設定します。これにはMetaMaskのようなブラウザウォレットが必要です。 + +1. ウォレットに[Base Sepoliaテストネット](https://chainlist.org/chain/84532)を追加し、ネットワークを切り替えます。 +2. [Coinbase Faucet](https://ethglobal.com/faucet/base-sepolia-84532)などから、テスト用のETHを取得しておきます。 +3. Remixの左メニューから「**Deploy & run transactions**」タブを選択します。 +4. `ENVIRONMENT`のドロップダウンから「**Injected Provider - (ウォレット名)**」を選択します。ウォレットがポップアップし、接続を求められたら許可してください。接続先がbase sepoliaになっていればOKです! + + + +### 4. デプロイの実行 + +いよいよコントラクトをデプロイします。 + +1. `CONTRACT`のドロップダウンで`ShootingGameNFT - contracts/ShootingGameNFT.sol`が選択されていることを確認します。 +2. `Deploy`ボタンの横にあるテキストフィールドに、`initialOwner`としてあなた自身のウォレットアドレスを入力します。 +3. 「**Deploy**」ボタンをクリックします。 +4. ウォレットがポップアップし、トランザクションの確認を求められます。内容を確認し、「**確認**」をクリックしてガス代を支払います。 +5. デプロイが完了すると、Remixの下部にあるターミナルに成功ログが表示され、`Deployed Contracts`セクションにコントラクトが表示されます。 + +## 📝 コントラクトアドレスの設定 + +デプロイしたコントラクトをフロントエンドから利用するために、コントラクトアドレスを設定します。 + +1. Remixの`Deployed Contracts`セクションに表示されている`SHOOTINGGAMENFT`コントラクトの横にあるコピーアイコンをクリックして、コントラクトアドレスをコピーします。 +2. `utils/constants.ts`ファイルを開きます。 +3. `GAME_NFT_CONTRACT_ADDRESS`の値を、先ほどコピーしたあなた自身のコントラクトアドレスに書き換えてください。 + +```typescript +// utils/constants.ts + +// base sepoliaにデプロイしたシューティングゲームNFT +export const NFT_ADDRESS = '<ここにコントラクトアドレスを貼り付ける>'; +``` + +これで、スマートコントラクトのデプロイとフロントエンドへの設定が完了しました。 + +次のセクションでは、いよいよフロントエンドアプリケーションの実装に入ります。 + +--- + +### 🙋♂️ 質問する + +ここまでの作業で何かわからないことがある場合は、Discordの`#base`で質問をしてください。 + +ヘルプをするときのフローが円滑になるので、エラーレポートには下記の3点を記載してください ✨ + +1. 質問が関連しているセクション番号とレッスン番号 +2. 何をしようとしていたか +3. エラー文をコピー&ペースト +4. エラー画面のスクリーンショット diff --git a/docs/Base/Base-Mini-Shooting-Game/section-3/lesson-1.md b/docs/Base/Base-Mini-Shooting-Game/section-3/lesson-1.md new file mode 100644 index 00000000..c74a781e --- /dev/null +++ b/docs/Base/Base-Mini-Shooting-Game/section-3/lesson-1.md @@ -0,0 +1,160 @@ +--- +title: "プロバイダーを設定する" +--- + +# Lesson 1: プロバイダーを設定する + +このレッスンでは、アプリケーション全体でweb3の機能を利用するための基盤となる「プロバイダー」を設定します。 + +`OnchainKitProvider`を使い、ウォレット接続やチェーン情報の管理を容易にします。 + +## 📦 `app/providers.tsx`の実装 + +まず、`packages/frontend/src/app/providers.tsx`というファイルを作成し、以下のコードを記述します。 + +```tsx +// app/providers.tsx + +'use client'; + +import { MiniKitProvider } from '@coinbase/onchainkit/minikit'; +import { type ReactNode } from 'react'; +import { baseSepolia } from 'wagmi/chains'; + +/** + * Providers コンポーネント + * アプリ全体のプロバイダー(MiniKitProvider) + * - OnchainKit の設定(API Key / Chain / 外観) + * - フレームの文脈や Wagmi のコネクタを内部で設定 + * @param props + * @returns + */ +export function Providers(props: { children: ReactNode }) { + return ( + + {props.children} + + ); +} +``` + +次に、このプロバイダーがアプリケーション全体をラップするように、`app/layout.tsx`を修正します。 + +```tsx +// app/layout.tsx + +import '@coinbase/onchainkit/styles.css'; +import type { Metadata, Viewport } from 'next'; +import './../css/globals.css'; +import './../css/theme.css'; +import { Providers } from './providers'; + +export const viewport: Viewport = { + width: 'device-width', + initialScale: 1, +}; + +/** + * メタデータを生成するメソッド + * @returns + */ +export async function generateMetadata(): Promise { + const URL = process.env.NEXT_PUBLIC_URL; + return { + title: process.env.NEXT_PUBLIC_ONCHAINKIT_PROJECT_NAME, + description: + 'This is a mini shooting game. Generated by `create-onchain --mini`, a Next.js template for MiniKit', + other: { + // Farcaster Frame 用のメタデータをヘッダに埋め込み + 'fc:frame': JSON.stringify({ + version: 'next', + imageUrl: process.env.NEXT_PUBLIC_APP_HERO_IMAGE, + button: { + title: `Launch ${process.env.NEXT_PUBLIC_ONCHAINKIT_PROJECT_NAME}`, + action: { + type: 'launch_frame', + name: process.env.NEXT_PUBLIC_ONCHAINKIT_PROJECT_NAME, + url: URL, + splashImageUrl: process.env.NEXT_PUBLIC_SPLASH_IMAGE, + splashBackgroundColor: process.env.NEXT_PUBLIC_SPLASH_BACKGROUND_COLOR, + }, + }, + }), + }, + }; +} + +/** + * ルートレイアウトコンポーネント + * @param param0 + * @returns + */ +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {/* MiniKitProvider 等でアプリ全体をラップ */} + {children} + + + ); +} +``` + +## 📖 コード解説 + +### `OnchainKitProvider` + +`@coinbase/onchainkit`から提供される中心的なコンポーネントです。 + +アプリケーション全体をこれでラップすることにより、内部のコンポーネントでウォレット接続の状態やユーザー情報、チェーン情報などを簡単に取得できるようになります。 + +- `apiKey`: + Section 1で取得したCoinbase Developer PlatformのAPIキーを渡します。環境変数`NEXT_PUBLIC_CDP_API_KEY`から読み込むように設定します。 + +- `chain`: + アプリケーションが対話するデフォルトのブロックチェーンを指定します。今回は`baseSepolia`テストネットを使用します。 + +### `QueryClientProvider` + +`@tanstack/react-query`から提供されるプロバイダーです。 + +`OnchainKit`は内部で`react-query`を使用して非同期の状態(例: ブロックチェーンからのデータ取得)を効率的に管理しています。このプロバイダーを設定することで、データのキャッシュや再取得などが自動的に最適化されます。 + +### `layout.tsx`の修正 + +作成した`Providers`コンポーネントを`layout.tsx`でインポートし、`children`(アプリケーションの各ページコンポーネント)をラップします。 + +これにより、アプリケーションのどのページでも`OnchainKit`が提供する機能(Context)にアクセスできるようになります。 + +--- + +これで、アプリケーション全体でweb3機能を使うための準備が整いました。 + +次のレッスンでは、この基盤の上にメインページを構築していきます。 + +### 🙋♂️ 質問する + +ここまでの作業で何かわからないことがある場合は、Discordの`#base`で質問をしてください。 + +ヘルプをするときのフローが円滑になるので、エラーレポートには下記の3点を記載してください ✨ + +1. 質問が関連しているセクション番号とレッスン番号 +2. 何をしようとしていたか +3. エラー文をコピー&ペースト +4. エラー画面のスクリーンショット diff --git a/docs/Base/Base-Mini-Shooting-Game/section-3/lesson-2.md b/docs/Base/Base-Mini-Shooting-Game/section-3/lesson-2.md new file mode 100644 index 00000000..7b8c11f1 --- /dev/null +++ b/docs/Base/Base-Mini-Shooting-Game/section-3/lesson-2.md @@ -0,0 +1,502 @@ +--- +title: "メインページを実装する" +--- + +# Lesson 2: メインページを実装する + +このレッスンでは、アプリケーションの顔となるメインページ`app/page.tsx`を実装します。 + +ゲームコンポーネントを配置し、ウォレットの接続状態に応じてUIが変化するようにします。 + +## 📦 `app/page.tsx`の実装 + +`app/page.tsx`を以下の内容で書き換えます。 + +```tsx +// app/page.tsx + +'use client'; + +// MiniKit の各種フックを用いたメインの画面コンポーネント。 +// - フレームの保存(addFrame) +// - Wallet 連携 UI(OnchainKit) +// - 簡易的なタブ切り替え(Home / Features) + +import { Footer, Header } from '@/components/common'; +import { Home } from '@/components/DemoComponents'; +import { useMiniKit } from '@coinbase/onchainkit/minikit'; +import { useEffect } from 'react'; + +/** + * App コンポーネント + * @returns + */ +export default function App() { + // MiniKit のコンテキスト(フレーム準備完了フラグやクライアント状態) + const { setFrameReady, isFrameReady } = useMiniKit(); + + useEffect(() => { + if (!isFrameReady) { + setFrameReady(); + } + }, [setFrameReady, isFrameReady]); + + return ( + + + {/* ヘッダー */} + + {/* メインコンポーネント */} + + + + {/* フッター */} + + + + ); +} +``` + +## 📖 コード解説 + +### `useMiniKit`フック + +`@coinbase/onchainkit/minikit`が提供するReactフックです。 + +このフックを呼び出すことで、MiniAppに関する処理を簡単に実行することができるようになります。 + +```tsx +const { setFrameReady, isFrameReady } = useMiniKit(); +``` + +今回は`frame`に対応したアプリになっているかどうかチェックするためのステート変数を取得しています。 + +### ``コンポーネント + +このアプリでメインに使用するHomeコンポーネントになります。 + +``コンポーネントの中身は以下の通りです! + +```ts +import ShootingGame from '@/components/Game'; + +/** + * Home コンポーネント + * @param param0 + * @returns + */ +export function Home() { + return ( + + {/* シューティングゲーム コンポーネント */} + + + ); +} +``` + +## 📦 `components/Game/ShootingGame.tsx`の実装 + +シューティングゲーム部分のコンポーネントである`ShootingGame`コンポーネントを実装していきます。 + +以下の内容をコピー&ペーストしてください。 + +```ts +// components/Game/ShootingGame.tsx + +'use client'; + +import { Card } from '@/components/common'; +import { SHOOTING_GAME_NFT_ABI } from '@/utils/abis'; +import { NFT_ADDRESS } from '@/utils/constants'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useAccount } from 'wagmi'; +import { TransactionCard } from '../TransactionCard'; + +type Vec = { x: number; y: number }; +type Rect = Vec & { w: number; h: number }; + +function intersects(a: Rect, b: Rect) { + return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y; +} + +/** + * シューティングゲーム コンポーネント + * @returns + */ +export function ShootingGame() { + const containerRef = useRef(null); + const canvasRef = useRef(null); + const rafRef = useRef(null); + const lastTickRef = useRef(undefined); + + // Logical canvas size (scaled for DPR) + const BASE_W = 360; + const BASE_H = 540; + + const [running, setRunning] = useState(false); + const runningRef = useRef(false); + const [gameOver, setGameOver] = useState(false); + const [score, setScore] = useState(0); + const [high, setHigh] = useState(0); + + // Game state kept in refs to avoid re-renders per frame + const playerRef = useRef({ x: BASE_W / 2 - 15, y: BASE_H - 40, w: 30, h: 14 }); + const bulletsRef = useRef([]); + const enemiesRef = useRef<(Rect & { vy: number })[]>([]); + const keyRef = useRef>({}); + const cooldownRef = useRef(0); + const spawnRef = useRef({ t: 0, interval: 1000 }); + + const { address } = useAccount(); + + // Resize canvas to parent width with DPR scaling + const fitCanvas = useCallback(() => { + const canvas = canvasRef.current; + const container = containerRef.current; + if (!canvas || !container) return; + + const dpr = Math.max(1, Math.floor(window.devicePixelRatio || 1)); + const width = Math.min(container.clientWidth, 480); // cap width + const height = Math.round((BASE_H / BASE_W) * width); + + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; + canvas.width = Math.floor(width * dpr); + canvas.height = Math.floor(height * dpr); + }, []); + + // Game loop + const loop = useCallback( + (now: number) => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const dpr = Math.max(1, Math.floor(window.devicePixelRatio || 1)); + const scaleX = canvas.width / (BASE_W * dpr); + const scaleY = canvas.height / (BASE_H * dpr); + const scale = Math.min(scaleX, scaleY); + // clear in device pixels, then draw with logical scale + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.setTransform(scale * dpr, 0, 0, scale * dpr, 0, 0); + + // time delta (ms) + const last = lastTickRef.current ?? now; + const dt = Math.min(32, now - last); + lastTickRef.current = now; + + const speedMul = 1 + Math.min(1.5, score / 50) * 0.5; // slightly speed up with score + + // Update only while running + const player = playerRef.current; + const bullets = bulletsRef.current; + const enemies = enemiesRef.current; + + if (runningRef.current) { + const move = 0.28 * dt * speedMul; + if (keyRef.current['ArrowLeft'] || keyRef.current['a']) player.x -= move; + if (keyRef.current['ArrowRight'] || keyRef.current['d']) player.x += move; + player.x = Math.max(6, Math.min(BASE_W - player.w - 6, player.x)); + + cooldownRef.current -= dt; + if ((keyRef.current[' '] || keyRef.current['Space']) && cooldownRef.current <= 0) { + bullets.push({ x: player.x + player.w / 2 - 2, y: player.y - 8, w: 4, h: 8 }); + cooldownRef.current = 180 / speedMul; // fire rate + } + + // bullets + for (let i = bullets.length - 1; i >= 0; i--) { + bullets[i].y -= 0.6 * dt * speedMul; + if (bullets[i].y + bullets[i].h < 0) bullets.splice(i, 1); + } + + // spawn enemies + spawnRef.current.t += dt; + if (spawnRef.current.t >= spawnRef.current.interval) { + spawnRef.current.t = 0; + spawnRef.current.interval = Math.max(350, 1000 - score * 10); // faster spawns over time + const w = 20 + Math.random() * 16; + const x = 8 + Math.random() * (BASE_W - w - 16); + const vy = 0.08 + Math.random() * 0.18 + Math.min(0.12, score * 0.002); + enemies.push({ x, y: -24, w, h: w, vy }); + } + + // enemies move + for (let i = enemies.length - 1; i >= 0; i--) { + enemies[i].y += enemies[i].vy * dt * speedMul; + if (enemies[i].y > BASE_H + 40) enemies.splice(i, 1); + } + + // collisions bullet-enemy + outer: for (let i = enemies.length - 1; i >= 0; i--) { + for (let j = bullets.length - 1; j >= 0; j--) { + if (intersects(enemies[i], bullets[j])) { + enemies.splice(i, 1); + bullets.splice(j, 1); + setScore((s) => s + 1); + break outer; + } + } + } + + // collisions player-enemy + for (let i = 0; i < enemies.length; i++) { + if (intersects(player, enemies[i])) { + setGameOver(true); + setRunning(false); + if (score + 0 > high) { + const next = score; + setHigh(next); + try { + localStorage.setItem('shooting_highscore', String(next)); + } catch {} + } + break; + } + } + } + + // Draw + ctx.clearRect(0, 0, BASE_W, BASE_H); + + // background + ctx.fillStyle = '#0a0a0a20'; + ctx.fillRect(0, 0, BASE_W, BASE_H); + + // player + ctx.fillStyle = '#0052ff'; + ctx.fillRect(player.x, player.y, player.w, player.h); + ctx.fillStyle = '#2b6bff'; + ctx.fillRect(player.x + 8, player.y - 6, player.w - 16, 6); // small "cockpit" + + // bullets + ctx.fillStyle = '#22c55e'; + bullets.forEach((b) => ctx.fillRect(b.x, b.y, b.w, b.h)); + + // enemies + ctx.fillStyle = '#f59e0b'; + enemies.forEach((e) => ctx.fillRect(e.x, e.y, e.w, e.h)); + + // score + ctx.fillStyle = '#ffffffcc'; + ctx.font = 'bold 16px system-ui, -apple-system, Segoe UI, Roboto'; + ctx.fillText(`Score: ${score}`, 10, 22); + ctx.fillText(`High: ${high}`, 10, 40); + + // overlays when idle or game over + if (!running && !gameOver) { + ctx.fillStyle = '#ffffffdd'; + ctx.font = 'bold 18px system-ui, -apple-system, Segoe UI, Roboto'; + ctx.textAlign = 'center'; + ctx.fillText('Tap / Space to Start', BASE_W / 2, BASE_H / 2); + ctx.textAlign = 'start'; + } + if (gameOver) { + ctx.fillStyle = '#ef4444'; + ctx.font = 'bold 22px system-ui, -apple-system, Segoe UI, Roboto'; + ctx.textAlign = 'center'; + ctx.fillText('Game Over', BASE_W / 2, BASE_H / 2 - 12); + ctx.fillStyle = '#ffffffdd'; + ctx.font = 'bold 16px system-ui, -apple-system, Segoe UI, Roboto'; + ctx.fillText('Press Restart', BASE_W / 2, BASE_H / 2 + 12); + ctx.textAlign = 'start'; + } + + rafRef.current = requestAnimationFrame(loop); + }, + [BASE_W, BASE_H, gameOver, high, running, score] + ); + + /** + * ゲームを開始するコールバックメソッド + */ + const startGame = useCallback(() => { + // reset + playerRef.current = { x: BASE_W / 2 - 15, y: BASE_H - 40, w: 30, h: 14 }; + bulletsRef.current = []; + enemiesRef.current = []; + cooldownRef.current = 0; + spawnRef.current = { t: 0, interval: 1000 }; + lastTickRef.current = undefined; + setScore(0); + setGameOver(false); + setRunning(true); + rafRef.current = requestAnimationFrame(loop); + }, [BASE_W, BASE_H, loop]); + + // lifecycle + useEffect(() => { + // keep ref in sync to avoid stale closure in RAF loop + runningRef.current = running; + + // highscore + try { + const hs = localStorage.getItem('shooting_highscore'); + if (hs) setHigh(Number(hs) || 0); + } catch {} + + fitCanvas(); + const onResize = () => fitCanvas(); + window.addEventListener('resize', onResize); + + const down = (e: KeyboardEvent) => { + keyRef.current[e.key] = true; + if ((e.key === ' ' || e.key === 'Space') && !running && !gameOver) { + // allow quick start with space + startGame(); + } + }; + const up = (e: KeyboardEvent) => { + keyRef.current[e.key] = false; + }; + window.addEventListener('keydown', down); + window.addEventListener('keyup', up); + + /** + * touch/click to shoot & move + * @param ev + * @returns + */ + const handlePointer = (ev: MouseEvent | TouchEvent) => { + const canvas = canvasRef.current; + if (!canvas) return; + const rect = canvas.getBoundingClientRect(); + const clientX = 'touches' in ev ? ev.touches[0]?.clientX : (ev as MouseEvent).clientX; + if (clientX != null) { + const x = clientX - rect.left; + // move player towards tap position + const width = rect.width; + const targetX = (x / width) * BASE_W - playerRef.current.w / 2; + playerRef.current.x = Math.max(6, Math.min(BASE_W - playerRef.current.w - 6, targetX)); + } + // fire + keyRef.current[' '] = true; + setTimeout(() => (keyRef.current[' '] = false), 60); + if (!running) startGame(); + }; + // capture canvas element to ensure stable cleanup references + const canvasEl = canvasRef.current; + canvasEl?.addEventListener('mousedown', handlePointer); + canvasEl?.addEventListener('touchstart', handlePointer, { passive: true }); + + // kick off passive render loop for initial frame + rafRef.current = requestAnimationFrame(loop); + + return () => { + window.removeEventListener('resize', onResize); + window.removeEventListener('keydown', down); + window.removeEventListener('keyup', up); + canvasEl?.removeEventListener('mousedown', handlePointer); + canvasEl?.removeEventListener('touchstart', handlePointer); + if (rafRef.current) cancelAnimationFrame(rafRef.current); + }; + }, [BASE_W, BASE_H, fitCanvas, running, gameOver, startGame, loop]); + + // NFTを発行するためのコールデータ + // 倒した敵の数分をNFTとして発行する + const calls = useMemo( + () => + address && score > 0 + ? [ + { + address: NFT_ADDRESS as `0x${string}`, + abi: SHOOTING_GAME_NFT_ABI, + functionName: 'mint', + args: [address as `0x${string}`, 0, score, '0x'] as [string, number, number, string], + }, + ] + : [], + [address, score] + ); + + console.log('calls', calls); + + return ( + + + + {!running && !gameOver && ( + + Tap / Space to Start + + )} + {gameOver && score > 0 && } + {gameOver && score === 0 && ( + Score is 0 — nothing to mint. Try again! + )} + + + ); +} + +export default ShootingGame; +``` + +## 📖 コード解説 + +### NFTコントラクトメソッドを呼び出すためのデータの作成 + +NFTをミントするためのデータは以下の部分で実装しています。 + +`useMemo`でメモ化しており、倒した敵の数だけ`ERC1155`規格のNFTがミントできるようになっています。 + +```ts +// NFTを発行するためのコールデータ +// 倒した敵の数分をNFTとして発行する +const calls = useMemo( + () => + address && score > 0 + ? [ + { + address: NFT_ADDRESS as `0x${string}`, + abi: SHOOTING_GAME_NFT_ABI, + functionName: 'mint', + args: [address as `0x${string}`, 0, score, '0x'] as [string, number, number, string], + }, + ] + : [], + [address, score] +); +``` + +そしてこのコールデータを``コンポーネントに渡しています。 + +``コンポーネントは後のレッスンで実装します! + +```ts +{gameOver && score > 0 && } +``` + +### UIの表示 + +- ``: ゲームが描画される領域です。 +- ゲームオーバー時のモーダル: `isGameOver`が`true`になると表示されます。最終スコアとNFTミントボタンが表示されます。 +- ``: 渡されたコールデータを実行するトランザクションを送信するための一覧のコンポーネントです(次のレッスンで実装予定)。 + +--- + +これで、アプリケーションの基本的なレイアウトとウォレット接続のロジックが完成しました。 + +次のレッスンでは、このアプリを`Farcaster`の`Mini App`として認識させるための設定ファイルを作成します。 + +### 🙋♂️ 質問する + +ここまでの作業で何かわからないことがある場合は、Discordの`#base`で質問をしてください。 + +ヘルプをするときのフローが円滑になるので、エラーレポートには下記の3点を記載してください ✨ + +1. 質問が関連しているセクション番号とレッスン番号 +2. 何をしようとしていたか +3. エラー文をコピー&ペースト +4. エラー画面のスクリーンショット diff --git a/docs/Base/Base-Mini-Shooting-Game/section-3/lesson-3.md b/docs/Base/Base-Mini-Shooting-Game/section-3/lesson-3.md new file mode 100644 index 00000000..e412a2d5 --- /dev/null +++ b/docs/Base/Base-Mini-Shooting-Game/section-3/lesson-3.md @@ -0,0 +1,114 @@ +--- +title: "Farcaster Mini Appのメタデータを実装する" +--- + +# Lesson 3: Farcaster Mini Appのメタデータを実装する + +このレッスンでは、私たちのWebアプリケーションが`Farcaster`上で`Mini App`として正しく認識され、表示されるようにするための「メタデータ」を設定します。 + +Farcasterのクローラーは、URLが投稿(Cast)された際にそのページのHTMLを読み取り、特定の``タグを探します。 + +これらのタグにMini Appとしての情報が記述されていると、フィード上でアプリが展開される仕組みです。 + +## 📦 `app/.well-known/farcaster.json/route.ts`の実装 + +FarcasterにMini Appの情報を提供するため、`app/.well-known/farcaster.json/route.ts`というファイルを作成し、以下の内容を記述します。 + +```bash +touch app/.well-known/farcaster.json/route.ts +``` + +次に以下のJSONを記述します。 + +```ts +//app/.well-known/farcaster.json/route.ts + +/** + * オブジェクトから未定義(undefined)、空文字、空配列のプロパティを取り除くためのヘルパー関数です。 + * Farcasterのフレームメタデータなど、不要なプロパティを含めないようにするために使用します。 + * @param properties - クリーニング対象のプロパティを持つオブジェクト + * @returns - 有効なプロパティのみを持つ新しいオブジェクト + */ +function withValidProperties(properties: Record) { + return Object.fromEntries( + Object.entries(properties).filter(([key, value]) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }) + ); +} + +/** + * Farcasterアプリケーションのメタデータを返すAPIエンドポイントです。 + * `.well-known/farcaster.json` としてFarcasterに認識されます。 + * アプリケーションの名前、説明、アイコン、Webhook URLなどの情報を提供します。 + * @see https://docs.farcaster.xyz/reference/app-metadata + */ +export async function GET() { + const URL = process.env.NEXT_PUBLIC_URL; + + return Response.json({ + // Farcasterアカウントの関連付け情報 + accountAssociation: { + header: process.env.FARCASTER_HEADER, + payload: process.env.FARCASTER_PAYLOAD, + signature: process.env.FARCASTER_SIGNATURE, + }, + // Farcasterフレームのメタデータ + frame: withValidProperties({ + version: '1', + name: process.env.NEXT_PUBLIC_ONCHAINKIT_PROJECT_NAME, + subtitle: process.env.NEXT_PUBLIC_APP_SUBTITLE, + description: process.env.NEXT_PUBLIC_APP_DESCRIPTION, + screenshotUrls: [], + iconUrl: process.env.NEXT_PUBLIC_APP_ICON, + splashImageUrl: process.env.NEXT_PUBLIC_APP_SPLASH_IMAGE, + splashBackgroundColor: process.env.NEXT_PUBLIC_SPLASH_BACKGROUND_COLOR, + homeUrl: URL, + webhookUrl: `${URL}/api/webhook`, + primaryCategory: process.env.NEXT_PUBLIC_APP_PRIMARY_CATEGORY, + tags: [], + heroImageUrl: process.env.NEXT_PUBLIC_APP_HERO_IMAGE, + tagline: process.env.NEXT_PUBLIC_APP_TAGLINE, + ogTitle: process.env.NEXT_PUBLIC_APP_OG_TITLE, + ogDescription: process.env.NEXT_PUBLIC_APP_OG_DESCRIPTION, + ogImageUrl: process.env.NEXT_PUBLIC_APP_OG_IMAGE, + }), + // Base Builderに登録する際に必要となる情報 + baseBuilder: { + // Base Buildに登録する際に必要となる(自分のFarcasterアカウントのウォレットアドレスを設定する) + // このアプリを編集・管理できるFarcasterアカウントに紐づくウォレットアドレス + allowedAddresses: ['<ここにFarcasterアカウントに紐づくウォレットアドレスを記載する>'], + }, + }); +} +``` + +> 上記の`allowedAddresses`はセクション1で作成した`Farcaster Account`のウォレットアドレスを記載してください。 + +> 上記の`accountAssociation`の3つのプロパティはこの後のセクションにて設定します。 + +## 📖 コード解説 + +この`route.ts`ファイルは、FarcasterのFrame仕様(Mini Appの前身であり、互換性を持つ)に基づいて、アプリケーションの基本的な情報を提供します。 + +このファイルを用意することで、Farcasterのクローラーは`https://<あなたのドメイン>/.well-known/farcaster/frame.json`というパスでこの情報にアクセスできるようになります。 + +--- + +この設定により、あなたのアプリケーションのURLがFarcasterで共有された際に、Mini Appとして展開される準備が整いました。 + +次のレッスンでは、ゲームのトランザクション結果を表示するコンポーネントを実装します。 + +### 🙋♂️ 質問する + +ここまでの作業で何かわからないことがある場合は、Discordの`#base`で質問をしてください。 + +ヘルプをするときのフローが円滑になるので、エラーレポートには下記の3点を記載してください ✨ + +1. 質問が関連しているセクション番号とレッスン番号 +2. 何をしようとしていたか +3. エラー文をコピー&ペースト +4. エラー画面のスクリーンショット diff --git a/docs/Base/Base-Mini-Shooting-Game/section-3/lesson-4.md b/docs/Base/Base-Mini-Shooting-Game/section-3/lesson-4.md new file mode 100644 index 00000000..2063a89d --- /dev/null +++ b/docs/Base/Base-Mini-Shooting-Game/section-3/lesson-4.md @@ -0,0 +1,198 @@ +--- +title: "トランザクションカードコンポーネントを実装する" +--- + +# Lesson 4: トランザクションカードコンポーネントを実装する + +このレッスンでは、NFTのミント処理(トランザクション)が実行された際に、その結果(成功または失敗)やトランザクションハッシュをユーザーに分かりやすく表示するためのUIコンポーネント、`TransactionCard.tsx`を実装します。 + +## 📦 `components/TransactionCard.tsx`の実装 + +components/TransactionCard.tsx`というファイルを作成し、以下のコードを記述します。 + +```tsx +//components/TransactionCard.tsx + +import { useNotification } from '@coinbase/onchainkit/minikit'; +import { + Transaction, + TransactionButton, + TransactionError, + TransactionResponse, + TransactionStatus, + TransactionStatusAction, + TransactionStatusLabel, + TransactionToast, + TransactionToastAction, + TransactionToastIcon, + TransactionToastLabel, +} from '@coinbase/onchainkit/transaction'; +import { useCallback } from 'react'; +import { Abi } from 'viem'; +import { useAccount } from 'wagmi'; + +type TransactionProps = { + calls: { + address: `0x${string}`; + abi: Abi; + functionName: string; + args: (string | number | bigint | boolean | `0x${string}`)[]; + }[]; +}; + +/** + * トランザクションカードコンポーネント + * @returns + */ +export function TransactionCard({ calls }: TransactionProps) { + const { address } = useAccount(); + + const sendNotification = useNotification(); + + /** + * トランザクションが正常に実行された時に実行するコールバック関数 + */ + const handleSuccess = useCallback( + async (response: TransactionResponse) => { + const transactionHash = response.transactionReceipts[0].transactionHash; + + console.log(`Transaction successful: ${transactionHash}`); + + // トランザクション成功時に MiniKit 通知を送る + await sendNotification({ + title: 'Congratulations!', + body: `You sent your a transaction, ${transactionHash}!`, + }); + }, + [sendNotification] + ); + + return ( + + {address ? ( + console.error('Transaction failed:', error)} + > + + + + + + + + + + + + ) : ( + + Connect your wallet to send a transaction + + )} + + ); +} +``` + +## 📖 コード解説 + +### `useAccount`フックの活用 + +`useOnchainKit`フックから`OnChainKit`で生成されたウォレットアドレスを取得しています。 + +```ts +const { address } = useAccount(); +``` + +### `useNotification`フックの活用 + +`useNotification`フックの`sendNotification`メソッドを使ってトランザクション正常終了時にユーザー向けに通知を送信するようにしています。 + + ```ts +const sendNotification = useNotification(); +``` + +### トランザクションが正常に実行された時に実行するコールバック関数 + +`OnChainKit`のコンポーネント越しに送信したトランザクションが正常に終了した時にユーザー向けに通知を飛ばすようにしています。 + +`sendNotification`メソッドはテンプレプロジェクトで生成されたものをそのまま使っています。 + +```tsx +const handleSuccess = useCallback( + async (response: TransactionResponse) => { + const transactionHash = response.transactionReceipts[0].transactionHash; + + console.log(`Transaction successful: ${transactionHash}`); + + // トランザクション成功時に MiniKit 通知を送る + await sendNotification({ + title: 'Congratulations!', + body: `You sent your a transaction, ${transactionHash}!`, + }); + }, + [sendNotification] +); +``` + +### OnChainKitのTransaction関連コンポーネント + +トランザクションを送信する部分には`OnChainKit`の`Transaction`関連コンポーネントを活用しています。 + +```ts +import { + Transaction, + TransactionButton, + TransactionError, + TransactionResponse, + TransactionStatus, + TransactionStatusAction, + TransactionStatusLabel, + TransactionToast, + TransactionToastAction, + TransactionToastIcon, + TransactionToastLabel, +} from '@coinbase/onchainkit/transaction'; +``` + +これらのコンポーネントを活用するだけですぐにトランザクションを送信するアプリを構築することができます! + +開発者側で用意する必要があるのはコールデータのみです! + +コールデータは前のレッスンで実装した`calls`を渡すようにしています。 + +```ts + console.error('Transaction failed:', error)} +> + + + + + + + + + + + +``` + +--- + +これで、ユーザーにトランザクションを実行するためのUIコンポーネントが実装できました! + +### 🙋♂️ 質問する + +ここまでの作業で何かわからないことがある場合は、Discordの`#base`で質問をしてください。 + +ヘルプをするときのフローが円滑になるので、エラーレポートには下記の3点を記載してください ✨ + +1. 質問が関連しているセクション番号とレッスン番号 +2. 何をしようとしていたか +3. エラー文をコピー&ペースト +4. エラー画面のスクリーンショット diff --git a/docs/Base/Base-Mini-Shooting-Game/section-4/lesson-1.md b/docs/Base/Base-Mini-Shooting-Game/section-4/lesson-1.md new file mode 100644 index 00000000..f074936d --- /dev/null +++ b/docs/Base/Base-Mini-Shooting-Game/section-4/lesson-1.md @@ -0,0 +1,154 @@ +--- +title: "Vercelにデプロイする" +--- + +# Lesson 1: Vercelにデプロイする + +このレッスンでは、完成したNext.jsアプリケーションを**Vercel**にデプロイし、世界中の誰もがアクセスできるようにします。 + +デプロイにはVercel CLI(コマンドラインインタフェース)を使用します。 + +## 🚀 Vercel CLIのインストールとログイン + +まず、Vercel CLIをインストールし、アカウントにログインします。 + +ターミナルで以下のコマンドを実行してください。 + +```bash +pnpm add -g vercel +vercel login +``` + +`vercel login`を実行すると、どの方法でログインするか尋ねられます。 + +GitHubアカウントでのログインがおすすめです。 + +ブラウザが開き、認証が完了すると、CLIでのログインも完了します。 + +## 🌐 デプロイの実行 + +プロジェクトのルートディレクトリ(`Base-Mini-Shooting-Game`)で、以下のコマンドを実行します。 + +```bash +vercel +``` + +初めてこのプロジェクトを`vercel`コマンドでデプロイする場合、いくつか質問されます。 + +1. **Set up and deploy “~/パス/to/your/project”?** -> `Y` +2. **Which scope do you want to deploy to?** -> 自分のVercelアカウント名を選択 +3. **Link to existing project?** -> `N` +4. **What’s your project’s name?** ->(デフォルトのままでOK) +5. **In which directory is your code located?** -> `packages/frontend`と入力 +6. **Want to override the settings?** -> `N` + +これにより、Vercelは`packages/frontend`ディレクトリをNext.jsプロジェクトとして認識し、ビルドとデプロイを自動的に開始します。 + +デプロイが完了すると、ターミナルに複数のURLが表示されます。 + +この時点ではまだプレビュー版のみのデプロイとなります。 + +なので次にプロダクション版のデプロイを行います。 + +```bash +vercel --prod +``` + +問題なくデプロイされればプロダクション版のURLがコンソールに出力されるはずです。 + +そして出力されたURLにデプロイしてシューティングゲームの画面が描画されれば大丈夫です! + + + +## accountAssociationの設定 + +次にaccountAssociationの設定に必要な以下の3つの値を取得します。 + +- FARCASTER_HEADER +- FARCASTER_PAYLOAD +- FARCASTER_SIGNATURE + + + +まず`Farcaster`のWebサイトにアクセスします。 + +そして左側のメニュー欄から`Developers`をクリックします。 + + + +すると以下のような画面が表示されるはずです。 + +このうちの`Manifests`ボタンをクリックします。 + + + +すると以下のような画面に遷移するはずです。 + +ここでvercelにデプロイしたアプリのドメイン情報を入力しましょう! + +例: my-minikit-app-delta.vercel.app + + + +すると以下のように検証が成功し`accountAssociation`のデータが表示されるはずです! + + + +ここで表示されている以下の3つの値を控えておいてください。 + +```json +"accountAssociation": { + "header": "<各々の値に置き換えてください>", + "payload": "<各々の値に置き換えてください>", + "signature": "<各々の値に置き換えてください>" +}, +``` + +## 🔑 環境変数の設定 + +次にVercelの管理画面で環境変数を設定します。 + +これらはデプロイしたアプリをMiniApp化するために絶対に必要なステップとなります。 + +1. [Vercelのダッシュボード](https://vercel.com/dashboard)にアクセスし、デプロイしたプロジェクトを選択します。 +2. 「**Settings**」タブに移動し、左側のメニューから「**Environment Variables**」を選択します。 +3. 以下の内容で5つの環境変数を追加します。 + + - **NEXT_PUBLIC_ONCHAINKIT_API_KEY** + - **KEY**: `NEXT_PUBLIC_ONCHAINKIT_API_KEY` + - **VALUE**: あなたのCDP APIキー(公開キー)を貼り付けます。 + - **NEXT_PUBLIC_URL** + - **KEY**: `NEXT_PUBLIC_URL` + - **VALUE**: Vercelにデプロイして公開されたURLをそのまま入力します。 + - **FARCASTER_HEADER** + - **KEY**: `FARCASTER_HEADER` + - **VALUE**: 上記`"accountAssociation`のうち、`header`の値を貼り付けます + - **FARCASTER_PAYLOAD** + - **KEY**: `FARCASTER_PAYLOAD` + - **VALUE**: 上記`"accountAssociation`のうち、`payload`の値を貼り付けます + - **FARCASTER_SIGNATURE** + - **KEY**: `FARCASTER_SIGNATURE` + - **VALUE**: 上記`"accountAssociation`のうち、`signature`の値を貼り付けます + +4. 「**Save**」ボタンをクリックします。 + +環境変数を追加した後、再度デプロイを行います。 + +「**Deployments**」タブに移動し、最新のデプロイのステータスが「**Ready**」になったら、設定は完了です。 + +--- + +これで、あなたのMini Appはインターネット上に公開されました。 + +次のレッスンでは、この公開URLを使って、FarcasterにMini Appとして認識させるための最終設定を行います。 + +### 🙋♂️ 質問する + +ここまでの作業で何かわからないことがある場合は、Discordの`#base`で質問をしてください。 + +ヘルプをするときのフローが円滑になるので、エラーレポートには下記の3点を記載してください ✨ + +1. 質問が関連しているセクション番号とレッスン番号 +2. 何をしようとしていたか +3. エラー文をコピー&ペースト +4. エラー画面のスクリーンショット diff --git a/docs/Base/Base-Mini-Shooting-Game/section-4/lesson-2.md b/docs/Base/Base-Mini-Shooting-Game/section-4/lesson-2.md new file mode 100644 index 00000000..8b2a0098 --- /dev/null +++ b/docs/Base/Base-Mini-Shooting-Game/section-4/lesson-2.md @@ -0,0 +1,117 @@ +--- +title: "FarcasterでMini Appを起動する" +--- + +# Lesson 2: FarcasterでMini Appを起動する + +ついに最終レッスンです。これまでに作成し、デプロイしたMini AppをFarcaster上で実際に起動し、動作を確認します。 + +## 🚀 Farcasterに投稿(Cast)する + +1. Farcasterクライアント(例: [Warpcast](https://warpcast.com/))を開きます。 +2. 新規投稿画面で、あなたのアプリケーションのVercel URL(例: `https://your-app-name.vercel.app`)を貼り付けます。 +3. URLを貼り付けると、Farcasterが自動的にメタデータを読み込み、Mini Appのプレビューが表示されるはずです。 +4. そのまま「**Cast**」ボタンを押して投稿します。 + + + +すると以下のような投稿画面となるはずです! + + + +**Launch**ボタン押してアプリの動作確認を行なってみましょう! + +起動するとウォレット接続が求められるのでウォレットを選択して接続処理を進めます。 + + + + + +以下のようにシューティングゲームが立ち上がればOKです! ! + + + +## 🔨 Base Buildへの登録 + +では最後に**Base Build**への登録を試してみようと思います! + +[Base Build](https://www.base.dev/)にアクセスします。 + +SignInを行いましょう。 + + + + + +SignInが完了したら以下のような画面に遷移するはずなので`Import your mini app`をクリックして自作したMiniAppをBase Buildに登録しましょう! + + + +App UrlにはVercelにデプロイした時のURLを入力します。 + + + +正常に登録が完了したら以下のように`Farcaster`の右上の`Mini App`に自分のアプリが表示されるようになります! + + + +Mini Appをクリックすると自分のアプリが起動するはずです! ! + + + +## 🎮 動作確認 + +最後に以下の手順で動作を確認しましょう。 + +1. **Mini Appの起動**: フィードに表示されたプレビュー内の「Play Game」ボタンや画像をクリックすると、アプリがフィード内で直接起動します。 +2. **ウォレット接続**: アプリが起動したら、左上の``コンポーネントを使ってウォレットを接続します。 +3. **ゲームプレイ**: ウォレットを接続するとゲーム画面が表示されます。キーボードで操作してゲームをプレイし、敵を倒してスコアを獲得してください。 + + + +4. **NFTミント**: ゲームオーバーになると、スコアと「Mint Score NFT」ボタンが表示されます。ボタンをクリックすると、ウォレットがトランザクションの確認を求めてきます。 + + + +5. **トランザクション確認**: トランザクションを承認すると、`TransactionCard`が表示され、ミント処理の進行状況がわかります。 + + + +6. **ブロックエクスプローラーで確認**: `View on Explorer`のリンクをクリックすると、Basescanが開き、ミントされたNFTのトランザクション詳細を確認できます。倒した敵の数(スコア)と同じIDのNFTが、あなたのアドレスにミントされていれば成功です! + +**NFTマーケットプレイスでの様子** + + + +**ブロックチェーンエクスプローラーの様子** + + + +おめでとうございます! + +これで、Base上で動作するMini Appの開発からデプロイ、公開までの一連の流れをすべて体験しました。 + +## 🌍 プロジェクトをアップグレードする + +[UNCHAIN](https://unchain.tech/) のプロジェクトは [UNCHAIN License](https://github.com/unchain-tech/UNCHAIN-projects/blob/main/LICENSE) により運用されています。 + +プロジェクトに参加していて、「こうすればもっと分かりやすいのに!」「これは間違っている!」と思ったら、ぜひ`pull request`を送ってください。 + +- **新しい敵やアイテムを追加する**: ゲームをより面白くしてみましょう。 +- **リーダーボードを実装する**: スマートコントラクトを改良して、ハイスコアを記録できるようにしてみましょう。 +- **デザインを改善する**: CSSを駆使して、オリジナルのゲーム画面を作成してみましょう。 + +このプロジェクトで学んだ知識を活かして、ぜひあなただけのオリジナルMini App開発に挑戦してみてください! + +--- + +### 🙋♂️ 質問する + +ここまでの作業で何かわからないことがある場合は、Discordの`#base`で質問をしてください。 + +ヘルプをするときのフローが円滑になるので、エラーレポートには下記の3点を記載してください ✨ + +1. 質問が関連しているセクション番号とレッスン番号 +2. 何をしようとしていたか +3. エラー文をコピー&ペースト +4. エラー画面のスクリーンショット diff --git a/public/images/Base-Mini-Shooting-Game/section-1/lesson-0/0.png b/public/images/Base-Mini-Shooting-Game/section-1/lesson-0/0.png new file mode 100644 index 00000000..04b88446 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-1/lesson-0/0.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-1/lesson-0/1.png b/public/images/Base-Mini-Shooting-Game/section-1/lesson-0/1.png new file mode 100644 index 00000000..76408916 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-1/lesson-0/1.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-1/lesson-0/2.png b/public/images/Base-Mini-Shooting-Game/section-1/lesson-0/2.png new file mode 100644 index 00000000..d9049848 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-1/lesson-0/2.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-1/lesson-2/0.png b/public/images/Base-Mini-Shooting-Game/section-1/lesson-2/0.png new file mode 100644 index 00000000..5a0ad5ae Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-1/lesson-2/0.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-1/lesson-2/1.png b/public/images/Base-Mini-Shooting-Game/section-1/lesson-2/1.png new file mode 100644 index 00000000..d457fa36 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-1/lesson-2/1.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-2/lesson-1/0.png b/public/images/Base-Mini-Shooting-Game/section-2/lesson-1/0.png new file mode 100644 index 00000000..d457fa36 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-2/lesson-1/0.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-2/lesson-1/1.png b/public/images/Base-Mini-Shooting-Game/section-2/lesson-1/1.png new file mode 100644 index 00000000..fdbe5c70 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-2/lesson-1/1.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-2/lesson-1/2.png b/public/images/Base-Mini-Shooting-Game/section-2/lesson-1/2.png new file mode 100644 index 00000000..373cac35 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-2/lesson-1/2.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-4/lesson-1/0.png b/public/images/Base-Mini-Shooting-Game/section-4/lesson-1/0.png new file mode 100644 index 00000000..b5cd2ff2 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-4/lesson-1/0.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-4/lesson-1/1.png b/public/images/Base-Mini-Shooting-Game/section-4/lesson-1/1.png new file mode 100644 index 00000000..b08967e5 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-4/lesson-1/1.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-4/lesson-1/2.png b/public/images/Base-Mini-Shooting-Game/section-4/lesson-1/2.png new file mode 100644 index 00000000..2b4801a4 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-4/lesson-1/2.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-4/lesson-1/3.png b/public/images/Base-Mini-Shooting-Game/section-4/lesson-1/3.png new file mode 100644 index 00000000..9220e9c7 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-4/lesson-1/3.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-4/lesson-1/4.png b/public/images/Base-Mini-Shooting-Game/section-4/lesson-1/4.png new file mode 100644 index 00000000..a9c50ac4 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-4/lesson-1/4.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/0.png b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/0.png new file mode 100644 index 00000000..9994f238 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/0.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/1.png b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/1.png new file mode 100644 index 00000000..ffd45bb0 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/1.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/10.png b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/10.png new file mode 100644 index 00000000..76408916 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/10.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/11.png b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/11.png new file mode 100644 index 00000000..d9049848 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/11.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/12.png b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/12.png new file mode 100644 index 00000000..70a393f2 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/12.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/13.png b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/13.png new file mode 100644 index 00000000..ac6ed7e7 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/13.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/14.png b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/14.png new file mode 100644 index 00000000..e3269b37 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/14.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/2.png b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/2.png new file mode 100644 index 00000000..85e88bff Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/2.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/3.png b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/3.png new file mode 100644 index 00000000..76408916 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/3.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/4.png b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/4.png new file mode 100644 index 00000000..467cab05 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/4.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/5.png b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/5.png new file mode 100644 index 00000000..59f803fa Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/5.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/6.png b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/6.png new file mode 100644 index 00000000..6145f637 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/6.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/7.png b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/7.png new file mode 100644 index 00000000..561c910e Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/7.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/8.png b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/8.png new file mode 100644 index 00000000..cbd04514 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/8.png differ diff --git a/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/9.png b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/9.png new file mode 100644 index 00000000..f291b2d8 Binary files /dev/null and b/public/images/Base-Mini-Shooting-Game/section-4/lesson-2/9.png differ diff --git a/public/metadata/Base-Mini-Shooting-Game/description.json b/public/metadata/Base-Mini-Shooting-Game/description.json new file mode 100644 index 00000000..5ca22675 --- /dev/null +++ b/public/metadata/Base-Mini-Shooting-Game/description.json @@ -0,0 +1,9 @@ +{ + "project_id": 1002, + "title": "Base-Mini-Shooting-Game", + "difficulty": "intermediate", + "chain": "Base", + "description": "👉 BaseのMiniAppKitとFarcasterを使ってミニシューティングゲームを作ろう!", + "total_sections": 4, + "total_lessons": 10 +} \ No newline at end of file diff --git a/public/metadata/Base-Mini-Shooting-Game/learn-banner.png b/public/metadata/Base-Mini-Shooting-Game/learn-banner.png new file mode 100644 index 00000000..a6b7435f Binary files /dev/null and b/public/metadata/Base-Mini-Shooting-Game/learn-banner.png differ diff --git a/public/metadata/Sui-zklogin/description.json b/public/metadata/Sui-zklogin/description.json index 3852e36a..4c5b3616 100644 --- a/public/metadata/Sui-zklogin/description.json +++ b/public/metadata/Sui-zklogin/description.json @@ -1,5 +1,5 @@ { - "project_id": 1002, + "project_id": 2001, "title": "Sui-zklogin", "difficulty": "advanced", "chain": "Sui",
Score is 0 — nothing to mint. Try again!
+ Connect your wallet to send a transaction +