From 0ff791dedc20de05aa7eddac6e33f8441a110273 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Fri, 27 Feb 2026 18:54:04 +0100 Subject: [PATCH] feat: add icp-cli support to flying_ninja (Rust + Motoko) - Add icp.yaml with backend and frontend asset canister for both examples - Migrate frontend from @dfinity/* to @icp-sdk/core + @icp-sdk/bindgen - Add actor.js with cookie-based canister env detection - Rewrite vite.config.js with bindgen plugin and dual dev server (icp-cli/dfx) - Commit backend.did for bindgen and add .gitignore for generated files - Add mops.toml [toolchain] section for Motoko, remove unused core dependency - Update README.md and BUILD.md with icp-cli instructions --- motoko/flying_ninja/.gitignore | 1 + motoko/flying_ninja/BUILD.md | 109 ++---------------- motoko/flying_ninja/README.md | 56 ++++++++- motoko/flying_ninja/backend/backend.did | 11 ++ motoko/flying_ninja/frontend/package.json | 11 +- motoko/flying_ninja/frontend/src/Game.jsx | 2 +- motoko/flying_ninja/frontend/src/actor.js | 26 +++++ motoko/flying_ninja/frontend/vite.config.js | 119 ++++++++++++++------ motoko/flying_ninja/icp.yaml | 16 +++ motoko/flying_ninja/mops.toml | 4 +- rust/flying_ninja/.gitignore | 2 + rust/flying_ninja/BUILD.md | 109 ++---------------- rust/flying_ninja/README.md | 57 ++++++++-- rust/flying_ninja/backend/backend.did | 7 ++ rust/flying_ninja/frontend/package.json | 11 +- rust/flying_ninja/frontend/src/Game.jsx | 2 +- rust/flying_ninja/frontend/src/actor.js | 26 +++++ rust/flying_ninja/frontend/vite.config.js | 119 ++++++++++++++------ rust/flying_ninja/icp.yaml | 16 +++ 19 files changed, 407 insertions(+), 297 deletions(-) create mode 100644 motoko/flying_ninja/.gitignore create mode 100644 motoko/flying_ninja/backend/backend.did create mode 100644 motoko/flying_ninja/frontend/src/actor.js create mode 100644 motoko/flying_ninja/icp.yaml create mode 100644 rust/flying_ninja/.gitignore create mode 100644 rust/flying_ninja/backend/backend.did create mode 100644 rust/flying_ninja/frontend/src/actor.js create mode 100644 rust/flying_ninja/icp.yaml diff --git a/motoko/flying_ninja/.gitignore b/motoko/flying_ninja/.gitignore new file mode 100644 index 000000000..7e4a81532 --- /dev/null +++ b/motoko/flying_ninja/.gitignore @@ -0,0 +1 @@ +frontend/src/bindings/ diff --git a/motoko/flying_ninja/BUILD.md b/motoko/flying_ninja/BUILD.md index 24cfcb754..ffb6557ec 100644 --- a/motoko/flying_ninja/BUILD.md +++ b/motoko/flying_ninja/BUILD.md @@ -1,113 +1,26 @@ # Continue building locally -Projects deployed through ICP Ninja are temporary; they will only be live for 20 minutes before they are removed. The command-line tool `dfx` can be used to continue building your ICP Ninja project locally and deploy it to the mainnet. +Projects deployed through ICP Ninja are temporary; they will only be live for 30 minutes before they are removed. To continue building locally, follow these steps. -To migrate your ICP Ninja project off of the web browser and develop it locally, follow these steps. +### 1. Install developer tools -### 1. Install developer tools. - -You can install the developer tools natively or use Dev Containers. - -#### Option 1: Natively install developer tools - -> Installing `dfx` natively is currently only supported on macOS and Linux systems. On Windows, it is recommended to use the Dev Containers option. - -1. Install `dfx` with the following command: - -``` - -sh -ci "$(curl -fsSL https://internetcomputer.org/install.sh)" - -``` - -> On Apple Silicon (e.g., Apple M1 chip), make sure you have Rosetta installed (`softwareupdate --install-rosetta`). - -2. [Install NodeJS](https://nodejs.org/en/download/package-manager). - -3. For Rust projects, you will also need to: - -- Install [Rust](https://doc.rust-lang.org/cargo/getting-started/installation.html#install-rust-and-cargo): `curl https://sh.rustup.rs -sSf | sh` - -- Install [candid-extractor](https://crates.io/crates/candid-extractor): `cargo install candid-extractor` - -4. For Motoko projects, you will also need to: - -- Install the Motoko package manager [Mops](https://docs.mops.one/quick-start#2-install-mops-cli): `npm i -g ic-mops` - -Lastly, navigate into your project's directory that you downloaded from ICP Ninja. - -#### Option 2: Dev Containers - -Continue building your projects locally by installing the [Dev Container extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) for VS Code and [Docker](https://docs.docker.com/engine/install/). - -Make sure Docker is running, then navigate into your project's directory that you downloaded from ICP Ninja and start the Dev Container by selecting `Dev-Containers: Reopen in Container` in VS Code's command palette (F1 or Ctrl/Cmd+Shift+P). - -> Note that local development ports (e.g. the ports used by `dfx` or `vite`) are forwarded from the Dev Container to your local machine. In the VS code terminal, use Ctrl/Cmd+Click on the displayed local URLs to open them in your browser. To view the current port mappings, click the "Ports" tab in the VS Code terminal window. - -### 2. Start the local development environment. - -``` -dfx start --background -``` - -### 3. Create a local developer identity. - -To manage your project's canisters, it is recommended that you create a local [developer identity](https://internetcomputer.org/docs/building-apps/getting-started/identities) rather than use the `dfx` default identity that is not stored securely. - -To create a new identity, run the commands: - -``` - -dfx identity new IDENTITY_NAME - -dfx identity use IDENTITY_NAME - -``` - -Replace `IDENTITY_NAME` with your preferred identity name. The first command `dfx start --background` starts the local `dfx` processes, then `dfx identity new` will create a new identity and return your identity's seed phase. Be sure to save this in a safe, secure location. - -The third command `dfx identity use` will tell `dfx` to use your new identity as the active identity. Any canister smart contracts created after running `dfx identity use` will be owned and controlled by the active identity. - -Your identity will have a principal ID associated with it. Principal IDs are used to identify different entities on ICP, such as users and canisters. - -[Learn more about ICP developer identities](https://internetcomputer.org/docs/building-apps/getting-started/identities). - -### 4. Deploy the project locally. - -Deploy your project to your local developer environment with: - -``` -npm install -dfx deploy +Install [Node.js](https://nodejs.org/en/download/) and [icp-cli](https://cli.icp.build): +```bash +npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm ``` -Your project will be hosted on your local machine. The local canister URLs for your project will be shown in the terminal window as output of the `dfx deploy` command. You can open these URLs in your web browser to view the local instance of your project. - -### 5. Obtain cycles. - -To deploy your project to the mainnet for long-term public accessibility, first you will need [cycles](https://internetcomputer.org/docs/building-apps/getting-started/tokens-and-cycles). Cycles are used to pay for the resources your project uses on the mainnet, such as storage and compute. - -> This cost model is known as ICP's [reverse gas model](https://internetcomputer.org/docs/building-apps/essentials/gas-cost), where developers pay for their project's gas fees rather than users pay for their own gas fees. This model provides an enhanced end user experience since they do not need to hold tokens or sign transactions when using a dapp deployed on ICP. - -> Learn how much a project may cost by using the [pricing calculator](https://internetcomputer.org/docs/building-apps/essentials/cost-estimations-and-examples). +Then navigate into your project's directory that you downloaded from ICP Ninja. -Cycles can be obtained through [converting ICP tokens into cycles using `dfx`](https://internetcomputer.org/docs/building-apps/developer-tools/dfx/dfx-cycles#dfx-cycles-convert). +### 2. Deploy locally -### 6. Deploy to the mainnet. - -Once you have cycles, run the command: - -``` - -dfx deploy --network ic +Start the local network and deploy the project: +```bash +icp network start -d +icp deploy ``` -After your project has been deployed to the mainnet, it will continuously require cycles to pay for the resources it uses. You will need to [top up](https://internetcomputer.org/docs/building-apps/canister-management/topping-up) your project's canisters or set up automatic cycles management through a service such as [CycleOps](https://cycleops.dev/). - -> If your project's canisters run out of cycles, they will be removed from the network. - ## Additional examples Additional code examples and sample applications can be found in the [DFINITY examples repo](https://github.com/dfinity/examples). diff --git a/motoko/flying_ninja/README.md b/motoko/flying_ninja/README.md index 0a170d441..756ef4c63 100644 --- a/motoko/flying_ninja/README.md +++ b/motoko/flying_ninja/README.md @@ -1,19 +1,63 @@ # Flying Ninja +[View this sample's code on GitHub](https://github.com/dfinity/examples/tree/master/motoko/flying_ninja) + +## Overview + Flying Ninja is a 2D side-scroller game where players interact with the flying ninja character using their keyboard's space bar to move up and down. The goal is to avoid the obstacles and obtain points for each obstacle you dodge. When the game ends, the user can add their score to the leaderboard. -## Build and deploy from the command-line +## Deploying from ICP Ninja + +This example can be deployed directly from [ICP Ninja](https://icp.ninja), a browser-based IDE for ICP. To continue developing locally after deploying from ICP Ninja, see [BUILD.md](BUILD.md). + +[![Open in ICP Ninja](https://icp.ninja/assets/open.svg)](https://icp.ninja/i?g=https://github.com/dfinity/examples/motoko/flying_ninja) + +> **Note:** ICP Ninja currently uses `dfx` under the hood, which is why this example includes a `dfx.json` configuration file. `dfx` is the legacy CLI, being superseded by [icp-cli](https://cli.icp.build), which is what developers should use for local development. + +## Build and deploy from the command line + +### Prerequisites + +- [x] Install [Node.js](https://nodejs.org/en/download/) +- [x] Install [icp-cli](https://cli.icp.build): `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` + +### Install + +Clone the example project: -### 1. [Download and install the IC SDK.](https://internetcomputer.org/docs/building-apps/getting-started/install) +```bash +git clone https://github.com/dfinity/examples +cd examples/motoko/flying_ninja +``` + +### Deployment + +Start the local network: + +```bash +icp network start -d +``` -### 2. Download your project from ICP Ninja using the 'Download files' button on the upper left corner, or [clone the GitHub examples repository.](https://github.com/dfinity/examples/) +Deploy the canisters: -### 3. Navigate into the project's directory. +```bash +icp deploy +``` -### 4. Deploy the project to your local environment: +Stop the local network when done: +```bash +icp network stop ``` -dfx start --background --clean && dfx deploy + +## Updating the Candid interface + +The `backend/backend.did` file defines the backend canister's public interface. The frontend TypeScript bindings are auto-generated from this file during the frontend build. + +If you modify the backend's public API, regenerate the `.did` file: + +```bash +$(mops toolchain bin moc) --idl $(mops sources) -o backend/backend.did backend/app.mo ``` ## Security considerations and best practices diff --git a/motoko/flying_ninja/backend/backend.did b/motoko/flying_ninja/backend/backend.did new file mode 100644 index 000000000..f629a4094 --- /dev/null +++ b/motoko/flying_ninja/backend/backend.did @@ -0,0 +1,11 @@ +type LeaderboardEntry = + record { + name: text; + score: nat; + }; +service : { + addLeaderboardEntry: (name: text, score: nat) -> (vec LeaderboardEntry); + getLeaderboard: () -> (vec LeaderboardEntry) query; + getRandomness: () -> (blob); + isHighScore: (score: nat) -> (bool) query; +} diff --git a/motoko/flying_ninja/frontend/package.json b/motoko/flying_ninja/frontend/package.json index 2ea263abe..c98614e9c 100644 --- a/motoko/flying_ninja/frontend/package.json +++ b/motoko/flying_ninja/frontend/package.json @@ -3,23 +3,20 @@ "private": true, "type": "module", "scripts": { - "prebuild": "npm i --include=dev && dfx generate backend", + "prebuild": "npm i --include=dev", "build": "vite build", "dev": "vite" }, "dependencies": { - "@dfinity/agent": "2.4.1", - "@dfinity/auth-client": "2.4.1", - "@dfinity/candid": "2.4.1", - "@dfinity/principal": "2.4.1", + "@icp-sdk/core": "~5.0.0", "react": "18.3.1", "react-dom": "18.3.1" }, "devDependencies": { + "@icp-sdk/bindgen": "~0.2.2", "@types/react": "18.3.12", "@types/react-dom": "18.3.1", "@vitejs/plugin-react": "4.3.3", - "vite": "5.4.11", - "vite-plugin-environment": "1.1.3" + "vite": "5.4.11" } } diff --git a/motoko/flying_ninja/frontend/src/Game.jsx b/motoko/flying_ninja/frontend/src/Game.jsx index 309f666dc..e9d5a5f57 100644 --- a/motoko/flying_ninja/frontend/src/Game.jsx +++ b/motoko/flying_ninja/frontend/src/Game.jsx @@ -3,7 +3,7 @@ import Ninja from './Ninja'; import Pipes from './Pipes'; import Score from './Score'; import Leaderboard from './Leaderboard'; -import { backend } from 'declarations/backend'; +import { backend } from './actor'; import '../index.css'; class SeededRNG { diff --git a/motoko/flying_ninja/frontend/src/actor.js b/motoko/flying_ninja/frontend/src/actor.js new file mode 100644 index 000000000..2f487dbdb --- /dev/null +++ b/motoko/flying_ninja/frontend/src/actor.js @@ -0,0 +1,26 @@ +import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"; +import { createActor } from "./bindings/backend"; + +// The ic_env cookie is set by the asset canister (SDK ≥0.30.2) on all HTML +// responses. It contains the replica root key and any PUBLIC_* canister +// environment variables. In dev mode the vite dev server sets the same cookie +// via Set-Cookie header (see vite.config.js). +const canisterEnv = safeGetCanisterEnv(); + +// Resolve canister ID: cookie (icp-cli + dev server) → env var (dfx build-time) +const canisterId = + canisterEnv?.["PUBLIC_CANISTER_ID:backend"] ?? + process.env.CANISTER_ID_BACKEND; + +if (!canisterId) { + throw new Error( + "Canister ID for 'backend' not found. Run 'icp deploy' or 'dfx deploy' first." + ); +} + +export const backend = createActor(canisterId, { + agentOptions: { + host: window.location.origin, + rootKey: canisterEnv?.IC_ROOT_KEY, + }, +}); diff --git a/motoko/flying_ninja/frontend/vite.config.js b/motoko/flying_ninja/frontend/vite.config.js index f9e04a9a9..b5ac17930 100644 --- a/motoko/flying_ninja/frontend/vite.config.js +++ b/motoko/flying_ninja/frontend/vite.config.js @@ -1,37 +1,88 @@ -import react from '@vitejs/plugin-react'; -import { defineConfig } from 'vite'; -import { fileURLToPath, URL } from 'url'; -import environment from 'vite-plugin-environment'; +import { defineConfig, loadEnv } from "vite"; +import react from "@vitejs/plugin-react"; +import { execSync } from "child_process"; +import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite"; -export default defineConfig({ - base: './', - plugins: [react(), environment('all', { prefix: 'CANISTER_' }), environment('all', { prefix: 'DFX_' })], - envDir: '../', - define: { - 'process.env': process.env - }, - optimizeDeps: { - esbuildOptions: { - define: { - global: 'globalThis' - } - } - }, - resolve: { - alias: [ - { - find: 'declarations', - replacement: fileURLToPath(new URL('../src/declarations', import.meta.url)) - } - ] - }, - server: { - proxy: { - '/api': { - target: 'http://127.0.0.1:4943', - changeOrigin: true - } +function getDevServerConfig() { + // Try icp-cli first + try { + const canisterId = execSync("icp canister status backend -e local -i", { + encoding: "utf-8", + stdio: "pipe", + }).trim(); + const networkStatus = JSON.parse( + execSync("icp network status --json", { + encoding: "utf-8", + stdio: "pipe", + }) + ); + return { + headers: { + "Set-Cookie": `ic_env=${encodeURIComponent( + `ic_root_key=${networkStatus.root_key}&PUBLIC_CANISTER_ID:backend=${canisterId}` + )}; SameSite=Lax;`, + }, + proxy: { + "/api": { target: "http://127.0.0.1:8000", changeOrigin: true }, + }, + }; + } catch {} + + // Try dfx + try { + const pingResult = JSON.parse( + execSync("dfx ping", { encoding: "utf-8", stdio: "pipe" }) + ); + const rootKeyHex = Buffer.from(pingResult.root_key).toString("hex"); + const canisterId = execSync("dfx canister id backend", { + encoding: "utf-8", + stdio: "pipe", + }).trim(); + return { + headers: { + "Set-Cookie": `ic_env=${encodeURIComponent( + `ic_root_key=${rootKeyHex}&PUBLIC_CANISTER_ID:backend=${canisterId}` + )}; SameSite=Lax;`, + }, + proxy: { + "/api": { + target: "http://127.0.0.1:4943", + changeOrigin: true, + }, + }, + host: "127.0.0.1", + }; + } catch {} + + throw new Error( + "No local network running. Start with:\n icp network start -d && icp deploy\nor:\n dfx start --background && dfx deploy" + ); +} + +export default defineConfig(({ command, mode }) => { + // dfx generates ../.env with CANISTER_ID_* vars on deploy. Bake them into the + // bundle so actor.js can fall back to them when the ic_env cookie does not + // contain canister IDs (dfx does not inject PUBLIC_CANISTER_ID:* env vars + // into the asset canister, unlike icp-cli). + const env = loadEnv(mode, "..", ["CANISTER_"]); + + return { + base: "./", + plugins: [ + react(), + icpBindgen({ + didFile: "../backend/backend.did", + outDir: "./src/bindings", + }), + ], + define: { + "process.env.CANISTER_ID_BACKEND": JSON.stringify( + env.CANISTER_ID_BACKEND + ), + }, + optimizeDeps: { + esbuildOptions: { define: { global: "globalThis" } }, }, - host: '127.0.0.1' - } + server: command === "serve" ? getDevServerConfig() : undefined, + }; }); diff --git a/motoko/flying_ninja/icp.yaml b/motoko/flying_ninja/icp.yaml new file mode 100644 index 000000000..a4edd3a0e --- /dev/null +++ b/motoko/flying_ninja/icp.yaml @@ -0,0 +1,16 @@ +canisters: + - name: backend + recipe: + type: "@dfinity/motoko@v4.1.0" + configuration: + main: backend/app.mo + candid: backend/backend.did + + - name: frontend + recipe: + type: "@dfinity/asset-canister@v2.1.0" + configuration: + dir: frontend/dist + build: + - npm install --prefix frontend + - npm run build --prefix frontend diff --git a/motoko/flying_ninja/mops.toml b/motoko/flying_ninja/mops.toml index 2f5e2e7bf..fe5d7a32a 100644 --- a/motoko/flying_ninja/mops.toml +++ b/motoko/flying_ninja/mops.toml @@ -1,5 +1,5 @@ -# Motoko dependencies (https://mops.one/) +[toolchain] +moc = "1.2.0" [dependencies] base = "0.14.9" -core = "1.0.0" diff --git a/rust/flying_ninja/.gitignore b/rust/flying_ninja/.gitignore new file mode 100644 index 000000000..85f31a82c --- /dev/null +++ b/rust/flying_ninja/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +frontend/src/bindings/ diff --git a/rust/flying_ninja/BUILD.md b/rust/flying_ninja/BUILD.md index 24cfcb754..ffb6557ec 100644 --- a/rust/flying_ninja/BUILD.md +++ b/rust/flying_ninja/BUILD.md @@ -1,113 +1,26 @@ # Continue building locally -Projects deployed through ICP Ninja are temporary; they will only be live for 20 minutes before they are removed. The command-line tool `dfx` can be used to continue building your ICP Ninja project locally and deploy it to the mainnet. +Projects deployed through ICP Ninja are temporary; they will only be live for 30 minutes before they are removed. To continue building locally, follow these steps. -To migrate your ICP Ninja project off of the web browser and develop it locally, follow these steps. +### 1. Install developer tools -### 1. Install developer tools. - -You can install the developer tools natively or use Dev Containers. - -#### Option 1: Natively install developer tools - -> Installing `dfx` natively is currently only supported on macOS and Linux systems. On Windows, it is recommended to use the Dev Containers option. - -1. Install `dfx` with the following command: - -``` - -sh -ci "$(curl -fsSL https://internetcomputer.org/install.sh)" - -``` - -> On Apple Silicon (e.g., Apple M1 chip), make sure you have Rosetta installed (`softwareupdate --install-rosetta`). - -2. [Install NodeJS](https://nodejs.org/en/download/package-manager). - -3. For Rust projects, you will also need to: - -- Install [Rust](https://doc.rust-lang.org/cargo/getting-started/installation.html#install-rust-and-cargo): `curl https://sh.rustup.rs -sSf | sh` - -- Install [candid-extractor](https://crates.io/crates/candid-extractor): `cargo install candid-extractor` - -4. For Motoko projects, you will also need to: - -- Install the Motoko package manager [Mops](https://docs.mops.one/quick-start#2-install-mops-cli): `npm i -g ic-mops` - -Lastly, navigate into your project's directory that you downloaded from ICP Ninja. - -#### Option 2: Dev Containers - -Continue building your projects locally by installing the [Dev Container extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) for VS Code and [Docker](https://docs.docker.com/engine/install/). - -Make sure Docker is running, then navigate into your project's directory that you downloaded from ICP Ninja and start the Dev Container by selecting `Dev-Containers: Reopen in Container` in VS Code's command palette (F1 or Ctrl/Cmd+Shift+P). - -> Note that local development ports (e.g. the ports used by `dfx` or `vite`) are forwarded from the Dev Container to your local machine. In the VS code terminal, use Ctrl/Cmd+Click on the displayed local URLs to open them in your browser. To view the current port mappings, click the "Ports" tab in the VS Code terminal window. - -### 2. Start the local development environment. - -``` -dfx start --background -``` - -### 3. Create a local developer identity. - -To manage your project's canisters, it is recommended that you create a local [developer identity](https://internetcomputer.org/docs/building-apps/getting-started/identities) rather than use the `dfx` default identity that is not stored securely. - -To create a new identity, run the commands: - -``` - -dfx identity new IDENTITY_NAME - -dfx identity use IDENTITY_NAME - -``` - -Replace `IDENTITY_NAME` with your preferred identity name. The first command `dfx start --background` starts the local `dfx` processes, then `dfx identity new` will create a new identity and return your identity's seed phase. Be sure to save this in a safe, secure location. - -The third command `dfx identity use` will tell `dfx` to use your new identity as the active identity. Any canister smart contracts created after running `dfx identity use` will be owned and controlled by the active identity. - -Your identity will have a principal ID associated with it. Principal IDs are used to identify different entities on ICP, such as users and canisters. - -[Learn more about ICP developer identities](https://internetcomputer.org/docs/building-apps/getting-started/identities). - -### 4. Deploy the project locally. - -Deploy your project to your local developer environment with: - -``` -npm install -dfx deploy +Install [Node.js](https://nodejs.org/en/download/) and [icp-cli](https://cli.icp.build): +```bash +npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm ``` -Your project will be hosted on your local machine. The local canister URLs for your project will be shown in the terminal window as output of the `dfx deploy` command. You can open these URLs in your web browser to view the local instance of your project. - -### 5. Obtain cycles. - -To deploy your project to the mainnet for long-term public accessibility, first you will need [cycles](https://internetcomputer.org/docs/building-apps/getting-started/tokens-and-cycles). Cycles are used to pay for the resources your project uses on the mainnet, such as storage and compute. - -> This cost model is known as ICP's [reverse gas model](https://internetcomputer.org/docs/building-apps/essentials/gas-cost), where developers pay for their project's gas fees rather than users pay for their own gas fees. This model provides an enhanced end user experience since they do not need to hold tokens or sign transactions when using a dapp deployed on ICP. - -> Learn how much a project may cost by using the [pricing calculator](https://internetcomputer.org/docs/building-apps/essentials/cost-estimations-and-examples). +Then navigate into your project's directory that you downloaded from ICP Ninja. -Cycles can be obtained through [converting ICP tokens into cycles using `dfx`](https://internetcomputer.org/docs/building-apps/developer-tools/dfx/dfx-cycles#dfx-cycles-convert). +### 2. Deploy locally -### 6. Deploy to the mainnet. - -Once you have cycles, run the command: - -``` - -dfx deploy --network ic +Start the local network and deploy the project: +```bash +icp network start -d +icp deploy ``` -After your project has been deployed to the mainnet, it will continuously require cycles to pay for the resources it uses. You will need to [top up](https://internetcomputer.org/docs/building-apps/canister-management/topping-up) your project's canisters or set up automatic cycles management through a service such as [CycleOps](https://cycleops.dev/). - -> If your project's canisters run out of cycles, they will be removed from the network. - ## Additional examples Additional code examples and sample applications can be found in the [DFINITY examples repo](https://github.com/dfinity/examples). diff --git a/rust/flying_ninja/README.md b/rust/flying_ninja/README.md index 1eb106ad4..965858977 100644 --- a/rust/flying_ninja/README.md +++ b/rust/flying_ninja/README.md @@ -1,25 +1,64 @@ -# Flying ninja +# Flying Ninja + +[View this sample's code on GitHub](https://github.com/dfinity/examples/tree/master/rust/flying_ninja) + +## Overview Flying Ninja is a 2D side-scroller game where players interact with the flying ninja character using their keyboard's spacebar to move the ninja character up and down. The goal is to avoid the obstacles and obtain points for each obstacle you dodge. When the game ends, the user can add their score to the leaderboard. ## Deploying from ICP Ninja -When viewing this project in ICP Ninja, you can deploy it directly to the mainnet for free by clicking "Run" in the upper right corner. Open this project in ICP Ninja: +This example can be deployed directly from [ICP Ninja](https://icp.ninja), a browser-based IDE for ICP. To continue developing locally after deploying from ICP Ninja, see [BUILD.md](BUILD.md). + +[![Open in ICP Ninja](https://icp.ninja/assets/open.svg)](https://icp.ninja/i?g=https://github.com/dfinity/examples/rust/flying_ninja) -[![](https://icp.ninja/assets/open.svg)](https://icp.ninja/i?g=https://github.com/dfinity/examples/rust/flying_ninja) +> **Note:** ICP Ninja currently uses `dfx` under the hood, which is why this example includes a `dfx.json` configuration file. `dfx` is the legacy CLI, being superseded by [icp-cli](https://cli.icp.build), which is what developers should use for local development. -## Build and deploy from the command-line +## Build and deploy from the command line -### 1. [Download and install the IC SDK.](https://internetcomputer.org/docs/building-apps/getting-started/install) +### Prerequisites -### 2. Download your project from ICP Ninja using the 'Download files' button on the upper left corner, or [clone the GitHub examples repository.](https://github.com/dfinity/examples/) +- [x] Install [Node.js](https://nodejs.org/en/download/) +- [x] Install [icp-cli](https://cli.icp.build): `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` -### 3. Navigate into the project's directory. +### Install -### 4. Deploy the project to your local environment: +Clone the example project: +```bash +git clone https://github.com/dfinity/examples +cd examples/rust/flying_ninja ``` -dfx start --background --clean && dfx deploy + +### Deployment + +Start the local network: + +```bash +icp network start -d +``` + +Deploy the canisters: + +```bash +icp deploy +``` + +Stop the local network when done: + +```bash +icp network stop +``` + +## Updating the Candid interface + +The `backend/backend.did` file defines the backend canister's public interface. The frontend TypeScript bindings are auto-generated from this file during the frontend build. + +If you modify the backend's public API, rebuild the canister and regenerate the `.did` file: + +```bash +icp build backend +candid-extractor target/wasm32-unknown-unknown/release/backend.wasm > backend/backend.did ``` ## Security considerations and best practices diff --git a/rust/flying_ninja/backend/backend.did b/rust/flying_ninja/backend/backend.did new file mode 100644 index 000000000..085d5d0ae --- /dev/null +++ b/rust/flying_ninja/backend/backend.did @@ -0,0 +1,7 @@ +type LeaderboardEntry = record { name : text; score : nat }; +service : { + add_leaderboard_entry : (text, nat) -> (vec LeaderboardEntry); + get_leaderboard : () -> (vec LeaderboardEntry) query; + get_randomness : () -> (blob); + is_high_score : (nat) -> (bool) query; +} diff --git a/rust/flying_ninja/frontend/package.json b/rust/flying_ninja/frontend/package.json index 2ea263abe..c98614e9c 100644 --- a/rust/flying_ninja/frontend/package.json +++ b/rust/flying_ninja/frontend/package.json @@ -3,23 +3,20 @@ "private": true, "type": "module", "scripts": { - "prebuild": "npm i --include=dev && dfx generate backend", + "prebuild": "npm i --include=dev", "build": "vite build", "dev": "vite" }, "dependencies": { - "@dfinity/agent": "2.4.1", - "@dfinity/auth-client": "2.4.1", - "@dfinity/candid": "2.4.1", - "@dfinity/principal": "2.4.1", + "@icp-sdk/core": "~5.0.0", "react": "18.3.1", "react-dom": "18.3.1" }, "devDependencies": { + "@icp-sdk/bindgen": "~0.2.2", "@types/react": "18.3.12", "@types/react-dom": "18.3.1", "@vitejs/plugin-react": "4.3.3", - "vite": "5.4.11", - "vite-plugin-environment": "1.1.3" + "vite": "5.4.11" } } diff --git a/rust/flying_ninja/frontend/src/Game.jsx b/rust/flying_ninja/frontend/src/Game.jsx index 1afcecc12..b7f157184 100644 --- a/rust/flying_ninja/frontend/src/Game.jsx +++ b/rust/flying_ninja/frontend/src/Game.jsx @@ -3,7 +3,7 @@ import Ninja from './Ninja'; import Pipes from './Pipes'; import Score from './Score'; import Leaderboard from './Leaderboard'; -import { backend } from 'declarations/backend'; +import { backend } from './actor'; import '../index.css'; class SeededRNG { diff --git a/rust/flying_ninja/frontend/src/actor.js b/rust/flying_ninja/frontend/src/actor.js new file mode 100644 index 000000000..2f487dbdb --- /dev/null +++ b/rust/flying_ninja/frontend/src/actor.js @@ -0,0 +1,26 @@ +import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"; +import { createActor } from "./bindings/backend"; + +// The ic_env cookie is set by the asset canister (SDK ≥0.30.2) on all HTML +// responses. It contains the replica root key and any PUBLIC_* canister +// environment variables. In dev mode the vite dev server sets the same cookie +// via Set-Cookie header (see vite.config.js). +const canisterEnv = safeGetCanisterEnv(); + +// Resolve canister ID: cookie (icp-cli + dev server) → env var (dfx build-time) +const canisterId = + canisterEnv?.["PUBLIC_CANISTER_ID:backend"] ?? + process.env.CANISTER_ID_BACKEND; + +if (!canisterId) { + throw new Error( + "Canister ID for 'backend' not found. Run 'icp deploy' or 'dfx deploy' first." + ); +} + +export const backend = createActor(canisterId, { + agentOptions: { + host: window.location.origin, + rootKey: canisterEnv?.IC_ROOT_KEY, + }, +}); diff --git a/rust/flying_ninja/frontend/vite.config.js b/rust/flying_ninja/frontend/vite.config.js index f9e04a9a9..b5ac17930 100644 --- a/rust/flying_ninja/frontend/vite.config.js +++ b/rust/flying_ninja/frontend/vite.config.js @@ -1,37 +1,88 @@ -import react from '@vitejs/plugin-react'; -import { defineConfig } from 'vite'; -import { fileURLToPath, URL } from 'url'; -import environment from 'vite-plugin-environment'; +import { defineConfig, loadEnv } from "vite"; +import react from "@vitejs/plugin-react"; +import { execSync } from "child_process"; +import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite"; -export default defineConfig({ - base: './', - plugins: [react(), environment('all', { prefix: 'CANISTER_' }), environment('all', { prefix: 'DFX_' })], - envDir: '../', - define: { - 'process.env': process.env - }, - optimizeDeps: { - esbuildOptions: { - define: { - global: 'globalThis' - } - } - }, - resolve: { - alias: [ - { - find: 'declarations', - replacement: fileURLToPath(new URL('../src/declarations', import.meta.url)) - } - ] - }, - server: { - proxy: { - '/api': { - target: 'http://127.0.0.1:4943', - changeOrigin: true - } +function getDevServerConfig() { + // Try icp-cli first + try { + const canisterId = execSync("icp canister status backend -e local -i", { + encoding: "utf-8", + stdio: "pipe", + }).trim(); + const networkStatus = JSON.parse( + execSync("icp network status --json", { + encoding: "utf-8", + stdio: "pipe", + }) + ); + return { + headers: { + "Set-Cookie": `ic_env=${encodeURIComponent( + `ic_root_key=${networkStatus.root_key}&PUBLIC_CANISTER_ID:backend=${canisterId}` + )}; SameSite=Lax;`, + }, + proxy: { + "/api": { target: "http://127.0.0.1:8000", changeOrigin: true }, + }, + }; + } catch {} + + // Try dfx + try { + const pingResult = JSON.parse( + execSync("dfx ping", { encoding: "utf-8", stdio: "pipe" }) + ); + const rootKeyHex = Buffer.from(pingResult.root_key).toString("hex"); + const canisterId = execSync("dfx canister id backend", { + encoding: "utf-8", + stdio: "pipe", + }).trim(); + return { + headers: { + "Set-Cookie": `ic_env=${encodeURIComponent( + `ic_root_key=${rootKeyHex}&PUBLIC_CANISTER_ID:backend=${canisterId}` + )}; SameSite=Lax;`, + }, + proxy: { + "/api": { + target: "http://127.0.0.1:4943", + changeOrigin: true, + }, + }, + host: "127.0.0.1", + }; + } catch {} + + throw new Error( + "No local network running. Start with:\n icp network start -d && icp deploy\nor:\n dfx start --background && dfx deploy" + ); +} + +export default defineConfig(({ command, mode }) => { + // dfx generates ../.env with CANISTER_ID_* vars on deploy. Bake them into the + // bundle so actor.js can fall back to them when the ic_env cookie does not + // contain canister IDs (dfx does not inject PUBLIC_CANISTER_ID:* env vars + // into the asset canister, unlike icp-cli). + const env = loadEnv(mode, "..", ["CANISTER_"]); + + return { + base: "./", + plugins: [ + react(), + icpBindgen({ + didFile: "../backend/backend.did", + outDir: "./src/bindings", + }), + ], + define: { + "process.env.CANISTER_ID_BACKEND": JSON.stringify( + env.CANISTER_ID_BACKEND + ), + }, + optimizeDeps: { + esbuildOptions: { define: { global: "globalThis" } }, }, - host: '127.0.0.1' - } + server: command === "serve" ? getDevServerConfig() : undefined, + }; }); diff --git a/rust/flying_ninja/icp.yaml b/rust/flying_ninja/icp.yaml new file mode 100644 index 000000000..da0587413 --- /dev/null +++ b/rust/flying_ninja/icp.yaml @@ -0,0 +1,16 @@ +canisters: + - name: backend + recipe: + type: "@dfinity/rust@v3.2.0" + configuration: + package: backend + candid: backend/backend.did + + - name: frontend + recipe: + type: "@dfinity/asset-canister@v2.1.0" + configuration: + dir: frontend/dist + build: + - npm install --prefix frontend + - npm run build --prefix frontend