diff --git a/.devcontainer/CODESPACE.md b/.devcontainer/CODESPACE.md new file mode 100644 index 000000000..5e68b9dde --- /dev/null +++ b/.devcontainer/CODESPACE.md @@ -0,0 +1,33 @@ +# ICP Codespace + +> **Setup in progress:** The ICP local network is starting and canisters are being deployed automatically. Check the terminal panel for status. Access URLs will be printed once deployment is complete. + +The network starts and canisters are deployed automatically when this Codespace opens. Access URLs are printed in the terminal once deployment completes. + +## Access URLs + +`icp deploy` prints URLs using `localhost:8000`, which do not work inside a Codespace. Run the following command to get the correct forwarded URLs: + +```bash +bash /workspaces/examples/.devcontainer/scripts/show-urls.sh +``` + +## Deploy / Redeploy + +Deploys or redeploys all canisters, preserving their state. This also runs automatically every time the Codespace starts. + +```bash +icp deploy +``` + +## Reset & Redeploy + +Reinstalls all canisters from scratch, wiping their state. The network keeps running. + +```bash +icp deploy --mode reinstall -y +``` + +## Note for non-SPA frontends + +Frontend URLs use a `?canisterId=` query parameter for routing. This works correctly for single-page apps (all navigation stays client-side). If your frontend uses real path-based navigation where clicking a link triggers a new browser request (e.g. navigating to `/page2`), the query parameter will be dropped and the gateway will not know which canister to serve. Subdomain-based routing is not available in Codespaces because GitHub's TLS certificate only covers one subdomain level, making `.-8000.app.github.dev` invalid. diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..efc0c7ef2 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,31 @@ +{ + "name": "ICP Examples (Motoko + Rust)", + "image": "ghcr.io/marc0olo/icp-dev-env-all:dev", + "workspaceFolder": "/workspaces/examples", + "forwardPorts": [8000, 5173], + "portsAttributes": { + "8000": { + "label": "ICP", + "onAutoForward": "ignore" + }, + "5173": { + "label": "Vite", + "onAutoForward": "openBrowser" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "dfinity-foundation.vscode-motoko", + "rust-lang.rust-analyzer" + ], + "settings": { + "git.openRepositoryInParentFolders": "always", + "workbench.startupEditor": "none", + "workbench.editorAssociations": { + "*.md": "vscode.markdown.preview.editor" + } + } + } + } +} diff --git a/.devcontainer/motoko-hello-world/devcontainer.json b/.devcontainer/motoko-hello-world/devcontainer.json new file mode 100644 index 000000000..4566d6ece --- /dev/null +++ b/.devcontainer/motoko-hello-world/devcontainer.json @@ -0,0 +1,32 @@ +{ + "name": "Hello World (Motoko)", + "image": "ghcr.io/marc0olo/icp-dev-env-motoko:dev", + "workspaceFolder": "/workspaces/examples/motoko/hello_world", + "forwardPorts": [8000, 5173], + "portsAttributes": { + "8000": { + "label": "ICP", + "onAutoForward": "ignore" + }, + "5173": { + "label": "Vite", + "onAutoForward": "openBrowser" + } + }, + "postStartCommand": "bash /workspaces/examples/.devcontainer/scripts/postStart.sh", + "postAttachCommand": "bash /workspaces/examples/.devcontainer/scripts/postAttach.sh", + "customizations": { + "vscode": { + "extensions": [ + "dfinity-foundation.vscode-motoko" + ], + "settings": { + "git.openRepositoryInParentFolders": "always", + "workbench.startupEditor": "none", + "workbench.editorAssociations": { + "*.md": "vscode.markdown.preview.editor" + } + } + } + } +} diff --git a/.devcontainer/motoko-who-am-i/devcontainer.json b/.devcontainer/motoko-who-am-i/devcontainer.json new file mode 100644 index 000000000..3ff50e8d8 --- /dev/null +++ b/.devcontainer/motoko-who-am-i/devcontainer.json @@ -0,0 +1,32 @@ +{ + "name": "Who Am I? (Motoko)", + "image": "ghcr.io/marc0olo/icp-dev-env-motoko:dev", + "workspaceFolder": "/workspaces/examples/motoko/who_am_i", + "forwardPorts": [8000, 5173], + "portsAttributes": { + "8000": { + "label": "ICP", + "onAutoForward": "ignore" + }, + "5173": { + "label": "Vite", + "onAutoForward": "openBrowser" + } + }, + "postStartCommand": "bash /workspaces/examples/.devcontainer/scripts/postStart.sh", + "postAttachCommand": "bash /workspaces/examples/.devcontainer/scripts/postAttach.sh", + "customizations": { + "vscode": { + "extensions": [ + "dfinity-foundation.vscode-motoko" + ], + "settings": { + "git.openRepositoryInParentFolders": "always", + "workbench.startupEditor": "none", + "workbench.editorAssociations": { + "*.md": "vscode.markdown.preview.editor" + } + } + } + } +} diff --git a/.devcontainer/rust-hello-world/devcontainer.json b/.devcontainer/rust-hello-world/devcontainer.json new file mode 100644 index 000000000..9676252c6 --- /dev/null +++ b/.devcontainer/rust-hello-world/devcontainer.json @@ -0,0 +1,32 @@ +{ + "name": "Hello World (Rust)", + "image": "ghcr.io/marc0olo/icp-dev-env-rust:dev", + "workspaceFolder": "/workspaces/examples/rust/hello_world", + "forwardPorts": [8000, 5173], + "portsAttributes": { + "8000": { + "label": "ICP", + "onAutoForward": "ignore" + }, + "5173": { + "label": "Vite", + "onAutoForward": "openBrowser" + } + }, + "postStartCommand": "bash /workspaces/examples/.devcontainer/scripts/postStart.sh", + "postAttachCommand": "bash /workspaces/examples/.devcontainer/scripts/postAttach.sh", + "customizations": { + "vscode": { + "extensions": [ + "rust-lang.rust-analyzer" + ], + "settings": { + "git.openRepositoryInParentFolders": "always", + "workbench.startupEditor": "none", + "workbench.editorAssociations": { + "*.md": "vscode.markdown.preview.editor" + } + } + } + } +} diff --git a/.devcontainer/rust-who-am-i/devcontainer.json b/.devcontainer/rust-who-am-i/devcontainer.json new file mode 100644 index 000000000..ba2260fc5 --- /dev/null +++ b/.devcontainer/rust-who-am-i/devcontainer.json @@ -0,0 +1,32 @@ +{ + "name": "Who Am I? (Rust)", + "image": "ghcr.io/marc0olo/icp-dev-env-rust:dev", + "workspaceFolder": "/workspaces/examples/rust/who_am_i", + "forwardPorts": [8000, 5173], + "portsAttributes": { + "8000": { + "label": "ICP", + "onAutoForward": "ignore" + }, + "5173": { + "label": "Vite", + "onAutoForward": "openBrowser" + } + }, + "postStartCommand": "bash /workspaces/examples/.devcontainer/scripts/postStart.sh", + "postAttachCommand": "bash /workspaces/examples/.devcontainer/scripts/postAttach.sh", + "customizations": { + "vscode": { + "extensions": [ + "rust-lang.rust-analyzer" + ], + "settings": { + "git.openRepositoryInParentFolders": "always", + "workbench.startupEditor": "none", + "workbench.editorAssociations": { + "*.md": "vscode.markdown.preview.editor" + } + } + } + } +} diff --git a/.devcontainer/scripts/postAttach.sh b/.devcontainer/scripts/postAttach.sh new file mode 100755 index 000000000..2f23ceec7 --- /dev/null +++ b/.devcontainer/scripts/postAttach.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e + +code CODESPACE.md + +echo "Deploying canisters..." +icp deploy +echo "" +echo "Access URLs:" +echo "" +bash /workspaces/examples/.devcontainer/scripts/show-urls.sh diff --git a/.devcontainer/scripts/postStart.sh b/.devcontainer/scripts/postStart.sh new file mode 100755 index 000000000..d51d228c8 --- /dev/null +++ b/.devcontainer/scripts/postStart.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -e + +# In GitHub Codespaces, inject the forwarded domain into icp.yaml so the +# HTTP gateway accepts requests with that Host header. +if [ -n "$CODESPACE_NAME" ]; then + DOMAIN="${CODESPACE_NAME}-8000.app.github.dev" + DEV_DOMAIN="${CODESPACE_NAME}-5173.app.github.dev" + node -e " + const fs = require('fs'); + const content = fs.readFileSync('icp.yaml', 'utf8'); + if (!content.includes('gateway:')) { + const gateway = ' gateway:\n domains:\n - localhost\n - ${DOMAIN}\n - ${DEV_DOMAIN}\n'; + let updated; + if (content.includes(' ii: true\n')) { + updated = content.replace(' ii: true\n', ' ii: true\n' + gateway); + } else if (content.includes(' mode: managed\n')) { + updated = content.replace(' mode: managed\n', ' mode: managed\n' + gateway); + } else { + updated = content.trimEnd() + '\nnetworks:\n - name: local\n mode: managed\n' + gateway; + } + fs.writeFileSync('icp.yaml', updated); + } + " +fi + +icp network start -d diff --git a/.devcontainer/scripts/show-urls.sh b/.devcontainer/scripts/show-urls.sh new file mode 100755 index 000000000..ddd4bb628 --- /dev/null +++ b/.devcontainer/scripts/show-urls.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +CANISTER_JSON=$(icp canister status --json 2>/dev/null) +NETWORK_JSON=$(icp network status --json 2>/dev/null) +CANDID_UI_ID=$(echo "$NETWORK_JSON" | jq -r '.candid_ui_principal // ""') + +if [ -n "$CODESPACE_NAME" ]; then + BASE="https://${CODESPACE_NAME}-8000.app.github.dev" +else + BASE="http://localhost:8000" +fi + +FRONTEND_NAMES=$(icp project show 2>/dev/null | node -e ' + let data = ""; + process.stdin.on("data", c => data += c); + process.stdin.on("end", () => { + const lines = data.split("\n"); + const frontends = []; + let inCanisters = false, currentCanister = null; + for (const line of lines) { + if (/^\S/.test(line)) { + inCanisters = (line === "canisters:"); + currentCanister = null; + continue; + } + if (!inCanisters) continue; + const m = line.match(/^ ([a-z][a-z0-9_-]*):\s*$/); + if (m) { currentCanister = m[1]; continue; } + if (currentCanister && !frontends.includes(currentCanister)) { + if (line.includes("asset-canister") || /^\s+type:\s+assets\s*$/.test(line)) { + frontends.push(currentCanister); + } + } + } + process.stdout.write(frontends.join("\n") + (frontends.length ? "\n" : "")); + }); +') + +echo "$CANISTER_JSON" | while IFS= read -r entry; do + ID=$(echo "$entry" | jq -r '.id') + NAME=$(echo "$entry" | jq -r '.name') + if echo "$FRONTEND_NAMES" | grep -qx "$NAME"; then + echo " $NAME: ${BASE}/?canisterId=${ID}" + elif [ -n "$CANDID_UI_ID" ]; then + echo " $NAME (Candid UI): ${BASE}/?canisterId=${CANDID_UI_ID}&id=${ID}" + fi +done diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ed0a23cf4..96cf519ac 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,7 +1,5 @@ # Contributing -> For ICP Ninja: check [NINJA_CONTRIBUTING.md](./../NINJA_CONTRIBUTING.md) for how to contribute a project to ICP Ninja. - Thank you for your interest in contributing to example apps for the Internet Computer. By participating in this project, you agree to abide by our [Code of Conduct](./CODE_OF_CONDUCT.md). @@ -116,4 +114,4 @@ To open a new issue: ### Submitting your own example -We're not accepting community examples at this time -- we have something better planned. +See [ADDING_AN_EXAMPLE.md](./../ADDING_AN_EXAMPLE.md) for guidelines on how to structure and submit a new example. diff --git a/.github/workflow-template.yml b/.github/workflow-template.yml new file mode 100644 index 000000000..e7b244c20 --- /dev/null +++ b/.github/workflow-template.yml @@ -0,0 +1,35 @@ +# Workflow template for ICP examples. +# Copy this file to .github/workflows/.yml and replace the placeholders. +# +# PLACEHOLDERS: +# e.g. hello_world +# motoko | rust +# ghcr.io/dfinity/icp-dev-env-motoko | ghcr.io/dfinity/icp-dev-env-rust + +name: + +on: + push: + branches: + - master + pull_request: + paths: + - //** + - .github/workflows/.yml + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + -: + runs-on: ubuntu-24.04 + container: + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Deploy and test + working-directory: / + run: | + icp network start -d + icp deploy + make test diff --git a/.github/workflows/hello_world.yml b/.github/workflows/hello_world.yml new file mode 100644 index 000000000..13d7bec8a --- /dev/null +++ b/.github/workflows/hello_world.yml @@ -0,0 +1,40 @@ +name: hello_world + +on: + push: + branches: + - master + pull_request: + paths: + - motoko/hello_world/** + - rust/hello_world/** + - .github/workflows/hello_world.yml + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + motoko-hello_world: + runs-on: ubuntu-24.04 + container: ghcr.io/marc0olo/icp-dev-env-motoko:dev + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Deploy and test + working-directory: motoko/hello_world + run: | + icp network start -d + icp deploy + make test + + rust-hello_world: + runs-on: ubuntu-24.04 + container: ghcr.io/marc0olo/icp-dev-env-rust:dev + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Deploy and test + working-directory: rust/hello_world + run: | + icp network start -d + icp deploy + make test diff --git a/.github/workflows/ninja_pr_checks.yml b/.github/workflows/ninja_pr_checks.yml index a5a39e7f7..b4bc032a7 100644 --- a/.github/workflows/ninja_pr_checks.yml +++ b/.github/workflows/ninja_pr_checks.yml @@ -4,6 +4,50 @@ on: pull_request: branches: - master + paths: + - 'hosting/**' + - 'motoko/backend_only/**' + - 'motoko/canister_logs/**' + - 'motoko/classes/**' + - 'motoko/basic_bitcoin/**' + - 'motoko/daily_planner/**' + - 'motoko/evm_block_explorer/**' + - 'motoko/filevault/**' + - 'motoko/flying_ninja/**' + - 'motoko/llm_chatbot/**' + - 'motoko/query_stats/**' + - 'motoko/send_http_get/**' + - 'motoko/send_http_post/**' + - 'motoko/superheroes/**' + - 'motoko/threshold-ecdsa/**' + - 'motoko/threshold-schnorr/**' + - 'rust/backend_only/**' + - 'rust/backend_wasm64/**' + - 'rust/basic_bitcoin/**' + - 'rust/canister-info/**' + - 'rust/canister_logs/**' + - 'rust/basic_ethereum/**' + - 'rust/candid_type_generation/**' + - 'rust/daily_planner/**' + - 'rust/evm_block_explorer/**' + - 'rust/flying_ninja/**' + - 'rust/guards/**' + - 'rust/llm_chatbot/**' + - 'rust/performance_counters/**' + - 'rust/periodic_tasks/**' + - 'rust/qrcode/**' + - 'rust/query_stats/**' + - 'rust/send_http_get/**' + - 'rust/send_http_post/**' + - 'rust/simd/**' + - 'rust/threshold-ecdsa/**' + - 'rust/unit_testable_rust_canister/**' + - 'rust/photo_gallery/**' + - 'rust/inter-canister-calls/**' + - 'rust/x509/**' + - 'rust/receiving-icp/**' + - 'rust/exchange-rates/**' + - '.github/workflows/ninja_pr_checks.yml' concurrency: group: ninja-pr-checks-${{ github.workflow }}-${{ github.ref }} @@ -41,7 +85,6 @@ jobs: ["EVM Block Explorer (Motoko)"]="motoko/evm_block_explorer" ["FileVault (Motoko)"]="motoko/filevault" ["Flying Ninja (Motoko)"]="motoko/flying_ninja" - ["Hello World (Motoko)"]="motoko/hello_world" ["LLM Chatbot (Motoko)"]="motoko/llm_chatbot" ["Query Stats (Motoko)"]="motoko/query_stats" ["Send HTTP Get (Motoko)"]="motoko/send_http_get" @@ -49,7 +92,6 @@ jobs: ["Superheroes (Motoko)"]="motoko/superheroes" ["Threshold ECDSA (Motoko)"]="motoko/threshold-ecdsa" ["Threshold Schnorr (Motoko)"]="motoko/threshold-schnorr" - ["Who Am I (Motoko)"]="motoko/who_am_i" ["Rust backend (Rust)"]="rust/backend_only" ["Rust backend Wasm64 (Rust)"]="rust/backend_wasm64" ["Basic Bitcoin (Rust)"]="rust/basic_bitcoin" @@ -61,7 +103,6 @@ jobs: ["EVM Block Explorer (Rust)"]="rust/evm_block_explorer" ["Flying Ninja (Rust)"]="rust/flying_ninja" ["Guards (Rust)"]="rust/guards" - ["Hello World (Rust)"]="rust/hello_world" ["LLM Chatbot (Rust)"]="rust/llm_chatbot" ["Performance Counters (Rust)"]="rust/performance_counters" ["Periodic Tasks (Rust)"]="rust/periodic_tasks" @@ -72,7 +113,6 @@ jobs: ["SIMD (Rust)"]="rust/simd" ["Threshold ECDSA (Rust)"]="rust/threshold-ecdsa" ["Unit Testable Canister (Rust)"]="rust/unit_testable_rust_canister" - ["Who Am I (Rust)"]="rust/who_am_i" ["Photo Gallery (Rust)"]="rust/photo_gallery" ["Inter-canister calls (Rust)"]="rust/inter-canister-calls" ["X.509 (Rust)"]="rust/x509" diff --git a/.github/workflows/who_am_i.yml b/.github/workflows/who_am_i.yml new file mode 100644 index 000000000..ad845724a --- /dev/null +++ b/.github/workflows/who_am_i.yml @@ -0,0 +1,40 @@ +name: who_am_i + +on: + push: + branches: + - master + pull_request: + paths: + - motoko/who_am_i/** + - rust/who_am_i/** + - .github/workflows/who_am_i.yml + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + motoko-who_am_i: + runs-on: ubuntu-24.04 + container: ghcr.io/marc0olo/icp-dev-env-motoko:dev + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Deploy and test + working-directory: motoko/who_am_i + run: | + icp network start -d + icp deploy + make test + + rust-who_am_i: + runs-on: ubuntu-24.04 + container: ghcr.io/marc0olo/icp-dev-env-rust:dev + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Deploy and test + working-directory: rust/who_am_i + run: | + icp network start -d + icp deploy + make test diff --git a/ADDING_AN_EXAMPLE.md b/ADDING_AN_EXAMPLE.md index 2aa0a0529..084af2565 100644 --- a/ADDING_AN_EXAMPLE.md +++ b/ADDING_AN_EXAMPLE.md @@ -1,40 +1,44 @@ # How to add a new example -> For ICP Ninja: check [NINJA_CONTRIBUTING.md](./NINJA_CONTRIBUTING.md) for how to contribute a project to ICP Ninja. - Each example should be available in both Rust and Motoko variations, implementing the same Candid interface (and, ideally, semantics). -To illustrate the pattern, this repo now contains one such example, project `hello_world`: +To illustrate the pattern, this repo contains one such example, project `hello_world`: -`motoko/hello_world` -`rust/hello_world` +``` +motoko/hello_world +rust/hello_world +``` -When adding a new `dfx` generated project, make sure to delete its GitHub metadata files (`.gitignore`, `.git` etc). +When adding a new project, make sure to delete any generated GitHub metadata files (`.gitignore`, `.git` etc). Each project should include a language-specific README.md that also links to the corresponding README.md of its counterpart in another language, making it easy for language-curious readers to explore both implementations. -## CI +## Codespaces -Apart from the standard `dfx` material, each project should provide a `Makefile` used by GitHub Actions CI to run (very) basic tests. +To make the example available in GitHub Codespaces, add a devcontainer config under `.devcontainer/-/devcontainer.json` pointing to the appropriate image: -For each example, there is a single CI file with four build actions to produce Darwin and Linux builds and tests of the Motoko/Rust, projects, such as: +- Motoko: `ghcr.io/dfinity/icp-dev-env-motoko` +- Rust: `ghcr.io/dfinity/icp-dev-env-rust` -``` -.github/workflows/hello_world.yml -``` +Add a Codespaces badge to the example's README pointing to the new devcontainer config. See the existing `who_am_i` examples for reference. -Implementing the GitHub action will ensure it runs in CI and helps keep examples in sync with releases of `dfx`. +## CI -## Documentation +Each project should provide a `Makefile` with a `test` target that runs basic canister tests using `icp canister call`. Each example also needs a GitHub Actions workflow file at `.github/workflows/.yml`. -For your new example to be included in the ICP developer documentation, make sure you update the `samples` submodule in the portal repository to point to the latest commit in this examples repository using the following command: +Use the workflow template as a starting point: -```bash -git submodule update --remote submodules/samples ``` +.github/workflow-template.yml +``` + +Copy it, replace the placeholders, and add the appropriate container image: + +- Motoko: `ghcr.io/dfinity/icp-dev-env-motoko` +- Rust: `ghcr.io/dfinity/icp-dev-env-rust` -After you run this command, commit the changes to a new PR to have them merged into the portal repo. +See `hello_world` and `who_am_i` for reference implementations. Workflows run on Linux only using container images — no provision scripts needed. -## Issues +## Notes -While this structure leads to some duplication (especially shared components like frontend code) it ensures that Motoko users can focus solely on Motoko-specific content, and likewise for Rust users. It also enables easily finding language-specific examples when a given use case is not easily supported in the other language. +While this structure leads to some duplication (especially shared frontend code) it ensures that Motoko users can focus solely on Motoko-specific content, and likewise for Rust users. It also enables easily finding language-specific examples when a given use case is not easily supported in the other language. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..549b11622 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,391 @@ +# Agent Instructions + +Guidelines for AI agents (Claude, Codex, Cursor, Copilot, etc.) working in this repository. + +## Skills — fetch before working + +ICP skills are live reference documents maintained by DFINITY. Always fetch the relevant skills **before** making changes — do not rely on cached or training-data versions. + +**Index:** `https://skills.internetcomputer.org/.well-known/skills/index.json` + +Fetch the skill content at its `url` field. Skills relevant to this repo: + +| Task | Skill to fetch | +|------|---------------| +| Any ICP project work, `icp.yaml`, canister lifecycle | `icp-cli` | +| Migrating an example from dfx to icp-cli | `icp-cli` + its `references/dfx-migration.md` file | +| Motoko canister code | `motoko` | +| `mops.toml`, toolchain pinning, moc flags | `mops-cli` | +| Internet Identity integration | `internet-identity` | +| Frontend asset canister | `asset-canister` | + +Skills take precedence over general knowledge when both cover the same topic. + +--- + +## Repository overview + +This repo contains canonical ICP examples, each available in both Motoko and Rust. Every example lives under two sibling directories: + +``` +motoko// +rust// +``` + +Both implement the same Candid interface so readers can compare language implementations side by side. + +--- + +## Toolchain + +- **Always use `icp-cli`** for all ICP operations. Never use `dfx`. +- CLI docs: https://cli.internetcomputer.org +- ICP developer docs: https://docs.internetcomputer.org + +--- + +## Canonical example structure + +Follow the `hello_world` layout. New examples and migrations should use this pattern: + +``` +// +├── icp.yaml # canister definitions (icp-cli project file) +├── Makefile # must contain a `test` target +├── README.md +├── CODESPACE.md # symlink or copy from .devcontainer/CODESPACE.md +├── package.json # npm workspaces root pointing to frontend/ +├── mops.toml # Motoko only +├── Cargo.toml # Rust only (workspace) +├── rust-toolchain.toml # Rust only +├── backend/ +│ ├── app.mo or lib.rs # canister entry point +│ └── backend.did # Candid interface (source of truth) +└── frontend/ + ├── index.html + ├── package.json + ├── vite.config.js + ├── src/ + │ ├── actor.js # icp-sdk actor wiring + │ ├── App.jsx + │ └── main.jsx + └── dist/ # build output (gitignored, rebuilt by icp deploy) +``` + +> **Note:** `who_am_i` uses `src/backend/` and `src/frontend/` for historical reasons. +> New examples and migrations should use the flat `backend/` / `frontend/` layout above. + +### What NOT to include + +- `dfx.json` — dfx project file, not used with icp-cli +- `BUILD.md` — ICP Ninja artifact +- `.dfx/` — dfx state directory +- `src/bindings/` — auto-generated by the bindgen Vite plugin, must be gitignored +- Committed `dist/` output — built by `icp deploy`; never commit pre-built assets + +--- + +## icp.yaml + +### Motoko + +```yaml +networks: # omit if no Internet Identity needed + - name: local + mode: managed + ii: true + +canisters: + - name: backend + recipe: + type: "@dfinity/motoko@vX.Y.Z" # see pending items; currently pinned to a commit SHA + configuration: + name: backend # must match [canisters.backend] key in mops.toml + + - name: frontend + recipe: + type: "@dfinity/asset-canister@v2.1.0" + configuration: + dir: frontend/dist + build: + - npm install --prefix frontend + - npm run build --prefix frontend +``` + +### Rust + +```yaml +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 +``` + +**Canister names are always `backend` and `frontend`.** Never use names like `_backend`, `internet_identity_app_backend`, etc. + +--- + +## mops.toml (Motoko) + +```toml +[toolchain] +moc = "1.8.2" + +[dependencies] +core = "2.5.0" + +[moc] +# M0236: use context dot notation +# M0237: redundant explicit implicit arguments +# M0223: redundant type instantiation +args = ["--default-persistent-actors", "-W=M0236,M0237,M0223"] + +[canisters.backend] +main = "backend/app.mo" +candid = "backend/backend.did" +``` + +`[canisters.]` replaces the `main`, `candid`, and `args` fields that were previously in `icp.yaml`. The `name` key must match the `name` in the recipe configuration. `--default-persistent-actors` makes all actors persistent by default, so the `persistent` keyword is not needed in source files. + +--- + +## Cargo.toml (Rust) + +Root workspace: +```toml +[workspace] +members = ["backend"] +resolver = "2" +``` + +`backend/Cargo.toml`: +```toml +[package] +name = "backend" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +candid = "0.10" +ic-cdk = "0.20" +``` + +--- + +## Makefile + +Every example must have a `Makefile` with a `test` target that exercises the deployed canister via `icp canister call`: + +```makefile +.PHONY: test + +test: + @echo "--- Testing ---" + @result=$$(icp canister call backend '') && \ + echo "$$result" && \ + echo "$$result" | grep -q '' && \ + echo "PASS" || (echo "FAIL" && exit 1) +``` + +- Tests must call the `backend` canister by that name. +- Use `grep -q` to assert on output content. +- Each assertion is a separate echo block for clarity. + +--- + +## Devcontainer config + +Add a file at `.devcontainer/-/devcontainer.json`: + +```json +{ + "name": " ()", + "image": "ghcr.io/dfinity/icp-dev-env-:", + "workspaceFolder": "/workspaces/examples//", + "forwardPorts": [8000, 5173], + "portsAttributes": { + "8000": { "label": "ICP", "onAutoForward": "ignore" }, + "5173": { "label": "Vite", "onAutoForward": "openBrowser" } + }, + "postStartCommand": "bash /workspaces/examples/.devcontainer/scripts/postStart.sh", + "postAttachCommand": "bash /workspaces/examples/.devcontainer/scripts/postAttach.sh", + "customizations": { + "vscode": { + "extensions": ["dfinity-foundation.vscode-motoko"], + "settings": { + "git.openRepositoryInParentFolders": "always", + "workbench.startupEditor": "none", + "workbench.editorAssociations": { "*.md": "vscode.markdown.preview.editor" } + } + } + } +} +``` + +- Motoko extension: `dfinity-foundation.vscode-motoko` +- Rust extension: `rust-lang.rust-analyzer` +- The `postStart` and `postAttach` scripts are shared across all examples — do not create per-example scripts. +- `forwardPorts` and `portsAttributes` are only needed for examples with a frontend. + +Also add a Codespaces badge to the example's README pointing to the new devcontainer config: + +```markdown +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/dfinity/examples?devcontainer_path=.devcontainer%2F-%2Fdevcontainer.json&ref=master) +``` + +--- + +## CI workflow + +Copy `.github/workflow-template.yml` to `.github/workflows/.yml` and fill in the placeholders. A single workflow file covers both language variants: + +```yaml +name: + +on: + push: + branches: [master] + pull_request: + paths: + - motoko//** + - rust//** + - .github/workflows/.yml + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + motoko-: + runs-on: ubuntu-24.04 + container: ghcr.io/dfinity/icp-dev-env-motoko: + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Deploy and test + working-directory: motoko/ + run: | + icp network start -d + icp deploy + make test + + rust-: + runs-on: ubuntu-24.04 + container: ghcr.io/dfinity/icp-dev-env-rust: + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Deploy and test + working-directory: rust/ + run: | + icp network start -d + icp deploy + make test +``` + +- Linux only, no macOS runners. +- No provision scripts — toolchain comes from the container image. +- Always include the `concurrency` block to cancel superseded runs. +- Pin the `actions/checkout` SHA and annotate it with the version tag. + +--- + +## README structure + +Each example's README should follow this structure: + +```markdown +# + +[View this sample's code on GitHub]() + +## Overview +<2-3 sentences describing what the example demonstrates> + +## Try in browser + + +## Build and deploy from the command line + +### Prerequisites +- [ ] Install Node.js +- [ ] Install icp-cli: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` + +### Install + + +### Deploy + + +## Updating the Candid interface + + +## Security considerations and best practices + +``` + +- Codespaces badge URL must include `ref=master` (or the appropriate branch). +- Security best practices URL: `https://docs.internetcomputer.org/guides/security/overview` +- Each README links to its counterpart in the other language. + +--- + +## Pending items (do not resolve prematurely) + +### Container images +All devcontainer configs and CI workflows currently reference `ghcr.io/marc0olo/icp-dev-env-*:dev` (a personal fork used while images are being prepared for the official dfinity org). When images are published at `ghcr.io/dfinity/icp-dev-env-*`, update every occurrence in: +- `.devcontainer/*/devcontainer.json` +- `.github/workflows/*.yml` + +Also note: `icp-dev-env-all` is a new combined Motoko+Rust image (for the root devcontainer) that does not yet exist in the dfinity org. + +### Motoko recipe version +Both `motoko/who_am_i/icp.yaml` and `motoko/hello_world/icp.yaml` pin a specific commit SHA of the Motoko recipe to pick up `[moc] args` support from `mops.toml` before it ships in a stable release: + +```yaml +type: https://raw.githubusercontent.com/dfinity/icp-cli-recipes/bc9581d9258d2d7feb15ab4ae8d04baf923b985f/recipes/motoko/recipe.hbs +``` + +Tracked in: https://github.com/dfinity/icp-cli-recipes/pull/26 + +Once that PR merges and a new `@dfinity/motoko` version is released, replace the raw URL in both files with the versioned tag (e.g. `@dfinity/motoko@vX.Y.Z`). + +--- + +## dfx → icp-cli migration checklist + +When migrating an existing example: + +- [ ] Replace `dfx.json` with `icp.yaml` using the canonical structure above +- [ ] Rename canisters to `backend` and `frontend` +- [ ] Rename `src//` to `backend/` (or `src/backend/` if keeping the `src/` layout) +- [ ] Rename `src//` to `frontend/` (or `src/frontend/`) +- [ ] Rename `.did` file to `backend.did` +- [ ] Update `Cargo.toml` package name to `backend` (Rust) +- [ ] Update `Cargo.lock` package name entry (Rust) +- [ ] Update workspace member path in root `Cargo.toml` (Rust) +- [ ] Update `vite.config.js`: canister name, `didFile` path, env var names, remove dfx fallback +- [ ] Update `actor.js`: import path, `PUBLIC_CANISTER_ID:backend`, `CANISTER_ID_BACKEND` +- [ ] Update root `package.json` workspace path to `frontend/` +- [ ] Update `.gitignore` bindings path to `frontend/src/bindings/` +- [ ] Update `mops.toml` to current toolchain versions (Motoko) +- [ ] Delete `dfx.json`, `BUILD.md`, `.dfx/`, `.env` (dfx-generated) +- [ ] Add `Makefile` with `test` target +- [ ] Add devcontainer config under `.devcontainer/-/` +- [ ] Add CI workflow under `.github/workflows/.yml` +- [ ] Add Codespaces badge to README +- [ ] Update README deploy instructions to use `icp-cli` +- [ ] Update README Candid regeneration command to use `icp-cli` / `mops` toolchain diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..bdc9873c0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,3 @@ +# Claude Code Instructions + +Read and follow all guidelines in [AGENTS.md](AGENTS.md) before making any changes to this repository. diff --git a/NINJA_CONTRIBUTING.md b/NINJA_CONTRIBUTING.md deleted file mode 100644 index 8c7a6e393..000000000 --- a/NINJA_CONTRIBUTING.md +++ /dev/null @@ -1,151 +0,0 @@ -# Contributing a project to ICP Ninja - -We recommend to build your example directly within ICP Ninja, such that it starts out with the correct tooling, structure and configs. -If you do that, your project will naturally be in the correct format and can be easily added to ICP Ninja. -Once the example is done, you can download the source files from ICP Ninja. -Alternatively, you can start with an existing Ninja project from this repo (see CODEOWNERS file for Ninja examples, e.g. `motoko/hello_world` or `rust/hello_world`) and modify it. - -Ideally, your project should have a frontend and backend. - -### Where to place your example -* `/motoko` for a Motoko project -* `/rust` for a Rust project -* `/hosting` for a frontend-only project - -### Compilation requirements -* Make sure the project compiles and runs with `dfx deploy` inside the `ghcr.io/dfinity/icp-dev-env-slim:22` container. -* Make sure there are no custom scripts for building or running the project, as ICP Ninja has no terminal! -* Make sure `npm run dev` works and the canister can be called through the browser (if applicable, this is if users download the project and run it locally). -* Make sure II login works (if applicable). -* If you use Rust, make sure the project has `ic_cdk::export_candid!();` in the `lib.rs` file, such that the Candid interface can be auto derived. -* If you use Motoko, use Mops as the package manager. - -## Preparing the PR -1. Add your project in the `CODEOWNERS` file, with your team as codeowner. -2. Add your project to the matrix in `.github/workflows/ninja_pr_checks.yml` to run PR tests. -3. Add a `README.md` file, copy the `BUILD.md` and `devcontainer.json` files. -4. Request review from the `@dfinity/ninja-devs` team if it is not added automatically. - -## Submit a PR to the ICP Ninja repo -1. Add your newly added project to `frontend/public/projects.json` -2. Bump the commit hash in `submodules/examples` to a commit hash after your PR has been merged into the examples repo. -3. Ask the Ninja team or AI to give you a beautiful image for your project - -## Templates - -### Recommended `dfx.json` for a Rust canister: - -```json -{ - "canisters": { - "backend": { - "candid": "backend/backend.did", - "type": "custom", - "shrink": true, - "gzip": true, - "wasm": "target/wasm32-unknown-unknown/release/backend.wasm", - "build": [ - "cargo build --target wasm32-unknown-unknown --release -p backend", - "candid-extractor target/wasm32-unknown-unknown/release/backend.wasm > backend/backend.did" - ], - "metadata": [ - { - "name": "candid:service" - } - ] - } - }, - "output_env_file": ".env" -} -``` - -### Recommended `dfx.json` for a Motoko canister: - -```json -{ - "canisters": { - "backend": { - "main": "backend/app.mo", - "type": "motoko", - "args": "--enhanced-orthogonal-persistence" - }, - }, - "output_env_file": ".env", - "defaults": { - "build": { - "packtool": "mops sources" - } - } -} - -``` - -### Recommended `dfx.json` for a frontend/asset canister: - -```json -{ - "canisters": { - "frontend": { - "dependencies": ["backend"], - "frontend": { - "entrypoint": "frontend/index.html" - }, - "source": ["frontend/dist"], - "type": "assets" - } - }, - "output_env_file": ".env" -} -``` - -### Recommended `package.json` -Make sure to install all packages necessary in the prebuild step of your `package.json`, e.g., with - -```js -"scripts": { - "prebuild": "npm i --include=dev && ...", - ... - }, -``` - -### Recommended `vite.config.js` - -```js -import react from '@vitejs/plugin-react'; -import { defineConfig } from 'vite'; -import { fileURLToPath, URL } from 'url'; -import environment from 'vite-plugin-environment'; - -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 - } - }, - host: '127.0.0.1' - } -}); -``` diff --git a/README.md b/README.md index 191a5d00b..6d39e1b69 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Internet Computer sample applications +# Internet Computer sample applications Get started building on ICP with the sample applications in this repository. From this repository, you can deploy, download, clone, fork, or share sample projects. @@ -14,7 +14,7 @@ Code samples are organized by programming language: - [Rust](https://github.com/dfinity/examples/tree/master/rust) - [C](https://github.com/dfinity/examples/tree/master/c) -Some examples include frontends written in a variety of frameworks, such as React, JavaScript, etc. +Some examples include frontends written in a variety of frameworks such as React, JavaScript, etc. Additional frontend samples can be found in the following folders: @@ -22,45 +22,41 @@ Additional frontend samples can be found in the following folders: - [HTML](https://github.com/dfinity/examples/tree/master/hosting) - [Unity](https://github.com/dfinity/examples/tree/master/native-apps) -## Deploying samples +## Try in browser -### ICP Ninja +Many examples include a GitHub Codespaces badge in their README. Clicking it opens a pre-configured environment with the ICP toolchain installed — the local network starts and canisters are deployed automatically. No local setup required. -You can open and deploy examples with [ICP Ninja](https://icp.ninja/), a web-based tool that allows you to create and manage Internet Computer projects without downloading any tools or setting up a local environment. +Browse all your Codespaces at [github.com/codespaces](https://github.com/codespaces). -To contribute an example that will be featured on ICP Ninja, check out the [NINJA_CONTRIBUTING.md](./NINJA_CONTRIBUTING.md) file. +## Local development -### GitHub Codespaces or Gitpod +### Dev Containers -This repo can be opened in a web-based developer environment such as [GitHub Codespaces](https://github.com/codespaces) or [Gitpod](https://www.gitpod.io/), allowing you to edit and deploy the sample projects without downloading any tools or setting up a local environment. +Open the repo root in [VS Code](https://code.visualstudio.com/) with the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) to get a pre-configured environment with the full ICP toolchain for both Motoko and Rust. VS Code will prompt you to reopen in the container automatically. -[Get started with GitHub codespaces](https://internetcomputer.org/docs/current/developer-docs/developer-tools/ide/codespaces). - -[Get started with Gitpod](https://internetcomputer.org/docs/current/developer-docs/developer-tools/ide/gitpod). - -### dfx - -dfx is a command-line tool used to create, deploy. and manage projects on ICP. To download and use dfx with this examples repo, run the following commands locally (macOS/Linux systems): - -``` +```bash git clone https://github.com/dfinity/examples.git -cd examples -sh -ci "$(curl -fsSL https://internetcomputer.org/install.sh)" ``` -Then, navigate into the folder of the sample that you want to use and follow the project's README instructions to setup and deploy the sample code. +Then navigate into an example and follow its README to deploy. +> **Note:** The per-example devcontainer configs are designed for GitHub Codespaces. For local Dev Container use, always open the repo root. -## Resources +### Command line -- [ICP Developer Docs](https://internetcomputer.org/docs/current/home) +Install [icp-cli](https://cli.internetcomputer.org), clone the repo, navigate into an example, and follow its README: -- [Overview of ICP](https://internetcomputer.org/docs/current/developer-docs/getting-started/overview-of-icp) +```bash +git clone https://github.com/dfinity/examples.git +cd examples// +``` -- [Installing dfx](https://internetcomputer.org/docs/current/developer-docs/getting-started/install/) +## Resources -- [Developer tools](https://internetcomputer.org/docs/current/developer-docs/developer-tools/dev-tools-overview) +- [Quickstart](https://docs.internetcomputer.org/getting-started/quickstart) +- [Developer tools](https://docs.internetcomputer.org/developer-tools) +- [icp-cli](https://cli.internetcomputer.org) ## Security considerations and best practices -If you base your application on one of these examples, we recommend you familiarize yourself with and adhere to the [security best practices](https://internetcomputer.org/docs/current/references/security/) for developing on the Internet Computer. The examples provided here may not implement all the best practices. +If you base your application on one of these examples, we recommend you familiarize yourself with and adhere to the [security best practices](https://docs.internetcomputer.org/guides/security/overview) for developing on the Internet Computer. The examples provided here may not implement all the best practices. diff --git a/motoko/hello_world/.devcontainer/devcontainer.json b/motoko/hello_world/.devcontainer/devcontainer.json deleted file mode 100644 index ebb0b8bcc..000000000 --- a/motoko/hello_world/.devcontainer/devcontainer.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "ICP Dev Environment", - "image": "ghcr.io/dfinity/icp-dev-env-slim:22", - "forwardPorts": [4943, 5173], - "portsAttributes": { - "4943": { - "label": "dfx", - "onAutoForward": "ignore" - }, - "5173": { - "label": "vite", - "onAutoForward": "openBrowser" - } - }, - "customizations": { - "vscode": { - "extensions": ["dfinity-foundation.vscode-motoko"] - } - } -} diff --git a/motoko/hello_world/BUILD.md b/motoko/hello_world/BUILD.md deleted file mode 100644 index 8877d3ed2..000000000 --- a/motoko/hello_world/BUILD.md +++ /dev/null @@ -1,26 +0,0 @@ -# Continue building locally - -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. - -### 1. Install developer tools - -Install [Node.js](https://nodejs.org/en/download/) and [icp-cli](https://cli.internetcomputer.org): - -```bash -npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm -``` - -Then navigate into your project's directory that you downloaded from ICP Ninja. - -### 2. Deploy locally - -Start the local network and deploy the project: - -```bash -icp network start -d -icp deploy -``` - -## 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/hello_world/CODESPACE.md b/motoko/hello_world/CODESPACE.md new file mode 120000 index 000000000..17b02d2b4 --- /dev/null +++ b/motoko/hello_world/CODESPACE.md @@ -0,0 +1 @@ +../../.devcontainer/CODESPACE.md \ No newline at end of file diff --git a/motoko/hello_world/Makefile b/motoko/hello_world/Makefile new file mode 100644 index 000000000..7f538e890 --- /dev/null +++ b/motoko/hello_world/Makefile @@ -0,0 +1,15 @@ +.PHONY: test + +test: + @echo "--- Testing default greeting ---" + @result=$$(icp canister call backend greet '("World")') && \ + echo "$$result" && \ + echo "$$result" | grep -q 'Hello, World!' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "--- Testing setGreeting ---" + @icp canister call backend setGreeting '("Hi, ")' + @result=$$(icp canister call backend greet '("Alice")') && \ + echo "$$result" && \ + echo "$$result" | grep -q 'Hi, Alice!' && \ + echo "PASS" || (echo "FAIL" && exit 1) diff --git a/motoko/hello_world/README.md b/motoko/hello_world/README.md index 4afb8738b..c369722e6 100644 --- a/motoko/hello_world/README.md +++ b/motoko/hello_world/README.md @@ -21,13 +21,11 @@ The `/backend` folder contains the Motoko canister, `app.mo`. The `/frontend` fo Edit the `mops.toml` file to add [Motoko dependencies](https://mops.one/) to the project. -## Deploying from ICP Ninja +## Try in browser -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 GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/dfinity/examples?devcontainer_path=.devcontainer%2Fmotoko-hello-world%2Fdevcontainer.json&ref=feat%2Fcodespaces) -[![Open in ICP Ninja](https://icp.ninja/assets/open.svg)](https://icp.ninja/i?g=https://github.com/dfinity/examples/motoko/hello_world) - -> **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.internetcomputer.org), which is what developers should use for local development. +Opens a pre-configured environment with the ICP toolchain installed. The local network starts and canisters are deployed automatically. You can browse all your Codespaces at [github.com/codespaces](https://github.com/codespaces). ## Build and deploy from the command line @@ -47,34 +45,23 @@ cd examples/motoko/hello_world ### Deployment -Start the local network: +Start the local network and deploy: ```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, regenerate the `.did` file using the Motoko compiler: +If you modify the backend's public API, regenerate the `.did` file: ```bash -$(mops toolchain bin moc) --idl -o backend/backend.did backend/app.mo +mops build backend --idl ``` ## Security considerations and best practices -If you base your application on this example, it is recommended that you familiarize yourself with and adhere to the [security best practices](https://docs.internetcomputer.org/building-apps/security/overview) for developing on ICP. This example may not implement all the best practices. +If you base your application on this example, it is recommended that you familiarize yourself with and adhere to the [security best practices](https://docs.internetcomputer.org/guides/security/overview) for developing on ICP. This example may not implement all the best practices. diff --git a/motoko/hello_world/backend/app.mo b/motoko/hello_world/backend/app.mo index 7bbd4f6f3..6e434246b 100644 --- a/motoko/hello_world/backend/app.mo +++ b/motoko/hello_world/backend/app.mo @@ -1,4 +1,4 @@ -persistent actor HelloWorld { +actor HelloWorld { // We store the greeting in a stable variable such that it gets persisted over canister upgrades. var greeting : Text = "Hello, "; diff --git a/motoko/hello_world/dfx.json b/motoko/hello_world/dfx.json deleted file mode 100644 index d073ee09f..000000000 --- a/motoko/hello_world/dfx.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "canisters": { - "backend": { - "main": "backend/app.mo", - "type": "motoko", - "args": "--enhanced-orthogonal-persistence" - }, - "frontend": { - "dependencies": ["backend"], - "frontend": { - "entrypoint": "frontend/index.html" - }, - "source": ["frontend/dist"], - "type": "assets" - } - }, - "output_env_file": ".env", - "defaults": { - "build": { - "packtool": "mops sources" - } - } -} diff --git a/motoko/hello_world/icp.yaml b/motoko/hello_world/icp.yaml index a4edd3a0e..3c1670447 100644 --- a/motoko/hello_world/icp.yaml +++ b/motoko/hello_world/icp.yaml @@ -1,10 +1,9 @@ canisters: - name: backend recipe: - type: "@dfinity/motoko@v4.1.0" + type: https://raw.githubusercontent.com/dfinity/icp-cli-recipes/bc9581d9258d2d7feb15ab4ae8d04baf923b985f/recipes/motoko/recipe.hbs configuration: - main: backend/app.mo - candid: backend/backend.did + name: backend - name: frontend recipe: diff --git a/motoko/hello_world/mops.toml b/motoko/hello_world/mops.toml index dc89d0891..49573d4b5 100644 --- a/motoko/hello_world/mops.toml +++ b/motoko/hello_world/mops.toml @@ -1,11 +1,15 @@ [toolchain] -moc = "1.5.1" +moc = "1.8.2" [dependencies] -core = "2.4.0" +core = "2.5.0" [moc] # M0236: use context dot notation (e.g. x.toText() instead of Nat.toText(x)) # M0237: redundant explicit implicit arguments (e.g. Nat.compare is inferred automatically) # M0223: redundant type instantiation (e.g. Array.tabulate instead of Array.tabulate) -args = ["-W=M0236,M0237,M0223"] +args = ["--default-persistent-actors", "-W=M0236,M0237,M0223"] + +[canisters.backend] +main = "backend/app.mo" +candid = "backend/backend.did" diff --git a/motoko/who_am_i/.devcontainer/devcontainer.json b/motoko/who_am_i/.devcontainer/devcontainer.json deleted file mode 100644 index ebb0b8bcc..000000000 --- a/motoko/who_am_i/.devcontainer/devcontainer.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "ICP Dev Environment", - "image": "ghcr.io/dfinity/icp-dev-env-slim:22", - "forwardPorts": [4943, 5173], - "portsAttributes": { - "4943": { - "label": "dfx", - "onAutoForward": "ignore" - }, - "5173": { - "label": "vite", - "onAutoForward": "openBrowser" - } - }, - "customizations": { - "vscode": { - "extensions": ["dfinity-foundation.vscode-motoko"] - } - } -} diff --git a/motoko/who_am_i/.gitignore b/motoko/who_am_i/.gitignore index 2a822d5c2..fcacb2b70 100644 --- a/motoko/who_am_i/.gitignore +++ b/motoko/who_am_i/.gitignore @@ -1 +1 @@ -src/internet_identity_app_frontend/src/bindings/ +src/frontend/src/bindings/ diff --git a/motoko/who_am_i/BUILD.md b/motoko/who_am_i/BUILD.md deleted file mode 100644 index 8877d3ed2..000000000 --- a/motoko/who_am_i/BUILD.md +++ /dev/null @@ -1,26 +0,0 @@ -# Continue building locally - -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. - -### 1. Install developer tools - -Install [Node.js](https://nodejs.org/en/download/) and [icp-cli](https://cli.internetcomputer.org): - -```bash -npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm -``` - -Then navigate into your project's directory that you downloaded from ICP Ninja. - -### 2. Deploy locally - -Start the local network and deploy the project: - -```bash -icp network start -d -icp deploy -``` - -## 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/who_am_i/CODESPACE.md b/motoko/who_am_i/CODESPACE.md new file mode 120000 index 000000000..17b02d2b4 --- /dev/null +++ b/motoko/who_am_i/CODESPACE.md @@ -0,0 +1 @@ +../../.devcontainer/CODESPACE.md \ No newline at end of file diff --git a/motoko/who_am_i/Makefile b/motoko/who_am_i/Makefile new file mode 100644 index 000000000..e5eba77f5 --- /dev/null +++ b/motoko/who_am_i/Makefile @@ -0,0 +1,14 @@ +.PHONY: test + +test: + @echo "--- Testing whoami returns a principal ---" + @result=$$(icp canister call backend whoami '()') && \ + echo "$$result" && \ + echo "$$result" | grep -q 'principal' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "--- Testing whoami is deterministic ---" + @result1=$$(icp canister call backend whoami '()') && \ + result2=$$(icp canister call backend whoami '()') && \ + [ "$$result1" = "$$result2" ] && \ + echo "PASS: $$result1" || (echo "FAIL: $$result1 != $$result2" && exit 1) diff --git a/motoko/who_am_i/README.md b/motoko/who_am_i/README.md index f64514f10..ab0df3a0d 100644 --- a/motoko/who_am_i/README.md +++ b/motoko/who_am_i/README.md @@ -6,13 +6,13 @@ Who am I? demonstrates how entities on the Internet Computer are identified. Every entity, such as a user or canister smart contract, has a principal identifier. Principals can be used for identification and authentication. Who am I? uses Internet Identity (II) for user authentication, then displays the principal identifier associated with that Internet Identity on the user interface. -## Deploying from ICP Ninja +## Try in browser -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 GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/dfinity/examples?devcontainer_path=.devcontainer%2Fmotoko-who-am-i%2Fdevcontainer.json&ref=feat%2Fcodespaces) -[![Open in ICP Ninja](https://icp.ninja/assets/open.svg)](https://icp.ninja/i?g=https://github.com/dfinity/examples/motoko/who_am_i) +Opens a pre-configured environment with the ICP toolchain installed. The local network starts and canisters are deployed automatically. You can browse all your Codespaces at [github.com/codespaces](https://github.com/codespaces). -> **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.internetcomputer.org), which is what developers should use for local development. +> **Note:** Authentication uses production [Internet Identity](https://id.ai) rather than a local test instance. You will see your real principal identifier. ## Build and deploy from the command line @@ -30,36 +30,35 @@ git clone https://github.com/dfinity/examples cd examples/motoko/who_am_i ``` -### Deployment +### Deploy -Start the local network: +Start the local network and deploy: ```bash icp network start -d +icp deploy ``` -Deploy the canisters: +The frontend is served by the asset canister. To run the Vite dev server with hot reload during frontend development: ```bash -icp deploy +npm run dev ``` -Stop the local network when done: +## Ready to deploy on mainnet? -```bash -icp network stop -``` +Codespaces is ideal for learning and local experimentation. When you're ready for mainnet, [install icp-cli locally](https://cli.internetcomputer.org) and follow the [mainnet deployment guide](https://cli.internetcomputer.org/0.2/guides/deploying-to-mainnet.md). Mainnet requires ICP tokens and cycles — managing identities securely is much better from your own machine. ## Updating the Candid interface -The `src/internet_identity_app_backend/internet_identity_app_backend.did` file defines the backend canister's public interface. The frontend TypeScript bindings are auto-generated from this file during the frontend build. +The `src/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 src/internet_identity_app_backend/internet_identity_app_backend.did src/internet_identity_app_backend/main.mo +mops build backend --idl ``` ## Security considerations and best practices -If you base your application on this example, it is recommended that you familiarize yourself with and adhere to the [security best practices](https://docs.internetcomputer.org/building-apps/security/overview) for developing on ICP. This example may not implement all the best practices. +If you base your application on this example, it is recommended that you familiarize yourself with and adhere to the [security best practices](https://docs.internetcomputer.org/guides/security/overview) for developing on ICP. This example may not implement all the best practices. diff --git a/motoko/who_am_i/dfx.json b/motoko/who_am_i/dfx.json deleted file mode 100644 index e7ba2df8b..000000000 --- a/motoko/who_am_i/dfx.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "canisters": { - "internet_identity_app_backend": { - "main": "src/internet_identity_app_backend/main.mo", - "type": "motoko" - }, - "internet_identity_app_frontend": { - "dependencies": ["internet_identity_app_backend"], - "internet_identity_app_frontend": { - "entrypoint": "src/internet_identity_app_frontend/index.html" - }, - "source": ["src/internet_identity_app_frontend/dist"], - "type": "assets" - }, - "internet_identity": { - "candid": "https://github.com/dfinity/internet-identity/releases/latest/download/internet_identity.did", - "type": "custom", - "specified_id": "rdmx6-jaaaa-aaaaa-aaadq-cai", - "remote": { - "id": { - "ic": "rdmx6-jaaaa-aaaaa-aaadq-cai" - } - }, - "wasm": "https://github.com/dfinity/internet-identity/releases/latest/download/internet_identity_production.wasm.gz" - }, - "internet_identity_frontend": { - "candid": "https://raw.githubusercontent.com/dfinity/internet-identity/refs/heads/main/src/internet_identity_frontend/internet_identity_frontend.did", - "type": "custom", - "specified_id": "uqzsh-gqaaa-aaaaq-qaada-cai", - "remote": { - "id": { - "ic": "uqzsh-gqaaa-aaaaq-qaada-cai" - } - }, - "wasm": "https://github.com/dfinity/internet-identity/releases/latest/download/internet_identity_frontend.wasm.gz", - "init_arg": "(record { fetch_root_key = opt true; dev_csp = opt true; backend_canister_id = principal \"rdmx6-jaaaa-aaaaa-aaadq-cai\"; analytics_config = null; related_origins = opt vec { \"http://uqzsh-gqaaa-aaaaq-qaada-cai.localhost:4943\" }; backend_origin = \"http://rdmx6-jaaaa-aaaaa-aaadq-cai.localhost:4943\"; captcha_config = opt record { max_unsolved_captchas = 50 : nat64; captcha_trigger = variant { Static = variant { CaptchaDisabled } } }})" - } - }, - "output_env_file": ".env", - "defaults": { - "build": { - "packtool": "mops sources" - } - } -} diff --git a/motoko/who_am_i/icp.yaml b/motoko/who_am_i/icp.yaml index 6cf444682..4cd8aef9c 100644 --- a/motoko/who_am_i/icp.yaml +++ b/motoko/who_am_i/icp.yaml @@ -4,18 +4,17 @@ networks: ii: true canisters: - - name: internet_identity_app_backend + - name: backend recipe: - type: "@dfinity/motoko@v4.1.0" + type: https://raw.githubusercontent.com/dfinity/icp-cli-recipes/bc9581d9258d2d7feb15ab4ae8d04baf923b985f/recipes/motoko/recipe.hbs configuration: - main: src/internet_identity_app_backend/main.mo - candid: src/internet_identity_app_backend/internet_identity_app_backend.did + name: backend - - name: internet_identity_app_frontend + - name: frontend recipe: type: "@dfinity/asset-canister@v2.1.0" configuration: - dir: src/internet_identity_app_frontend/dist + dir: src/frontend/dist build: - - npm install --prefix src/internet_identity_app_frontend - - npm run build --prefix src/internet_identity_app_frontend + - npm install --prefix src/frontend + - npm run build --prefix src/frontend diff --git a/motoko/who_am_i/mops.toml b/motoko/who_am_i/mops.toml index dc89d0891..85ea333c4 100644 --- a/motoko/who_am_i/mops.toml +++ b/motoko/who_am_i/mops.toml @@ -1,11 +1,15 @@ [toolchain] -moc = "1.5.1" +moc = "1.8.2" [dependencies] -core = "2.4.0" +core = "2.5.0" [moc] # M0236: use context dot notation (e.g. x.toText() instead of Nat.toText(x)) # M0237: redundant explicit implicit arguments (e.g. Nat.compare is inferred automatically) # M0223: redundant type instantiation (e.g. Array.tabulate instead of Array.tabulate) -args = ["-W=M0236,M0237,M0223"] +args = ["--default-persistent-actors", "-W=M0236,M0237,M0223"] + +[canisters.backend] +main = "src/backend/main.mo" +candid = "src/backend/backend.did" diff --git a/motoko/who_am_i/package.json b/motoko/who_am_i/package.json index 7684efc81..969610713 100644 --- a/motoko/who_am_i/package.json +++ b/motoko/who_am_i/package.json @@ -7,6 +7,6 @@ }, "type": "module", "workspaces": [ - "src/internet_identity_app_frontend" + "src/frontend" ] } diff --git a/motoko/who_am_i/src/internet_identity_app_backend/internet_identity_app_backend.did b/motoko/who_am_i/src/backend/backend.did similarity index 100% rename from motoko/who_am_i/src/internet_identity_app_backend/internet_identity_app_backend.did rename to motoko/who_am_i/src/backend/backend.did diff --git a/motoko/who_am_i/src/internet_identity_app_backend/main.mo b/motoko/who_am_i/src/backend/main.mo similarity index 82% rename from motoko/who_am_i/src/internet_identity_app_backend/main.mo rename to motoko/who_am_i/src/backend/main.mo index f011aecd3..70015e177 100644 --- a/motoko/who_am_i/src/internet_identity_app_backend/main.mo +++ b/motoko/who_am_i/src/backend/main.mo @@ -1,6 +1,6 @@ import Principal "mo:core/Principal"; -persistent actor Whoami { +actor Whoami { public query (message) func whoami() : async Principal { message.caller; }; diff --git a/motoko/who_am_i/src/internet_identity_app_frontend/index.css b/motoko/who_am_i/src/frontend/index.css similarity index 100% rename from motoko/who_am_i/src/internet_identity_app_frontend/index.css rename to motoko/who_am_i/src/frontend/index.css diff --git a/motoko/who_am_i/src/internet_identity_app_frontend/index.html b/motoko/who_am_i/src/frontend/index.html similarity index 100% rename from motoko/who_am_i/src/internet_identity_app_frontend/index.html rename to motoko/who_am_i/src/frontend/index.html diff --git a/rust/who_am_i/src/internet_identity_app_frontend/package.json b/motoko/who_am_i/src/frontend/package.json similarity index 91% rename from rust/who_am_i/src/internet_identity_app_frontend/package.json rename to motoko/who_am_i/src/frontend/package.json index 49a3f1ace..412b87e6d 100644 --- a/rust/who_am_i/src/internet_identity_app_frontend/package.json +++ b/motoko/who_am_i/src/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "internet_identity_app_frontend", + "name": "frontend", "private": true, "type": "module", "scripts": { diff --git a/motoko/who_am_i/src/internet_identity_app_frontend/public/favicon.ico b/motoko/who_am_i/src/frontend/public/favicon.ico similarity index 100% rename from motoko/who_am_i/src/internet_identity_app_frontend/public/favicon.ico rename to motoko/who_am_i/src/frontend/public/favicon.ico diff --git a/motoko/who_am_i/src/internet_identity_app_frontend/src/App.jsx b/motoko/who_am_i/src/frontend/src/App.jsx similarity index 100% rename from motoko/who_am_i/src/internet_identity_app_frontend/src/App.jsx rename to motoko/who_am_i/src/frontend/src/App.jsx diff --git a/rust/who_am_i/src/internet_identity_app_frontend/src/actor.js b/motoko/who_am_i/src/frontend/src/actor.js similarity index 77% rename from rust/who_am_i/src/internet_identity_app_frontend/src/actor.js rename to motoko/who_am_i/src/frontend/src/actor.js index becf24513..1e4ac04ad 100644 --- a/rust/who_am_i/src/internet_identity_app_frontend/src/actor.js +++ b/motoko/who_am_i/src/frontend/src/actor.js @@ -1,15 +1,15 @@ import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"; -import { createActor } from "./bindings/internet_identity_app_backend"; +import { createActor } from "./bindings/backend"; const canisterEnv = safeGetCanisterEnv(); const canisterId = - canisterEnv?.["PUBLIC_CANISTER_ID:internet_identity_app_backend"] ?? - process.env.CANISTER_ID_INTERNET_IDENTITY_APP_BACKEND; + canisterEnv?.["PUBLIC_CANISTER_ID:backend"] ?? + process.env.CANISTER_ID_BACKEND; if (!canisterId) { throw new Error( - "Canister ID for 'internet_identity_app_backend' not found. Run 'icp deploy' or 'dfx deploy' first." + "Canister ID for 'backend' not found. Run 'icp deploy' first." ); } diff --git a/motoko/who_am_i/src/internet_identity_app_frontend/src/main.jsx b/motoko/who_am_i/src/frontend/src/main.jsx similarity index 100% rename from motoko/who_am_i/src/internet_identity_app_frontend/src/main.jsx rename to motoko/who_am_i/src/frontend/src/main.jsx diff --git a/rust/who_am_i/src/internet_identity_app_frontend/vite.config.js b/motoko/who_am_i/src/frontend/vite.config.js similarity index 50% rename from rust/who_am_i/src/internet_identity_app_frontend/vite.config.js rename to motoko/who_am_i/src/frontend/vite.config.js index 41d03427c..8bb840ac3 100644 --- a/rust/who_am_i/src/internet_identity_app_frontend/vite.config.js +++ b/motoko/who_am_i/src/frontend/vite.config.js @@ -4,9 +4,8 @@ import react from "@vitejs/plugin-react"; import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite"; function getDevServerConfig() { - // Try icp-cli first try { - const canisterId = execSync("icp canister status internet_identity_app_backend -e local -i", { + const canisterId = execSync("icp canister status backend -e local -i", { encoding: "utf-8", stdio: "pipe", }).trim(); @@ -20,7 +19,7 @@ function getDevServerConfig() { replicaPort: "8000", headers: { "Set-Cookie": `ic_env=${encodeURIComponent( - `ic_root_key=${networkStatus.root_key}&PUBLIC_CANISTER_ID:internet_identity_app_backend=${canisterId}` + `ic_root_key=${networkStatus.root_key}&PUBLIC_CANISTER_ID:backend=${canisterId}` )}; SameSite=Lax;`, }, proxy: { @@ -29,46 +28,12 @@ function getDevServerConfig() { }; } 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 internet_identity_app_backend", - { - encoding: "utf-8", - stdio: "pipe", - } - ).trim(); - return { - replicaPort: "4943", - headers: { - "Set-Cookie": `ic_env=${encodeURIComponent( - `ic_root_key=${rootKeyHex}&PUBLIC_CANISTER_ID:internet_identity_app_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" + "No local network running. Start with:\n icp network start -d && icp 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_"]); const devConfig = command === "serve" ? getDevServerConfig() : undefined; @@ -77,14 +42,13 @@ export default defineConfig(({ command, mode }) => { plugins: [ react(), icpBindgen({ - didFile: - "../internet_identity_app_backend/internet_identity_app_backend.did", + didFile: "../backend/backend.did", outDir: "./src/bindings", }), ], define: { - "process.env.CANISTER_ID_INTERNET_IDENTITY_APP_BACKEND": JSON.stringify( - env.CANISTER_ID_INTERNET_IDENTITY_APP_BACKEND + "process.env.CANISTER_ID_BACKEND": JSON.stringify( + env.CANISTER_ID_BACKEND ), "process.env.REPLICA_PORT": JSON.stringify(devConfig?.replicaPort ?? ""), }, diff --git a/rust/hello_world/.devcontainer/devcontainer.json b/rust/hello_world/.devcontainer/devcontainer.json deleted file mode 100644 index ebb0b8bcc..000000000 --- a/rust/hello_world/.devcontainer/devcontainer.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "ICP Dev Environment", - "image": "ghcr.io/dfinity/icp-dev-env-slim:22", - "forwardPorts": [4943, 5173], - "portsAttributes": { - "4943": { - "label": "dfx", - "onAutoForward": "ignore" - }, - "5173": { - "label": "vite", - "onAutoForward": "openBrowser" - } - }, - "customizations": { - "vscode": { - "extensions": ["dfinity-foundation.vscode-motoko"] - } - } -} diff --git a/rust/hello_world/BUILD.md b/rust/hello_world/BUILD.md deleted file mode 100644 index ffb6557ec..000000000 --- a/rust/hello_world/BUILD.md +++ /dev/null @@ -1,26 +0,0 @@ -# Continue building locally - -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. - -### 1. Install developer tools - -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 -``` - -Then navigate into your project's directory that you downloaded from ICP Ninja. - -### 2. Deploy locally - -Start the local network and deploy the project: - -```bash -icp network start -d -icp deploy -``` - -## 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/hello_world/CODESPACE.md b/rust/hello_world/CODESPACE.md new file mode 120000 index 000000000..17b02d2b4 --- /dev/null +++ b/rust/hello_world/CODESPACE.md @@ -0,0 +1 @@ +../../.devcontainer/CODESPACE.md \ No newline at end of file diff --git a/rust/hello_world/Makefile b/rust/hello_world/Makefile new file mode 100644 index 000000000..269592eca --- /dev/null +++ b/rust/hello_world/Makefile @@ -0,0 +1,15 @@ +.PHONY: test + +test: + @echo "--- Testing default greeting ---" + @result=$$(icp canister call backend greet '("World")') && \ + echo "$$result" && \ + echo "$$result" | grep -q 'Hello, World!' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "--- Testing set_greeting ---" + @icp canister call backend set_greeting '("Hi, ")' + @result=$$(icp canister call backend greet '("Alice")') && \ + echo "$$result" && \ + echo "$$result" | grep -q 'Hi, Alice!' && \ + echo "PASS" || (echo "FAIL" && exit 1) diff --git a/rust/hello_world/README.md b/rust/hello_world/README.md index 468adb75d..26d10c261 100644 --- a/rust/hello_world/README.md +++ b/rust/hello_world/README.md @@ -17,20 +17,18 @@ The frontend provides a simple form where users can enter their name and receive The `/backend` folder contains the Rust canister source code. The `/frontend` folder contains web assets for the application's user interface. The user interface is written with plain JavaScript, but any frontend framework can be used. -## Deploying from ICP Ninja +## Try in browser -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 GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/dfinity/examples?devcontainer_path=.devcontainer%2Frust-hello-world%2Fdevcontainer.json&ref=feat%2Fcodespaces) -[![Open in ICP Ninja](https://icp.ninja/assets/open.svg)](https://icp.ninja/i?g=https://github.com/dfinity/examples/rust/hello_world) - -> **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. +Opens a pre-configured environment with the ICP toolchain installed. The local network starts and canisters are deployed automatically. You can browse all your Codespaces at [github.com/codespaces](https://github.com/codespaces). ## 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` +- [x] Install [icp-cli](https://cli.internetcomputer.org): `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` ### Install @@ -43,24 +41,13 @@ cd examples/rust/hello_world ### Deployment -Start the local network: +Start the local network and deploy: ```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. @@ -74,4 +61,4 @@ candid-extractor target/wasm32-unknown-unknown/release/backend.wasm > backend/ba ## Security considerations and best practices -If you base your application on this example, it is recommended that you familiarize yourself with and adhere to the [security best practices](https://docs.internetcomputer.org/building-apps/security/overview) for developing on ICP. This example may not implement all the best practices. +If you base your application on this example, it is recommended that you familiarize yourself with and adhere to the [security best practices](https://docs.internetcomputer.org/guides/security/overview) for developing on ICP. This example may not implement all the best practices. diff --git a/rust/hello_world/dfx.json b/rust/hello_world/dfx.json deleted file mode 100644 index 6ed5a89e5..000000000 --- a/rust/hello_world/dfx.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "canisters": { - "backend": { - "candid": "backend/backend.did", - "type": "custom", - "shrink": true, - "gzip": true, - "wasm": "target/wasm32-unknown-unknown/release/backend.wasm", - "build": [ - "cargo build --target wasm32-unknown-unknown --release -p backend", - "candid-extractor target/wasm32-unknown-unknown/release/backend.wasm > backend/backend.did" - ], - "metadata": [ - { - "name": "candid:service" - } - ] - }, - "frontend": { - "dependencies": ["backend"], - "frontend": { - "entrypoint": "frontend/index.html" - }, - "source": ["frontend/dist"], - "type": "assets" - } - }, - "output_env_file": ".env" -} diff --git a/rust/who_am_i/.devcontainer/devcontainer.json b/rust/who_am_i/.devcontainer/devcontainer.json deleted file mode 100644 index ebb0b8bcc..000000000 --- a/rust/who_am_i/.devcontainer/devcontainer.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "ICP Dev Environment", - "image": "ghcr.io/dfinity/icp-dev-env-slim:22", - "forwardPorts": [4943, 5173], - "portsAttributes": { - "4943": { - "label": "dfx", - "onAutoForward": "ignore" - }, - "5173": { - "label": "vite", - "onAutoForward": "openBrowser" - } - }, - "customizations": { - "vscode": { - "extensions": ["dfinity-foundation.vscode-motoko"] - } - } -} diff --git a/rust/who_am_i/.gitignore b/rust/who_am_i/.gitignore index 2a822d5c2..fcacb2b70 100644 --- a/rust/who_am_i/.gitignore +++ b/rust/who_am_i/.gitignore @@ -1 +1 @@ -src/internet_identity_app_frontend/src/bindings/ +src/frontend/src/bindings/ diff --git a/rust/who_am_i/BUILD.md b/rust/who_am_i/BUILD.md deleted file mode 100644 index 8877d3ed2..000000000 --- a/rust/who_am_i/BUILD.md +++ /dev/null @@ -1,26 +0,0 @@ -# Continue building locally - -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. - -### 1. Install developer tools - -Install [Node.js](https://nodejs.org/en/download/) and [icp-cli](https://cli.internetcomputer.org): - -```bash -npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm -``` - -Then navigate into your project's directory that you downloaded from ICP Ninja. - -### 2. Deploy locally - -Start the local network and deploy the project: - -```bash -icp network start -d -icp deploy -``` - -## 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/who_am_i/CODESPACE.md b/rust/who_am_i/CODESPACE.md new file mode 120000 index 000000000..17b02d2b4 --- /dev/null +++ b/rust/who_am_i/CODESPACE.md @@ -0,0 +1 @@ +../../.devcontainer/CODESPACE.md \ No newline at end of file diff --git a/rust/who_am_i/Cargo.lock b/rust/who_am_i/Cargo.lock index b830c16d5..bc1209943 100644 --- a/rust/who_am_i/Cargo.lock +++ b/rust/who_am_i/Cargo.lock @@ -298,7 +298,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] -name = "internet_identity_app_backend" +name = "backend" version = "0.1.0" dependencies = [ "candid", diff --git a/rust/who_am_i/Cargo.toml b/rust/who_am_i/Cargo.toml index b72184e38..4c16df46e 100644 --- a/rust/who_am_i/Cargo.toml +++ b/rust/who_am_i/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["src/internet_identity_app_backend"] +members = ["src/backend"] resolver = "2" diff --git a/rust/who_am_i/Makefile b/rust/who_am_i/Makefile new file mode 100644 index 000000000..e5eba77f5 --- /dev/null +++ b/rust/who_am_i/Makefile @@ -0,0 +1,14 @@ +.PHONY: test + +test: + @echo "--- Testing whoami returns a principal ---" + @result=$$(icp canister call backend whoami '()') && \ + echo "$$result" && \ + echo "$$result" | grep -q 'principal' && \ + echo "PASS" || (echo "FAIL" && exit 1) + + @echo "--- Testing whoami is deterministic ---" + @result1=$$(icp canister call backend whoami '()') && \ + result2=$$(icp canister call backend whoami '()') && \ + [ "$$result1" = "$$result2" ] && \ + echo "PASS: $$result1" || (echo "FAIL: $$result1 != $$result2" && exit 1) diff --git a/rust/who_am_i/README.md b/rust/who_am_i/README.md index 236aa318e..a9ee448b9 100644 --- a/rust/who_am_i/README.md +++ b/rust/who_am_i/README.md @@ -6,13 +6,13 @@ Who am I? demonstrates how entities on the Internet Computer are identified. Every entity, such as a user or canister smart contract, has a principal identifier. Principals can be used for identification and authentication. Who am I? uses Internet Identity (II) for user authentication, then displays the principal identifier associated with that Internet Identity on the user interface. -## Deploying from ICP Ninja +## Try in browser -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 GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/dfinity/examples?devcontainer_path=.devcontainer%2Frust-who-am-i%2Fdevcontainer.json&ref=feat%2Fcodespaces) -[![Open in ICP Ninja](https://icp.ninja/assets/open.svg)](https://icp.ninja/i?g=https://github.com/dfinity/examples/rust/who_am_i) +Opens a pre-configured environment with the ICP toolchain installed. The local network starts and canisters are deployed automatically. You can browse all your Codespaces at [github.com/codespaces](https://github.com/codespaces). -> **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.internetcomputer.org), which is what developers should use for local development. +> **Note:** Authentication uses production [Internet Identity](https://id.ai) rather than a local test instance. You will see your real principal identifier. ## Build and deploy from the command line @@ -30,37 +30,36 @@ git clone https://github.com/dfinity/examples cd examples/rust/who_am_i ``` -### Deployment +### Deploy -Start the local network: +Start the local network and deploy: ```bash icp network start -d +icp deploy ``` -Deploy the canisters: +The frontend is served by the asset canister. To run the Vite dev server with hot reload during frontend development: ```bash -icp deploy +npm run dev ``` -Stop the local network when done: +## Ready to deploy on mainnet? -```bash -icp network stop -``` +Codespaces is ideal for learning and local experimentation. When you're ready for mainnet, [install icp-cli locally](https://cli.internetcomputer.org) and follow the [mainnet deployment guide](https://cli.internetcomputer.org/0.2/guides/deploying-to-mainnet.md). Mainnet requires ICP tokens and cycles — managing identities securely is much better from your own machine. ## Updating the Candid interface -The `src/internet_identity_app_backend/internet_identity_app_backend.did` file defines the backend canister's public interface. The frontend TypeScript bindings are auto-generated from this file during the frontend build. +The `src/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 internet_identity_app_backend -candid-extractor target/wasm32-unknown-unknown/release/internet_identity_app_backend.wasm > src/internet_identity_app_backend/internet_identity_app_backend.did +icp build backend +candid-extractor target/wasm32-unknown-unknown/release/backend.wasm > src/backend/backend.did ``` ## Security considerations and best practices -If you base your application on this example, it is recommended that you familiarize yourself with and adhere to the [security best practices](https://docs.internetcomputer.org/building-apps/security/overview) for developing on ICP. This example may not implement all the best practices. +If you base your application on this example, it is recommended that you familiarize yourself with and adhere to the [security best practices](https://docs.internetcomputer.org/guides/security/overview) for developing on ICP. This example may not implement all the best practices. diff --git a/rust/who_am_i/dfx.json b/rust/who_am_i/dfx.json deleted file mode 100644 index a4c690961..000000000 --- a/rust/who_am_i/dfx.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "canisters": { - "internet_identity_app_backend": { - "candid": "src/internet_identity_app_backend/internet_identity_app_backend.did", - "package": "internet_identity_app_backend", - "type": "rust" - }, - "internet_identity_app_frontend": { - "dependencies": ["internet_identity_app_backend"], - "internet_identity_app_frontend": { - "entrypoint": "src/internet_identity_app_frontend/index.html" - }, - "source": ["src/internet_identity_app_frontend/dist"], - "type": "assets" - }, - "internet_identity": { - "candid": "https://github.com/dfinity/internet-identity/releases/latest/download/internet_identity.did", - "type": "custom", - "specified_id": "rdmx6-jaaaa-aaaaa-aaadq-cai", - "remote": { - "id": { - "ic": "rdmx6-jaaaa-aaaaa-aaadq-cai" - } - }, - "wasm": "https://github.com/dfinity/internet-identity/releases/latest/download/internet_identity_production.wasm.gz" - }, - "internet_identity_frontend": { - "candid": "https://raw.githubusercontent.com/dfinity/internet-identity/refs/heads/main/src/internet_identity_frontend/internet_identity_frontend.did", - "type": "custom", - "specified_id": "uqzsh-gqaaa-aaaaq-qaada-cai", - "remote": { - "id": { - "ic": "uqzsh-gqaaa-aaaaq-qaada-cai" - } - }, - "wasm": "https://github.com/dfinity/internet-identity/releases/latest/download/internet_identity_frontend.wasm.gz", - "init_arg": "(record { fetch_root_key = opt true; dev_csp = opt true; backend_canister_id = principal \"rdmx6-jaaaa-aaaaa-aaadq-cai\"; analytics_config = null; related_origins = opt vec { \"http://uqzsh-gqaaa-aaaaq-qaada-cai.localhost:4943\" }; backend_origin = \"http://rdmx6-jaaaa-aaaaa-aaadq-cai.localhost:4943\"; captcha_config = opt record { max_unsolved_captchas = 50 : nat64; captcha_trigger = variant { Static = variant { CaptchaDisabled } } }})" - } - }, - "output_env_file": ".env" -} \ No newline at end of file diff --git a/rust/who_am_i/icp.yaml b/rust/who_am_i/icp.yaml index e354013ab..bae2f48bc 100644 --- a/rust/who_am_i/icp.yaml +++ b/rust/who_am_i/icp.yaml @@ -4,18 +4,18 @@ networks: ii: true canisters: - - name: internet_identity_app_backend + - name: backend recipe: type: "@dfinity/rust@v3.2.0" configuration: - package: internet_identity_app_backend - candid: src/internet_identity_app_backend/internet_identity_app_backend.did + package: backend + candid: src/backend/backend.did - - name: internet_identity_app_frontend + - name: frontend recipe: type: "@dfinity/asset-canister@v2.1.0" configuration: - dir: src/internet_identity_app_frontend/dist + dir: src/frontend/dist build: - - npm install --prefix src/internet_identity_app_frontend - - npm run build --prefix src/internet_identity_app_frontend + - npm install --prefix src/frontend + - npm run build --prefix src/frontend diff --git a/rust/who_am_i/package.json b/rust/who_am_i/package.json index 3544e9035..2d7d15ddb 100644 --- a/rust/who_am_i/package.json +++ b/rust/who_am_i/package.json @@ -3,7 +3,7 @@ "node": ">=16.0.0", "npm": ">=7.0.0" }, - "name": "internet_identity_app", + "name": "who-am-i", "scripts": { "build": "npm run build --workspaces --if-present", "prebuild": "npm run prebuild --workspaces --if-present", @@ -13,6 +13,6 @@ }, "type": "module", "workspaces": [ - "src/internet_identity_app_frontend" + "src/frontend" ] } diff --git a/rust/who_am_i/src/internet_identity_app_backend/Cargo.toml b/rust/who_am_i/src/backend/Cargo.toml similarity index 85% rename from rust/who_am_i/src/internet_identity_app_backend/Cargo.toml rename to rust/who_am_i/src/backend/Cargo.toml index ceda7f950..734a31d6b 100644 --- a/rust/who_am_i/src/internet_identity_app_backend/Cargo.toml +++ b/rust/who_am_i/src/backend/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "internet_identity_app_backend" +name = "backend" version = "0.1.0" edition = "2021" diff --git a/rust/who_am_i/src/internet_identity_app_backend/internet_identity_app_backend.did b/rust/who_am_i/src/backend/backend.did similarity index 100% rename from rust/who_am_i/src/internet_identity_app_backend/internet_identity_app_backend.did rename to rust/who_am_i/src/backend/backend.did diff --git a/rust/who_am_i/src/internet_identity_app_backend/src/lib.rs b/rust/who_am_i/src/backend/src/lib.rs similarity index 100% rename from rust/who_am_i/src/internet_identity_app_backend/src/lib.rs rename to rust/who_am_i/src/backend/src/lib.rs diff --git a/rust/who_am_i/src/internet_identity_app_frontend/index.css b/rust/who_am_i/src/frontend/index.css similarity index 100% rename from rust/who_am_i/src/internet_identity_app_frontend/index.css rename to rust/who_am_i/src/frontend/index.css diff --git a/rust/who_am_i/src/internet_identity_app_frontend/index.html b/rust/who_am_i/src/frontend/index.html similarity index 100% rename from rust/who_am_i/src/internet_identity_app_frontend/index.html rename to rust/who_am_i/src/frontend/index.html diff --git a/motoko/who_am_i/src/internet_identity_app_frontend/package.json b/rust/who_am_i/src/frontend/package.json similarity index 91% rename from motoko/who_am_i/src/internet_identity_app_frontend/package.json rename to rust/who_am_i/src/frontend/package.json index 49a3f1ace..412b87e6d 100644 --- a/motoko/who_am_i/src/internet_identity_app_frontend/package.json +++ b/rust/who_am_i/src/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "internet_identity_app_frontend", + "name": "frontend", "private": true, "type": "module", "scripts": { diff --git a/rust/who_am_i/src/internet_identity_app_frontend/public/favicon.ico b/rust/who_am_i/src/frontend/public/favicon.ico similarity index 100% rename from rust/who_am_i/src/internet_identity_app_frontend/public/favicon.ico rename to rust/who_am_i/src/frontend/public/favicon.ico diff --git a/rust/who_am_i/src/internet_identity_app_frontend/src/App.jsx b/rust/who_am_i/src/frontend/src/App.jsx similarity index 100% rename from rust/who_am_i/src/internet_identity_app_frontend/src/App.jsx rename to rust/who_am_i/src/frontend/src/App.jsx diff --git a/motoko/who_am_i/src/internet_identity_app_frontend/src/actor.js b/rust/who_am_i/src/frontend/src/actor.js similarity index 77% rename from motoko/who_am_i/src/internet_identity_app_frontend/src/actor.js rename to rust/who_am_i/src/frontend/src/actor.js index becf24513..1e4ac04ad 100644 --- a/motoko/who_am_i/src/internet_identity_app_frontend/src/actor.js +++ b/rust/who_am_i/src/frontend/src/actor.js @@ -1,15 +1,15 @@ import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"; -import { createActor } from "./bindings/internet_identity_app_backend"; +import { createActor } from "./bindings/backend"; const canisterEnv = safeGetCanisterEnv(); const canisterId = - canisterEnv?.["PUBLIC_CANISTER_ID:internet_identity_app_backend"] ?? - process.env.CANISTER_ID_INTERNET_IDENTITY_APP_BACKEND; + canisterEnv?.["PUBLIC_CANISTER_ID:backend"] ?? + process.env.CANISTER_ID_BACKEND; if (!canisterId) { throw new Error( - "Canister ID for 'internet_identity_app_backend' not found. Run 'icp deploy' or 'dfx deploy' first." + "Canister ID for 'backend' not found. Run 'icp deploy' first." ); } diff --git a/rust/who_am_i/src/internet_identity_app_frontend/src/main.jsx b/rust/who_am_i/src/frontend/src/main.jsx similarity index 100% rename from rust/who_am_i/src/internet_identity_app_frontend/src/main.jsx rename to rust/who_am_i/src/frontend/src/main.jsx diff --git a/rust/who_am_i/src/internet_identity_app_frontend/tsconfig.json b/rust/who_am_i/src/frontend/tsconfig.json similarity index 100% rename from rust/who_am_i/src/internet_identity_app_frontend/tsconfig.json rename to rust/who_am_i/src/frontend/tsconfig.json diff --git a/motoko/who_am_i/src/internet_identity_app_frontend/vite.config.js b/rust/who_am_i/src/frontend/vite.config.js similarity index 50% rename from motoko/who_am_i/src/internet_identity_app_frontend/vite.config.js rename to rust/who_am_i/src/frontend/vite.config.js index 41d03427c..8bb840ac3 100644 --- a/motoko/who_am_i/src/internet_identity_app_frontend/vite.config.js +++ b/rust/who_am_i/src/frontend/vite.config.js @@ -4,9 +4,8 @@ import react from "@vitejs/plugin-react"; import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite"; function getDevServerConfig() { - // Try icp-cli first try { - const canisterId = execSync("icp canister status internet_identity_app_backend -e local -i", { + const canisterId = execSync("icp canister status backend -e local -i", { encoding: "utf-8", stdio: "pipe", }).trim(); @@ -20,7 +19,7 @@ function getDevServerConfig() { replicaPort: "8000", headers: { "Set-Cookie": `ic_env=${encodeURIComponent( - `ic_root_key=${networkStatus.root_key}&PUBLIC_CANISTER_ID:internet_identity_app_backend=${canisterId}` + `ic_root_key=${networkStatus.root_key}&PUBLIC_CANISTER_ID:backend=${canisterId}` )}; SameSite=Lax;`, }, proxy: { @@ -29,46 +28,12 @@ function getDevServerConfig() { }; } 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 internet_identity_app_backend", - { - encoding: "utf-8", - stdio: "pipe", - } - ).trim(); - return { - replicaPort: "4943", - headers: { - "Set-Cookie": `ic_env=${encodeURIComponent( - `ic_root_key=${rootKeyHex}&PUBLIC_CANISTER_ID:internet_identity_app_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" + "No local network running. Start with:\n icp network start -d && icp 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_"]); const devConfig = command === "serve" ? getDevServerConfig() : undefined; @@ -77,14 +42,13 @@ export default defineConfig(({ command, mode }) => { plugins: [ react(), icpBindgen({ - didFile: - "../internet_identity_app_backend/internet_identity_app_backend.did", + didFile: "../backend/backend.did", outDir: "./src/bindings", }), ], define: { - "process.env.CANISTER_ID_INTERNET_IDENTITY_APP_BACKEND": JSON.stringify( - env.CANISTER_ID_INTERNET_IDENTITY_APP_BACKEND + "process.env.CANISTER_ID_BACKEND": JSON.stringify( + env.CANISTER_ID_BACKEND ), "process.env.REPLICA_PORT": JSON.stringify(devConfig?.replicaPort ?? ""), },